类别 | 解释 | 特色 |
---|---|---|
单一视图 | 即一个View,如TextView | 不包含子View |
视图组 | 即多个View组成的ViewGroup,如LinearLayout | 包含子View |
建立自定义的组件主要围绕着如下五个方面:java
因为Android的2D渲染如今能够比较好的支持硬件加速了,可是在自定义控件进行绘制是仍是有不少api不兼容的,因此在自定义控件的时候,在你不能100%确认你使用的api支持硬件加速的话,最好把硬件加速关闭了,不然有可能出现一些莫名其妙的问题:android
- 硬件加速关闭方法
在清单文件的application节点下进行关闭或者打开,这种方式是做用于整个应用的:canvas
<!--false表示关闭,true表示打开--> android:hardwareAccelerated="false" 复制代码
- 在activity注册时进行关闭或者打开,这种方式只做用于该activity:
<activity android:name=".WebViewTest" android:hardwareAccelerated="false"/> 复制代码
三、在指定View初始化时关闭或者打开,这种方式只做用于该View控件:api
//若是是自定义的view,可在构造方法中调用该方法,便可开启或者关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 复制代码
共有4个,具体以下:bash
// 若是View是在Java代码里面new的,则调用第一个构造函数
public CustomView(Context context) {
super(context);
}
// 若是View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
// 通常是在第二个构造函数里主动调用
// 如View有style属性时
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21以后才使用
// 不会自动调用
// 通常是在第二个构造函数里主动调用
// 如View有style属性时
@TargetApi(21)
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
复制代码
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<!-- 自动解析命名空间 -->
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 添加自定义View -->
<com.zeroxuan.customviewtest.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
复制代码
视图能够经过XML来配置属性和样式,你须要想清楚要添加那些自定义的属性,好比咱们想让用户能够选择形状的颜色、是否显示形状的名称,好比咱们想让视图能够像下面同样配置:app
<com.zeroxuan.customviewtest.CustomView
android:layout_width="wrap_content"
app:shapeColor="#FF0000"
app:displayShapeName="true"
android:layout_height="wrap_content"
···/>
复制代码
为了可以定义shapeColor和displayShapeName,咱们须要在res/values/
中新建一个文件名为custom_view_attrs.xml
的文件(文件名随意
),在这个文件中包含<resources></resources>
标签,添加<declare-styleable name="ShapeSelectorView"></declare-styleable>
标签,标签的name
属性一般是自定义的类名,在declare-styleable
中添加attr
元素,attr
元素是key (“name=”) -- value (“format=”)
的形式:ide
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="shapeColor" format="color"/>
<attr name="displayShapeName" format="boolean"/>
</declare-styleable>
</resources>
复制代码
对于每一个你想自定义的属性你须要定义attr
节点,每一个节点有name和format属性,format属性是咱们指望的值的类型,好比color,dimension,boolean,integer,float等。一旦定义好了属性,你能够像使用自带属性同样使用他们,惟一的区别在于你的自定义属性属于一个不一样的命名空间,你能够在根视图的layout里面定义命名空间,通常状况下你只须要这样指定:http://schemas.android.com/apk/res/<package_name>
,可是你可使用http://schemas.android.com/apk/res-auto
自动解析命名空间。函数
public class CustomView extends View {
private int shapeColor;
private boolean displayShapeName;
public CustomView(Context context) {
super(context);
initCustomView(null);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
initCustomView(attrs);
}
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCustomView(attrs);
}
@TargetApi(21)
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initCustomView(attrs);
}
private void initCustomView(AttributeSet attrs) {
if (attrs == null) {
return;
}
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);
try {
shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);
displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);
} finally {
a.recycle();
}
}
}
复制代码
public class CustomView extends View {
private int shapeColor;
private boolean displayShapeName;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@TargetApi(21)
public CustomView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
if (attrs == null) {
return;
}
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);
try {
shapeColor = a.getColor(R.styleable.CustomView_shapeColor, Color.WHITE);
displayShapeName = a.getBoolean(R.styleable.CustomView_displayShapeName, false);
} finally {
a.recycle();
}
}
}
复制代码
建议使用
方式一
,好比你自定义的View继承自ListView或者TextView的时候,ListView或者TextView内部的构造函数会有一个默认的defStyle, 第二种方法调用时defStyle会传入0,这将覆盖基类中默认的defStyle,进而致使一系列问题。布局
接下来添加一些getter和setter方法post
public class ShapeSelectorView extends View {
// ...
public boolean isDisplayingShapeName() {
return displayShapeName;
}
public void setDisplayingShapeName(boolean state) {
this.displayShapeName = state;
// 当视图属性发生改变的时候可能须要从新绘图
invalidate();
requestLayout();
}
public int getShapeColor() {
return shapeColor;
}
public void setShapeColor(int color) {
this.shapeColor = color;
invalidate();
requestLayout();
}
}
复制代码
当视图属性发生改变的时候可能须要从新绘图,你须要调用`invalidate()`和`requestLayout()`来刷新显示。
draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。
// View.java 的 draw() 方法的简化版大体结构(是大体结构,不是源码哦):
public void draw(Canvas canvas) {
...
drawBackground(Canvas); // 绘制背景(不能重写)
onDraw(Canvas); // 绘制主体
dispatchDraw(Canvas); // 绘制子 View
onDrawForeground(Canvas); // 绘制滑动相关和前景
...
}
复制代码
从上面的代码能够看出,onDraw()
dispatchDraw()
onDrawForeground()
这三个方法在 draw()
中被依次调用,所以它们的遮盖关系就是——dispatchDraw() 绘制的内容盖住 onDraw() 绘制的内容;onDrawForeground() 绘制的内容盖住 dispatchDraw() 绘制的内容。而在它们的外部,则是由 draw() 这个方法做为总的调度。因此,你也能够重写 draw() 方法来作自定义的绘制。
想在滑动边缘渐变、滑动条和前景之间插入绘制代码?虽然这三部分是依次绘制的,但它们被一块儿写进了
onDrawForeground()
方法里,因此你要么把绘制内容插在它们以前,要么把绘制内容插在它们以后。而想往它们之间插入绘制,是作不到的。
因为 draw()
是总调度方法,因此若是把绘制代码写在 super.draw()
的下面,那么这段代码会在其余全部绘制完成以后再执行,也就是说,它的绘制内容会盖住其余的全部绘制内容。
它的效果和重写 onDrawForeground()
,并把绘制代码写在 super.onDrawForeground()
的下面效果是同样的:都会盖住其余的全部内容。
固然了,虽然说它们效果同样,但若是你既重写
draw()
又重写onDrawForeground()
,那么draw()
里的内容仍是会盖住onDrawForeground()
里的内容的。因此严格来说,它们的效果仍是有一点点不同的。
因为 draw()
是总调度方法,因此若是把绘制代码写在 super.draw()
的上面,那么这段代码会在其余全部绘制以前被执行,因此这部分绘制内容会被其余全部的内容盖住,包括背景。
例如:
EditText重写它的 draw() 方法,而后在 super.draw() 的上方插入代码,以此来在全部内容的底部涂上一片绿色:
public AppEditText extends EditText {
...
public void draw(Canvas canvas) {
canvas.drawColor(Color.parseColor("#F0FF0000")); // 涂上红色
super.draw(canvas);
}
}
复制代码
注意:出于效率的考虑,
ViewGroup
默认会绕过draw()
方法,换而直接执行dispatchDraw()
,以此来简化绘制流程。因此若是你自定义了某个 ViewGroup 的子类而且须要在它的除dispatchDraw()
之外的任何一个绘制方法内绘制内容,你可能会须要调用View.setWillNotDraw(false)
这行代码来切换到完整的绘制流程。
其中棕色部分为手机屏幕
View的坐标系统是相对于父控件而言的
/* 获取子View左上角距父View顶部的距离 * 即左上角纵坐标 */
getTop();
/* 获取子View左上角距父View左侧的距离 * 即左上角横坐标 */
getLeft();
/* 获取子View右下角距父View顶部的距离 * 即右下角纵坐标 */
getBottom();
/* 获取子View右下角距父View左侧的距离 * 即右下角横坐标 */
getRight();
复制代码
width = right - left;
height = bottom - top;
复制代码
Android 新增的参数
x
,y
:View的左上角坐标- translationX,translationY:相对于父容器的偏移量(有get/set方法)。
注意:View在平移过程当中,原始位置不会改变。
// 换算关系 x = left + translationX y = top + translationY 复制代码
- 从API21开始增长了z(垂直屏幕方向)和elevation(浮起来的高度,3D)
dp与px(像素)相互转换代码
// dp转为px
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
// px转为dp
public static int px2dp(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
复制代码
ACTION_DOWN–手指刚触摸屏幕
ACTION_MOVE–手指在屏幕上移动
ACTION_UP–手指从屏幕上分开的一瞬间
复制代码
getX (相对于当前View左上角的坐标)
getY
getRawX(相对于屏幕左上角的坐标)
getRawY
复制代码
TouchSlop滑动最小距离
ViewConfiguration.get(getContext()).getScaledTouchSlop();
复制代码
示例
float x = 0, y = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取TouchSlop(滑动最小距离)
float slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent: " + "按下");
Log.e(TAG, "getX: " + event.getX());
Log.e(TAG, "getY: " + event.getY());
Log.e(TAG, "getRawX: " + event.getRawX());
Log.e(TAG, "getRawY: " + event.getRawY());
x = event.getX();
y = event.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent: " + "移动");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent: " + "松开" + x);
if (event.getX() - x > slop) {
Log.e(TAG, "onTouchEvent: " + "往右滑动" + event.getX());
} else if (x - event.getX() > slop) {
Log.e(TAG, "onTouchEvent: " + "往左滑动" + event.getX());
} else {
Log.e(TAG, "onTouchEvent: " + "无效滑动" + event.getX());
}
x = 0;
y = 0;
break;
}
// 返回true,拦截这个事件
// 返回false,不拦截
return true;
}
复制代码
// 解决长按屏幕后没法拖动的现象,可是这样会没法识别长按事件
mGestureDetector.setIsLongpressEnable(false);
复制代码
return mGestureDetector.onTouchEvent(event);
复制代码
private GestureDetector mGestureDetector;
... ...
private void init(Context context){
this.mContext = context;
mGestureDetector = new GestureDetector(mContext,onGestureListener);
mGestureDetector.setOnDoubleTapListener(onDoubleTapListener);
//解决长按屏幕没法拖动,可是会形成没法识别长按事件
//mGestureDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 接管onTouchEvent
return mGestureDetector.onTouchEvent(event);
}
GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
Log.i(TAG, "onDown: 按下");
return true;
}
@Override
public void onShowPress(MotionEvent e) {
Log.i(TAG, "onShowPress: 刚碰上还没松开");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.i(TAG, "onSingleTapUp: 轻轻一碰后立刻松开");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.i(TAG, "onScroll: 按下后拖动");
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i(TAG, "onLongPress: 长按屏幕");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i(TAG, "onFling: 滑动后松开");
return true;
}
};
GestureDetector.OnDoubleTapListener onDoubleTapListener = new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i(TAG, "onSingleTapConfirmed: 严格的单击");
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i(TAG, "onDoubleTap: 双击");
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.i(TAG, "onDoubleTapEvent: 表示发生双击行为");
return true;
}
};
复制代码