这是我参与「第四届青训营 」笔记创作活动的的第1天
View
体系是较为复杂的,但是又非常重要的一个知识点。我们把这部分知识吃透吃熟是十分必要的,打卡第一天,我把View体系的第一部分知识整理出来,快来和我一起学习吧。
View树结构
官方给出我们使用的各种布局和各种 View
都是继承自 ViewGroup
、 View
或者他们的派生类。所以,了解View体系是极其重要的任务
如下图的 View部分继承关系
,我们可以看到常用的 View
组件、布局组件是如何继承的。
坐标系
学习 View
,首先需要知道 View
的位置在 Android
中是如何定义和测量的。
上图之中的蓝色和绿色是有着不同作用含义,我们平时使用也是在不同的地方调用
绿色:在
View
中获得View
到其父控件之间的距离蓝色:来自于点击事件
MotionEvent
内部的方法,可以在重写View
事件分发体系的的三大方法的时候,利用传入的事件调用上图的蓝色方法,获取点击的位置坐标
获取坐标绘制View的滑动
//自定义一个View,点击该View可以随意滑动其位置
//下面有5个方法可以实现,其中两个由于理解为移动的是屏幕框,会使得其他元素一起偏移
public class CoutomView extends View {
private int lastX;
private int lastY;
Scroller mScroller;
public CoutomView(Context context) {
super(context);
}
public CoutomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
// M1
layout(getLeft()+offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY);
// M2
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
// M3
// ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)getLayoutParams();
// layoutParams.leftMargin = getLeft()+offsetX;
// layoutParams.topMargin = getTop()+offsetY;
// setLayoutParams(layoutParams);
// M4 会使得其他元素一起偏移
// ((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
}
return true;
}
/**
* M5
* 提供给Activity调用滑动,也会使得其他元素一起偏移
*
* @param destX
* @param destY
*/
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
}
属性动画
区别
View动画:只展示普通动画效果,响应的点击时间的位置依旧在原来的地方。所以无法左交互效果
属性动画:利用属性和对象来控制 View ,同时使得动画执行后可交互
属性动画的执行,可以带上属性以及属性参数
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
//使用传入
ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f).setDuration(1000).start()
ObjectAnimator
常用属性
translationX
和translantionY
沿轴平移rotatian
、rotatianX
和rotatianY
沿着某支点进行旋转PrivotX
和PrivotY
可以控制支点位置,围绕支点旋转和缩放,支点默认为中心位置、alpha
透明度,默认为1,1不透明 0 全透明x
,y
View的终点位置
使用
ObjectAnimator
时,要调用某个属性,该属性需要有对应的get()
和set()
方法。若是没有,我们就需要自定义一个属性类或者包装类添加该方法
//MainActivity
val myView = MyView(binding.button)
ObjectAnimator.ofInt(myView,"width",500).setDuration(500).start()
//MyView,给MyView里面的 width 添加一个 set() 和 get() 功能
class MyView(private val mTarget: View) {
var width: Int
get() = mTarget.layoutParams.width
set(width) {
mTarget.layoutParams.width = width
mTarget.requestLayout()
}
}
ValueAnimator
这个方法不提供动画效果,类似数值发生器,你需要根据里面的 AnimatorUpdateListener
来监听数值,设置动画变化
//传入的值被 a.animatedValue 获取到,根据该值设置做动画
val animator = ValueAnimator.ofFloat(0f,100f).apply {
setTarget(binding.button2)
duration = 1000
start()
addUpdateListener { a ->
val mFloat = a.animatedValue as Float
binding.button2.rotation = mFloat
binding.button2.translationX = 100f
}
}
//复杂些的动画
binding.button8.setOnClickListener {
val anim = ValueAnimator.ofFloat(0f, 360f)
anim.addUpdateListener { animation ->
val angle = animation.animatedValue as Float
binding.layer.rotation = angle
binding.layer.scaleX = 1 + (180 - Math.abs(angle - 180)) / 20f
binding.layer.scaleY = 1 + (180 - Math.abs(angle - 180)) / 20f
var shift_x = 500 * Math.sin(Math.toRadians((angle * 5).toDouble())).toFloat()
var shift_y = 500 * Math.sin(Math.toRadians((angle * 7).toDouble())).toFloat()
binding.layer.translationX = shift_x
binding.layer.translationY = shift_y
}
anim.duration = 4000
anim.start()
}
动画的监听
//完整的监听,四个过程
ObjectAnimator.ofFloat(binding.layer,"alpha",1.5f).addListener(object : Animator.AnimatorListener{
override fun onAnimationStart(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationEnd(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationCancel(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationRepeat(p0: Animator?) {
TODO("Not yet implemented")
}
})
//不完整的监听,匿名类中,重写其中的一个方法
ObjectAnimator.ofFloat(binding.layer,"alpha",0f,1f,0f,1f,0f,1f).apply {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
}
})
duration = 10000
start()
}
AnimatorSet 组合动画
我们可以调用 AnimatorSet
里面的 play()
方法,以及该方法内部的类,就可以完成多个动画的组合展示功能
//eg,下面的执行顺序是 a3 -> a2 -> a1
val a1 = ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f,0f)
val a2 = ObjectAnimator.ofFloat(binding.coutomView,"scaleX",1.0f,2.0f)
val a3 = ObjectAnimator.ofFloat(binding.coutomView,"rotationX",0.0f,90f,0.0f)
val set = AnimatorSet().apply {
duration = 1000
play(a1).with(a2).after(a3)
start()
}
//简单展示下对应方法的结构
//play()
public AnimatorSet.Builder play(Animator anim){
if(anim != null) return new Builder(anim);
return null;
}
//Builder 结构,对应源码可自行查看
public class Builder {
Builder() {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder with(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder before(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder after(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder after(long delay) {
throw new RuntimeException("Stub!");
}
}
after(Animator anim)
当下Builder
的动画放到传入的动画之后执行
after(long delay)
当下Builder
的动画延迟指定的毫秒执行
before(Animator anim)
当下Builder
的动画放到传入的动画之前执行
with(Animator anim)
当下Builder
的动画与传入的动画并行执行
根据这个属性,浅析一下这段代码的逻辑。play(a1).with(a2).after(a3)
,
- 首先传入
play()
的是a1
,会返回一个含有a1
的Builder
对象,我们简称这个对象为b1
- 再次调用
with()
传入a2
,其实就是传入b1
的with()
中。那当前动画就是a1
,传入的是a2
,两个并行执行。最后会返回this
即为b1
- 最后再调用
after()
传入a3
,也还是b1
内部的after()
中。即当前动画还是a1
,传入的是a3
,a1
在a3
后面执行。最后会返回this
即为b1
所以最终的顺序是 :
a3
->a1
/a2
由于这几个方法都是同一个对象内的,所以当前动画
currentNode
是不变的,一直都是a1
。那么其他需要组合的动画,都还会是以a1
为主题,看是插入到他的前或者后。如果有两个动画是放置与同一个位置,即
play(a1).after(a2).after(a3)
。那么a2
和a3
是并行执行的,即顺序为a2
/a3
->a1
PropertyValuesHolder 组合动画
该动画无法实现前后关系,都是并行执行的。用法如下
val valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX",1.0f,1.5f)
val valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX",0.0f,90.0f,0.0f)
val valuesHolder3 = PropertyValuesHolder.ofFloat("alpha",1.0f,0.3f,1.0f)
ObjectAnimator.ofPropertyValuesHolder(binding.coutomView,valuesHolder1,valuesHolder2,valuesHolder3).apply {
duration = 2000
start()
}
xml 使用属性动画
//aimator.scale.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="2.0"
android:valueType="floatType">
</objectAnimator>
AnimatorInflater.loadAnimator(this,R.animator.scale).apply {
setTarget(binding.coutomView)
start()
}
Scroller
graph LR
A[startScroll] --> B[invalidate] --> C[draw] --> D[computeScroll]
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
Scroller
并不能直接实现滑动,他最大的功能是在 startScroll()
处保存传入的滑动信息。后面再不断调用到 computeScroll()
这个方法,使用其中的 scrollTo()
来实现滑动。
computeScroll()
中,在判断方法中会调用到 mScroller.computeScrollOffset()
,这是用于获取 scrollX
和 scrollY
两个位置参数以及做出判断是否滑动结束。若是未滑动结束,就会让 computeScroll()
不断滑动重绘。
View事件分发
Activity构成图
Activity 的层级基本如上所示,在 xml
文件中构建的布局就是在 contentParent
位置,也就是 contentView
位置。
分发机制
首先需要了解的是 MotionEvent
,当屏幕被点击 ->产生点击事件 ->MotionEvent
产生。
点击事件产生后层层下发,不断传递到根 ViewGroup
。
graph LR
A[MotionEvent] --> B[Activity]
B --> C[PhoneWindow]
C --> D[DecorView]
D --> E[ViewGroup]
事件分发的三大方法
dispatchTouchEvent(MotionEvent event)
: 用以事件分发。下面简称dTE()方法onInterceptTouchEvent(MotionEvent e)
: 用以拦截事件,在dispatchTouchEvent(MotionEvent event)
中被调用来拦截。该方法只有ViewGroup
中有,View
中没有onTouchEvent(MotionEvent e)
: 用以处理点击事件,在dispatchTouchEvent(MotionEvent event)
中被调用。这个方法是View
中的,但是由于ViewGroup
是继承自View
的,所以ViewGroup
可以使用。下面简称oTE()方法
下面简述一下 dispatchTouchEvent(MotionEvent event)
方法,
//ViewGroup内
public boolean dispatchTouchEvent(MotionEvent ev){
//拦截部分
...
onInterceptTouchEvent(ev);
...
//点击处理事件
...
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign))
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child,int desiredPointerIdBits){
...
if(child == null){
handled = super.dispatchTouchEvent(event);
}else{
handled = child..dispatchTouchEvent(event);
}
...
}
//View内
public boolean dispatchTouchEvent(MotionEvent ev){
if(... && li.mOnTouchListener != null && li.mOnTouchListener.onTouch(this,event)){
result = true;
}
if(!result && onTouchEvent(event)){
result = true;
}
}
拦截的处理逻辑
这其中的
允许拦截?
,一般通过子View的requestDisallowInterceptTouchEvent
来设置,这也是处理滑动冲突的方法之一。当事件在
ViewGroup
被拦截之后,后续的事件序列都交给其处理了
点击事件处理逻辑
事件被拦截后会被当前的
ViewGroup
处理,上图就是详细的点击事件处理流程图。
事件分发传递规则
View
的事件分发是,首先 View
层层分发下来,若是 onInterceptTouchEvent(ev)
为 true
就拦截,为 false
就继续下发。
当某一层级拦截后,就调用 onTouchEvent(event)
来处理,若是该层无法处理,就传递给父层的 onTouchEvent(event)
来处理。如此层层传递直到有对应可以处理的父层。
整个事件的分发过程看起来复杂,当最终归于三大方法可以用下面的伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
return result;
}
总结
以上就是 View
体系的基础内容,理解 View
的事件分发原理,是我们能化用 View 的前提。View
处理事件层层下发的思想,是非常具有借鉴学习价值的,我们代码的设计也可以借鉴这套思想,提高代码的质量。
参考文章
Android中View的继承关系图_Huangrong_000的博客-CSDN博客
布局 | Android 开发者 | Android Developers (google.cn)
android之View坐标系(view获取自身坐标的方法和点击事件中坐标的获取)_炸斯特的博客-CSDN博客