内容简介:创建自定义的组件主要围绕着以下五个方面:共有4个,具体如下:视图可以通过XML来配置属性和样式,你需要想清楚要添加那些自定义的属性,比如我们想让用户可以选择形状的颜色、是否显示形状的名称,比如我们想让视图可以像下面一样配置:
类别 | 解释 | 特点 |
---|---|---|
单一视图 | 即一个View,如TextView | 不包含子View |
视图组 | 即多个View组成的ViewGroup,如LinearLayout | 包含子View |
创建完全自定义的组件
创建自定义的组件主要围绕着以下五个方面:
- 绘图(Drawing): 控制视图的渲染,通常通过覆写onDraw方法来实现
- 交互(Interaction): 控制用户和视图的交互方式,比如OnTouchEvent,gestures
- 尺寸(Measurement): 控制视图内容的维度,通过覆写onMeasure方法
- 属性(Attributes): 在XML中定义视图的属性,使用TypedArray来获取属性值
- 持久化(Persistence): 配置发生改变时保存和恢复状态,通过onSaveInstanceState和onRestoreInstanceState
View类简介
- View类是Android中各种组件的基类,如View是ViewGroup基类
- View表现为显示在屏幕上的各种视图
View的构造函数
共有4个,具体如下:
// 如果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来配置属性和样式,你需要想清楚要添加那些自定义的属性,比如我们想让用户可以选择形状的颜色、是否显示形状的名称,比如我们想让视图可以像下面一样配置:
<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=”)
的形式:
<?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方法
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()
来刷新显示。
Android 绘制顺序
draw()
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()
方法里,所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们之间插入绘制,是做不到的。
写在 super.draw() 的下面
由于 draw()
是总调度方法,所以如果把绘制代码写在 super.draw()
的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。
它的效果和重写 onDrawForeground()
,并把绘制代码写在 super.onDrawForeground()
时的效果是一样的:都会盖住其他的所有内容。
当然了,虽说它们效果一样,但如果你既重写 draw()
又重写 onDrawForeground()
,那么 draw()
里的内容还是会盖住 onDrawForeground()
里的内容的。所以严格来讲,它们的效果还是有一点点不一样的。
写在 super.draw() 的上面
由于 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) 这行代码来切换到完整的绘制流程。
Android坐标系
其中棕色部分为手机屏幕
View坐标系
View的坐标系统是相对于父控件而言的
- 原始位置(不受偏移量影响,单位是像素px)
/* 获取子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); } 复制代码
MotionEvent
- 手指触摸屏幕后产生的事件,典型事件如下:
ACTION_DOWN–手指刚触摸屏幕 ACTION_MOVE–手指在屏幕上移动 ACTION_UP–手指从屏幕上分开的一瞬间 复制代码
- MotionEvent获取点击事件发生的坐标
getX (相对于当前View左上角的坐标) getY getRawX(相对于屏幕左上角的坐标) getRawY 复制代码
-
TouchSlop滑动最小距离
- 滑动小于这个常量,系统将不会认为这是滑动(常量为8dp,使用时系统会自动转为px)
- 获取方式
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; } 复制代码
GestureDetector
- 辅助检测用户的单击、滑动、长按、双击等行为
- 使用
- 创建一个GestureDetector对象并实现OnGestureListener接口,根据需要实现OnDoubleTapListener接口
// 解决长按屏幕后无法拖动的现象,但是这样会无法识别长按事件 mGestureDetector.setIsLongpressEnable(false); 复制代码
- 接管目标View的onTouchEvent方法
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; } }; 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 自定义View基础
- Azure 入门基础:自定义 Table storage 查询条件
- 重新定义堡垒机 构建IT基础设施特权身份银行
- 从0到1Android自定义View(一)零散基础知识
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
你不知道的JavaScript(中卷)
[美] Kyle Simpson / 单业、姜南 / 人民邮电出版社 / 2016-8 / 79.00元
JavaScript这门语言简单易用,很容易上手,但其语言机制复杂微妙,即使是经验丰富的JavaScript开发人员,如果没有认真学习的话也无法真正理解。本套书直面当前JavaScript开发人员不求甚解的大趋势,深入理解语言内部的机制,全面介绍了JavaScript中常被人误解和忽视的重要知识点。本书是其中卷,主要介绍了类型、语法、异步和性能。一起来看看 《你不知道的JavaScript(中卷)》 这本书的介绍吧!