说到那些炫酷的自定义View,就离不开动画。
属性动画和硬件加速
属性动画
ViewPropertyAnimator
1.使用View.animate()创建对象,以及使用ViewPropertyAnimator.translationX()等方法来设置动画;
2.可以连续调用来设置多个动画;
3.可以用setDuration()来设置持续时间;
4.可以用setStartDelay()来设置开始延时;
ObjectAnimator
使用ObjectAnimator.ofxxx()来创建对象,以及使用ObjectAnimator.statr()来主动启动动画。它的优势在于,可以为自定义属性设置动画。
ObjectAnimator animator = ObjectAnimator.ofObject(view, "radius", Utils.dp2px(200));
另外,自定义属性需要设置 getter 和 setter 方法,并且 setter 方法里需要调用 来触发重绘:
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius; invalidate();
}
可以使用 setduration()来设置持续时间;
可以用 setStartDelay()来设置开始延时;
以及其他一些便捷方法。
Interpolator
插值器,用于设置时间完成度到动画完成度的计算公式,直白地说即设置动画的速度曲线,通过setInterpolator(Interpolator)方法来设置.
常用的有
AccelerateDecelerateInterpolator:开始与结束的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator:开始的地方速率改变比较慢,然后开始加速
DecelerateInterpolator:在开始的地方快然后慢
LinearInterpolator: 以常量速率改变
PropertyValuesHolder
用于设置更加详细的动画,例如多个属性应用于同一个对象:
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("radius", Utils.dp2px(200));
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("offset", Utils.dp2px(100));
ObjectAnimator animator = PropertyValuesHolder.ofPropertyValuesHolder(view, holder1, holder2);
或者,配合使用 ,对一个属性分多个段:
Keyframe keyframe1 = Keyframe.ofFloat(0, Utils.dpToPixel(100));
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, Utils.dpToPixel(250));
Keyframe keyframe3 = Keyframe.ofFloat(1, Utils.dpToPixel(200));
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("radius", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder);
AnimatorSet
将多个 Animator 合并在一起使用,先后顺序或并列顺序都可以:
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animator1, animator2);
animatorSet.start();
TypeEvaluator
用于设置动画完成度到属性具体值的计算公式。默认的offInt()和ofFloat()已经有了自带的IntEvaluator和FloatEvaluator,但有的时候需要自己设置 Evaluator。例如,对于颜色,需要为 int 类型的颜色设置 ArgbEvaluator,而不是让它们使用 IntEvaluator:
animator.setEvaluator(new ArgbEvaluator());
硬件加速
硬件加速是什么
1.使用 CPU 绘制到 Bitmap,然后把 Bitmap 贴到屏幕,就是软件绘制;
2.使用 CPU 把绘制内容转换成 GPU 操作,交给 GPU,由 GPU 负责真正的绘制,就叫硬件绘制;
3.使用 GPU 绘制就叫做硬件加速
怎么就加速了?
1.GPU 分摊了工作
2.GPU 绘制简单图形(例如方形、圆形、直线)在硬件设计上具有先天优势,会更快
3.流程得到优化(重绘流程涉及的内容更少)
硬件加速的缺陷:
兼容性。由于使用 GPU 的绘制(暂时)无法完成某些绘制,因此对于一些特定的 API,需要关闭硬件加速来转回到使用 CPU 进行绘制。
离屏缓冲:
1.离屏缓冲是什么:单独的一个绘制 View(或 View 的一部分)的区域
2.setLayerType() 和 saveLayer()
2.setLayerType() 是对整个 View,不能针对 onDraw() 里面的某一具体过程
3.这个方法常用来关闭硬件加速,但它的定位和定义都不只是一个「硬件加速开关」。 它的作用是为绘制设置一个离屏缓冲,让后面的绘制都单独写在这个离屏缓冲内。如果参数填写LAYER_TYPE_SOFTWARE,会把离屏缓冲设置为一个 Bitmap ,即使用软件绘制来进行缓冲,这样就导致在设置离屏缓冲的同时,将硬件加速关闭了。但需要知道,这个方法被用来关闭硬件加速,只是因为 Android 并没有提供一个便捷的方法在 View 级别简单地开关硬件加速而已。
4.saveLayer() 是针对 Canvas 的,所以在 onDraw() 里可以使用 saveLayer() 来圈出具体哪部分绘制要用离屏缓冲
5.然而……最新的文档表示这个方法太重了,能不用就别用,尽量用 setLayerType() 代替。
自定义布局
布局过程
1.确定每个 View 的位置和尺寸 作用:为绘制和触摸范围做支持
2.绘制:知道往哪里绘制
3.触摸反馈:知道用户点的是哪里
流程
1.从整体看:
测量流程:从根 View 递归调用每一级子 View 的 measure() 方法,对它们进行测量
布局流程:从根 View 递归调用每一级子 View 的 layout() 方法,把测量过程得出的子 View的位置和尺寸传给子 View,子 View 保存
2.从个体看,对于每个 View:
- 运行前,开发者在 xml 文件里写入对 View 的布局要求 layout_xxx
- 父 View 在自己的 onMeasure() 中,根据开发者在 xml 中写的对子 View 的要求,和自己的可用空间,得出对子 View 的具体尺寸要求
- 子 View 在自己的 onMeasure() 中,根据自己的特性算出自己的期望尺寸(如果是 ViewGroup,还会在这里调用每个子 View 的 measure() 进行测量)
- 父 View 在子 View 计算出期望尺寸后,得出子 View 的实际尺寸和位置
- 子 View 在自己的 layout() 方法中,将父 View 传进来的自己的实际尺寸和位置保存(如果是 ViewGroup,还会在 onLayout() 里调用每个字 View 的 layout() 把它们的尺寸位置传给它们)
具体开发
继承已有的 View,简单改写它们的尺寸:SquareImageView
- 重写 onMeasure()
- 用 getMeasuredWidth() 和 getMeasuredSize() 获取到测量出的尺寸
- 计算出最终要的尺寸
- 用 setMeasuredDimension(width, height) 把结果保存
public class SquareImageView extends androidx.appcompat.widget.AppCompatImageView {
public SquareImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
int size = Math.max(measuredWidth, measuredHeight);
setMeasuredDimension(size, size); // 保存测得的尺寸
}
}
对自定义 View 完全进行自定义尺寸计算:重写 onMeasure():CircleView
- 重写 onMeasure()
- 计算出自己的尺寸
- 用 resolveSize() 或者 resolveSizeAndState() 修正结果
3.1resolveSize() / resolveSizeAndState() 内部实现
3.1.1首先用 MeasureSpec.getMode(measureSpec) 和MeasureSpec.getSize(measureSpec) 取出父 对自己的尺寸限制类型和具体限制尺寸;
3.1.2如果 measure spec 的 mode 是 EXACTLY,表示父 View 对子 View 的尺寸做出了精确限制,所以就放弃计算出的 size,直接选用 measure spec 的 size;
3.1.3如果 measure spec 的 mode 是 AT_MOST,表示父 View 对子 View 的尺寸只限制了上限,需要看情况:
(1)如果计算出的 size 不大于 spec 中限制的 size,表示尺寸没有超出限制,所以选用计算出的 size;
(2)而如果计算出的 size 大于 spec 中限制的 size,表示尺寸超限了,所以选用spec 的 size,并且在 resolveSizeAndState() 中会添加标志MEASURED_STATE_TOO_SMALL(这个标志可以辅助父 View 做测量和布局的计算;
(3)如果 measure spec 的 mode 是 UNSPECIFIED,表示父 View 对子 View 没有任何尺寸限制,所以直接选用计算出的 size,忽略 spec 中的 size。
3.1.4使用 setMeasuredDimension(width, height) 保存结果
public class CircleView extends View {
private static final int RADIUS = (int) Utils.dpToPixel(80);
private static final int PADDING = (int) Utils.dpToPixel(30);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = (PADDING + RADIUS) * 2;
int height = (PADDING + RADIUS) * 2;
width = resolveSizeAndState(width, widthMeasureSpec, 0);
height = resolveSizeAndState(height, widthMeasureSpec, 0);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, paint);
}
}
自定义 Layout:重写 onMeasure() 和 onLayout():TagLayout
1.重写onMeasure()
(1)遍历每个子 View,用 measureChildWidthMargins() 测量子 View
(2)需要重写 generateLayoutParams() 并返回 MarginLayoutParams 才能measureChildWithMargins() 方法
(3)有些子 View 可能需要重新测量(比如换行处)
(4)测量完成后,得出子 View 的实际位置和尺寸,并暂时保存
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed = 0;
int heightUsed = 0;
int lineWidthUsed = 0;
int lineMaxHeight = 0;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
if (specMode != MeasureSpec.UNSPECIFIED &&
lineWidthUsed + child.getMeasuredWidth() > specWidth) {
lineWidthUsed = 0;
heightUsed += lineMaxHeight;
lineMaxHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
Rect childBound;
if (childrenBounds.size() <= i) {
childBound = new Rect();
childrenBounds.add(childBound);
} else {
childBound = childrenBounds.get(i);
}
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth();
widthUsed = Math.max(widthUsed, lineWidthUsed);
lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight());
}
int width = widthUsed;
int height = heightUsed + lineMaxHeight;
setMeasuredDimension(width, height);
}
2.重写 onLayout()
- 如果开发者写了具体值(例如 layout_width="24dp"),就不用再考虑父View 的剩余空间了,直接用 LayoutParams.width / height 来作为子 View 的限制 size,而限制 mode 为 EXACTLY(为什么?课堂上说过,因为软件的直接开发者——即 xml 布局文件的编写者——的意见最重要,发生冲突的时候应该以开发者的意见为准。换个角度说,如果真的由于冲突导致界面 不正确,开发者可以通过修改 xml 文件来解决啊,所以开发者的意见是第一位,但你如果设计成冲突时开发者的意见不在第一位,就会导致软件的可 配置性严重降低);
- 如果开发者写的是 MATCH_PARENT,即要求填满父控件的可用空间,那么由于自己的可用空间和自己的两个 MeasureSpec 有关,所以需要根据自己的 widthMeasureSpec 或 heightMeasureSpec 中的 mode 来分情况判 断:
- 如果开发者写的是 WRAP_CONTENT,即要求子 View 在不超限制的前提下
- 如果自己的 spec 中的 mode 是 UNSPECIFIED,说明自己的尺寸没有上限,那么让子 View 填满自己的可用空间就无从说起,因此选用退让方案:给子 View 限制的 mode 就设置为 UNSPECIFIED,size 写 0 就好;
- 如果自己的 spec 中的 mode 是 EXACTLY 或者 AT_MOST,说明自己的尺寸有上限,那么把 spec 中的 size 减去自己的已用宽度或高度, 就是自己可以给子 View 的 size;至于 mode,就用 EXACTLY(注意:就算自己的 mode 是 AT_MOST,传给子 View 的也是EXACTLY,想不通的话好好琢磨一下);
自我测量,那么同样由于自己的可用空间和自己的两个 MeasureSpec 有关,所以也需要根据自己的 widthMeasureSpec 和 heightMeasureSpec 中的 mode 来分情况判断:
- 如果自己的 spec 中的 mode 是 EXACTLY 或者 AT_MOST,说明自己的尺寸有上限,那么把 spec 中的 size 减去自己的已用宽度或高度, 就是自己可以给子 View 的尺寸上限;至于 mode,就用AT_MOST(注意,就算自己的 mode 是 EXACTLY,传给子 View 的也是 AT_MOST,想不通的话好好琢磨一下;
- 如果自己的 spec 中的 mode 是 UNSPECIFIED,说明自己的尺寸没有上限,那么也就不必限制子 View 的上限,因此给子 View 限制的mode 就设置为 UNSPECIFIED,size 写 0 就好。
测量出所有子 View 的位置和尺寸后,计算出自己的尺寸,并用setMeasuredDimension(width, height) 保存
最后重写 onLayout()遍历每个子 View,调用它们的 layout() 方法来将位置和尺寸传给它们:完整代码
public class TagLayout extends ViewGroup {
List<Rect> childrenBounds = new ArrayList<>();
public TagLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed = 0;
int heightUsed = 0;
int lineWidthUsed = 0;
int lineMaxHeight = 0;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
if (specMode != MeasureSpec.UNSPECIFIED &&
lineWidthUsed + child.getMeasuredWidth() > specWidth) {
lineWidthUsed = 0;
heightUsed += lineMaxHeight;
lineMaxHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
Rect childBound;
if (childrenBounds.size() <= i) {
childBound = new Rect();
childrenBounds.add(childBound);
} else {
childBound = childrenBounds.get(i);
}
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth();
widthUsed = Math.max(widthUsed, lineWidthUsed);
lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight());
}
int width = widthUsed;
int height = heightUsed + lineMaxHeight;
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
Rect childBounds = childrenBounds.get(i);
child.layout(childBounds.left, childBounds.top, childBounds.right, childBounds.bottom);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
触摸反馈基础
自定义单 View 的触摸反馈
重写 onTouchEvent(),在方法内部定制触摸反馈算法
(1)是否消费事件取决于 ACTION_DOWN 事件是否返回 true
(2)MotionEvent
2.1 getActionMasked() 和 getAction()
2.2 POINTER_DOWN / POINTER_UP 和 getActionIndex()
View.onTouchEvent()
(1)当用户按下(ACTION_DOWN):
如果不在滑动控件中,切换至按下状态,并注册长按计时器 ; 如果在滑动控件中,切换至预按下状态,并注册按下计时器
(2)当进入按下状态并移动(ACTION_MOVE):
重绘 Ripple Effect; 如果移动出自己的范围,自我标记本次事件失效,忽略后续事件
(3)当用户抬起(ACTION_UP):
如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除一切状态 ;如果已经触发长按,切换至抬起状态并清除一切状态
当事件意外结束(ACTION_CANCEL): 切换至抬起状态,并清除一切状态
自定义 ViewGroup 的触摸反馈
(1)除 了 重 写 onTouchEvent() , 还 需 要 重 写 onInterceptTouchEvent() (2)onInterceptTouchEvent() 不用在第一时间返回 true,而是在任意事件,需要拦截的时候返回true 就行
触摸反馈的流程

View.dispatchTouchEvent()
1.如果设置了 OnTouchListener,调用 OnTouchListener.onTouch()
(1)如果 OnTouchListener 消费了事件,返回 true
(2)如果 OnTouchListener 没有消费事件,继续调用自己的 onTouchEvent(),并返回和onTouchEvent() 相同的结果
2.如果没有设置 OnTouchListener,同上
ViewGroup.dispatchTouchEvent()
1.如果是用户初次按下(ACTION_DOWN),清空 TouchTargets 和 DISALLOW_INTERCEPT 标记拦截处理
2.如果不拦截并且不是 CANCEL 事件,并且是 DOWN 或者 POINTER_DOWN,尝试把pointer(手指)通过 TouchTarget 分配给子 View;并且如果分配给了新的子 View,调用child.dispatchTouchEvent() 把事件传给子 View
3.看有没有 TouchTarget
(1)如果没有,调用自己的 super.dispatchTouchEvent()
(2)如果有,调用 child.dispatchTouchEvent() 把事件传给对应的子 View(如果有的话)
4.如果是 POINTER_UP,从 TouchTargets 中清除 POINTER 信息,如果是 UP 或 CANCEL,重置状态
TouchTarget
作用:记录每个子 View 是被哪些 pointer(手指)
按下的结构:单向链表
拦截处理
1.如果不是初次按下,并且没有 TouchTarget,直接拦截
2.如果是初次按下,或者有 TouchTarget
(1)如果设置了 disallow intercept,不拦截
(2)否则,调用 onInterceptTouchEvent(),如果返回 true 则拦截,返回 false 则不拦截

本文详细讲解了Android开发中如何使用属性动画(ViewPropertyAnimator和ObjectAnimator)创建炫酷视图动画,包括设置动画属性、插值器和自定义属性处理。同时介绍了硬件加速的概念,以及如何通过自定义布局和触摸反馈来优化性能。
&spm=1001.2101.3001.5002&articleId=120019415&d=1&t=3&u=45c054a7fa0f4191ac21437d436cc57a)
2942

被折叠的 条评论
为什么被折叠?



