View体系(上)|青训营笔记

298 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的的第1天

View 体系是较为复杂的,但是又非常重要的一个知识点。我们把这部分知识吃透吃熟是十分必要的,打卡第一天,我把View体系的第一部分知识整理出来,快来和我一起学习吧。

View树结构

官方给出我们使用的各种布局和各种 View 都是继承自 ViewGroupView 或者他们的派生类。所以,了解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

常用属性

  • translationXtranslantionY 沿轴平移
  • rotatianrotatianXrotatianY 沿着某支点进行旋转
  • PrivotXPrivotY 可以控制支点位置,围绕支点旋转和缩放,支点默认为中心位置、
  • alpha 透明度,默认为1,1不透明 0 全透明
  • xy 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 ,会返回一个含有 a1Builder对象,我们简称这个对象为 b1
  • 再次调用 with() 传入 a2 ,其实就是传入 b1with() 中。那当前动画就是 a1 ,传入的是 a2 ,两个并行执行。最后会返回 this 即为 b1
  • 最后再调用 after() 传入 a3,也还是 b1 内部的 after() 中。即当前动画还是 a1 ,传入的是 a3a1a3 后面执行。最后会返回 this 即为 b1

所以最终的顺序是 :a3 -> a1/a2

由于这几个方法都是同一个对象内的,所以当前动画 currentNode 是不变的,一直都是 a1 。那么其他需要组合的动画,都还会是以 a1 为主题,看是插入到他的前或者后。

如果有两个动画是放置与同一个位置,即 play(a1).after(a2).after(a3) 。那么 a2a3 是并行执行的,即顺序为 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() ,这是用于获取 scrollXscrollY 两个位置参数以及做出判断是否滑动结束。若是未滑动结束,就会让 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;
}

总结

img

以上就是 View 体系的基础内容,理解 View 的事件分发原理,是我们能化用 View 的前提。View 处理事件层层下发的思想,是非常具有借鉴学习价值的,我们代码的设计也可以借鉴这套思想,提高代码的质量。

参考文章

Android中View的继承关系图_Huangrong_000的博客-CSDN博客

布局 | Android 开发者 | Android Developers (google.cn)

android之View坐标系(view获取自身坐标的方法和点击事件中坐标的获取)_炸斯特的博客-CSDN博客

Activity 的组成 - 简书 (jianshu.com)

《Android进阶之光(第2版) (博文视点出品)》(刘望舒)【摘要 书评 试读】- 京东图书 (jd.com)