View
View的移动
- 不改变布局参数(不会触发layout)
- scrollTo
- 传统动画和属性动画(translationX translationY)
- 改变布局参数(改变LayoutParams)
平滑滑动动画
- Scroller
- ObjectAnimator
View 的四个构造函数
1. 一个参数构造函数
1
constructor(context: Context?) : super(context)
使用Java代码创建 View 的时候,比如
1
new TextView(context)
2. 两个参数构造函数
1
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
inflate XML布局的时候会调用这个构造函数
1
2
3
4
5
6
7
8
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
任何形式的 xml inflate 都是调用的第二个构造函数
3. 三个参数构造函数
1
2
3
4
5
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
这个构造函数并不是用于 infalte xml 布局,而是被第二个构造函数调用设置 defStyleAttr
1
2
3
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
- TextView 通过 xml 布局 inflate 时会调用 第二个构造函数,而第二个构造函数会调用第三个构造函数,设置一个默认的 style
- 可以才动态创建 View 的形式直接调用第三个构造函数
4. 四个参数构造函数
1
2
3
4
5
6
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes)
用法和第三个相同
总结
- 第一个构造函数用于 Java 代码动态创建 View
- 所有的 xml 的 inflate 都是调用第二个构造函数
- 第三个和第四个构造函数用于,Java 代码动态创建 View 和 第二个构造函数间接调用
View 触摸事件分发
常用手势辅助类
- GestureDetector
- ViewFlinger
滑动冲突
常见的滑动冲突
- 外部滑动方向和内部滑动方向不一致
根据滑动是水平滑动还是数值华东判断到底是由谁来拦截事件,最简单的是通过水平和竖直方向移动的距离来判断
- 外部拦截法
重写父容器的onInterceptTouchEvent在内部做响应的拦截即可参考《Android开发探索艺术》408页
伪代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (父容器需要当前点击事件) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
- 内部拦截法
重写子元素的dispatchTouchEvent方法(思考:为什么不是onTouchEvent方法)配合requestDisallowInterceptTouchEvent
伪代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件)) { parent.requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }
- 外部拦截法
重写父容器的onInterceptTouchEvent在内部做响应的拦截即可参考《Android开发探索艺术》408页
伪代码如下
- 外部滑动方向和内部滑动方向一直
- 上面两种情况叠加嵌套
自定 View onMeasure onLayout onDraw
一般分为四种
- 继承View重写onDraw方法 采用这种方式需要自己支持wrap_content 并且padding也需要自己处理 如果由线程或者动画要及时停止,参考View.OnDetachedFromWindow
- 继承ViewGroup派生特殊的Layout 需要适合的处理ViewGroup的 onMeasure onLayout 这两个过程 需要考虑自己的padding和子View的margin和显隐状态 一般LinearLayout等空间是默认不开启绘画功能的,所以在onDraw是无法进行绘制的,需要调用setWillNotDraw进行设置
- 继承特定的View(比如TextView)
- 继承特定的ViewGroup (比如LinearLayout)
onMeasure
- ViewGroup.measureChildWithMargins
如果是自定义 ViewGroup 需要调用 measureChildWithMargins 测量 Child 的宽高,如果自定义 View 是直接继承自 ViewGroup 还需要重写 generateLayoutParams(AttributeSet attrs) 方法 - View.setMeasuredDimension(int measuredWidth, int measuredHeight)
onMeasure 最终需要调用 setMeasuredDimension 设置最终的宽高,而其参数 mesuredWidth 和 measuerdHeight 需要通过方法 resolveSizeAndState 来获取以添加一些额外信息 - resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
为 measuredWidth 和 measuredHeight 添加一些额外信息,比如:《最终值比预期的小,还是比预期的大;是否发生变化》;第三个参数可以通过 child.getMeasuredState() 获取,多个child 可以通过 View.combineMeasuredStates 获取最终值
onLayout
如果是自定义 View 不需要重写这个方法
如果是 ViewGroup 需要遍历调用 child.layout 方法,计算 child 布局的时候需要考虑到:自身的 padding、child 的 margin、child.getVisibility() 等因素
onDraw
ViewGroup 默认是不会调用 onDraw 方法的,需要调用 View.setWillNotDraw(false) 让ViewGroup 调用 onDraw 方法
自定义属性(不用写了,应该都知道)
如何获取到系统属性? ———- 参考KvLayout
view.post 为什么可以获取到 View 的宽高
1
2
3
4
5
6
7
8
public boolean View.post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) { // attachInfo 不为 null 表示已经执行过 dispatchAttachedToWindow
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
1
2
3
4
5
6
7
8
9
10
11
//ViewRootImpl.performTraversals 可知
//执行完 dispatchAttachedToWindow 方法之后就会执行 performMeasuer 方法,
//而在此时通过 mRunQueue.executeActions(info.mHandler);
//发送的 Messager 肯定在 performMeasuer 之后才执行所以可以获取到 宽高
void View.dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
generateLayoutParams
相关方法有三个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// xml infalte 成对象的时候,获取到的 LayoutParams 就是取自父控件的 generateLayoutParams(AttributeSet attrs) 方法
// 如果自定义 View 直接继承自 ViewGroup ,当使用 measureChildWithMargins 测量子 View 的时候,会报错 ViewGroup.LayoutParam 无法转换成 MarginLayoutParams
// 这是因为 ViewGroup.generateLayoutParams(AttributeSet attrs) 默认返回的是 ViewGroup.LayoutParam
// 因此如果你需要支持 margin 属性 generateLayoutParams(AttributeSet attrs) 需要被重写
// 具体重写方式可参考 FrameLayout
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return //TODO;
}
// 当 checkLayoutParams 方法检查到 View 的 LayoutParams 不满足当前 ViewGroup 的要求的时候,
// 会调用 generateLayoutParams(ViewGroup.LayoutParams p) 方法将原先不和的 LayoutParams 转换成满足要求的 LayoutParams
// 因此这个方法需要配合 checkLayoutParams 一起重写
// 具体重写方式可参考 FrameLayout
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return // TODO;
}
// 在调用 addView 的时候,如果添加的 View 没有 LayoutParam 或者调用 addView(View child, int width, int height)
// 都会通过 generateDefaultLayoutParams 方法获取默认 LayoutParam
// 具体重写方式可参考 FrameLayout
protected LayoutParams generateDefaultLayoutParams() {
return //TODO;
}
// 具体作用参考 generateLayoutParams(ViewGroup.LayoutParams p) 的说明
// 具体重写方式可参考 FrameLayout
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return // TODO;
}
需要注意的问题
- 在 onDraw 方法中不要创建复杂对象,容易造成内存抖动
- 在 onDraw 方法中不要调用 invalidate ,造成不断的向消息队列中添加消息调用 onDraw 方法,形成循环
优秀文章