(三)View的事件体系

2019-08-27  本文已影响0人  YongtaoHuang

View的渲染机制是Android区别于其他操作系统最闪亮的点,熟悉Android的View体系是我们开发自定义View的基础。

3.1 View基础知识

3.1.1 什么是View

View是Android中所有控件的基类。
另,ViewGroup即一组控件,一组View,且ViewGroup继承了View。

public abstract class ViewGroup extends View implements{}

3.1.2 View的位置参数

left: 左上角横坐标
right: 右下角横坐标
top: 左上角纵坐标
bottom: 右下角纵坐标
translationX:左上角相对于父容器的X偏移量
translationY: 左上角相对于父容器的Y偏移量
所以:
width = right - left
height = bottom - top

View的位置坐标.png
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    protected int mLeft; //
    protected int mRight; //
    protected int mTop; //
    protected int mBottom; // 
    
    public final int getLeft() {
        return mLeft;
    }
    
    public final void setLeft(int left) {
        if (left != mLeft) {
            final boolean matrixIsIdentity = hasIdentityMatrix();
            if (matrixIsIdentity) {
                if (mAttachInfo != null) {
                    int minLeft;
                    int xLoc;
                    if (left < mLeft) {
                        minLeft = left;
                        xLoc = left - mLeft;
                    } else {
                        minLeft = mLeft;
                        xLoc = 0;
                    }
                    invalidate(xLoc, 0, mRight - minLeft, mBottom - mTop);
                }
            } else {
                // Double-invalidation is necessary to capture view's old and new areas
                invalidate(true);
            }

            int oldWidth = mRight - mLeft;
            int height = mBottom - mTop;

            mLeft = left;
            mRenderNode.setLeft(left);

            sizeChange(mRight - mLeft, height, oldWidth, height);

            if (!matrixIsIdentity) {
                mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
                invalidate(true);
            }
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            invalidateParentIfNeeded();
            if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) {
                // View was rejected last time it was drawn by its parent; this may have changed
                invalidateParentIfNeeded();
            }
        }
    }
    
    public final int getRight() {
        return mRight;
    }
    
    public final void setRight(int right) {

    }

    public final int getTop() {
        return mTop;
    }

    public final void setTop(int top) {
 
    }   

    public final int getBottom() {
        return mBottom;
    }

    public final void setBottom(int bottom) {
 
    }
    
    public float getX() {
        return mLeft + getTranslationX();
    }
    
    public void setX(float x) {
        setTranslationX(x - mLeft);
    }
    
    public float getY() {
        return mTop + getTranslationY();
    }
    
    public void setY(float y) {
        setTranslationY(y - mTop);
    }
    
    public float getZ() {
        return getElevation() + getTranslationZ();
    }
    
    public void setZ(float z) {
        setTranslationZ(z - getElevation());
    }
    
    public float getTranslationX() {
        return mRenderNode.getTranslationX();
    }
    
    public void setTranslationX(float translationX) {
        if (translationX != getTranslationX()) {
            invalidateViewProperty(true, false);
            mRenderNode.setTranslationX(translationX);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }
    
    public float getTranslationY() {
        return mRenderNode.getTranslationY();
    }
    
    public void setTranslationY(float translationY) {
        
    }
    
    public float getTranslationZ() {
        return mRenderNode.getTranslationZ();
    }

    public void setTranslationZ(float translationZ) {

    }

}

3.1.3 MotionEvent和TouchSlop

MotionEvent的主要的三个事件:
※ ACTION_DOWN:手指接触屏幕
※ ACTION_MOVE:手指移动
※ ACTION_UP:手指松开
发生MotionEvent时可以得到x和y坐标,getX/getY相对于当前View左上角的x和y坐标和getRawX/getRawY相对于手机屏幕左上角的x和y坐标。

TouchSlop是系统所能识别出的被认为是滑动的最小距离:

Log.d(TAG, "onCreate: "+ViewConfiguration.get(getContext()).getScaledTouchSlop());
// 输出:onCreate: 16

3.1.4 VelocityTracker、GestureDetector和Scroller

VelocityTracker
滑动速度跟踪器

public class MainActivity extends AppCompatActivity{

    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    VelocityTracker velocityTracker;
    int xVelocity = 0;
    int yVelocity = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //初始化
                velocityTracker = VelocityTracker.obtain();
                break;
            case MotionEvent.ACTION_MOVE:
                //追踪
                velocityTracker.addMovement(event);
                velocityTracker.computeCurrentVelocity(1000);
                xVelocity = (int) velocityTracker.getXVelocity();
                yVelocity = (int) velocityTracker.getYVelocity();
                Log.d(TAG, "onTouchEvent: "+xVelocity);
                Log.d(TAG, "onTouchEvent: "+yVelocity);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //回收
                velocityTracker.clear();
                velocityTracker.recycle();
                break;
        }
        return true;
    }

}

GestureDetector
GestureDetector是手势检测器对象,用于辅助检测用户的单击、滑动、长按、双击等行为,配合GestureDetector使用必须实现OnGestureListener、OnDoubleTapListener接口。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private GestureDetector gestureDetector; // 声明一个手势检测器

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        gestureDetector = new GestureDetector(this,new MyGestureListener());
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        gestureDetector.onTouchEvent(ev); // 命令由手势检测器接管当前的手势事件
        return true;
    }

    // 定义一个手势检测监听器
    final class MyGestureListener implements GestureDetector.OnGestureListener{

        @Override
        public boolean onDown(MotionEvent e) {
            Log.d(TAG, "onDown: "+"按下");
            return false;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            Log.d(TAG, "onShowPress: "+"按下但还未滑动或松开");
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.d(TAG, "onSingleTapUp: "+"轻点");
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.d(TAG, "onScroll: "+"手势滑动");
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.d(TAG, "onLongPress: "+"长按");
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d(TAG, "onFling: "+"飞过掠过");
            return false;
        }
    }
}

Scroller
弹性滑动对象,用于实现view的弹性滑动。当直接使用View的scrollTo/scrollBy方法来进行滑动时,过程时瞬间完成的。所以需要Scroller对象和View本身的computeScroll方法配合共同完成。详见3.2和3.3。

3.2 View的滑动

滑动时Android所有应用的基础和标配:
实现View滑动三种办法:
1、通过View本身提供的scrollTo/scrollBy方法
2、通过动画给View施加平移效果
3、通过改变View的LayoutParams使得View重新布局

3.2.1 使用scrollTo/scrollBy

    protected int mScrollX;  // View左边缘与View内容左边缘的距离
    protected int mScrollY;  // View上边缘与View内容上边缘的距离
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

3.2.2 使用动画

可以使用View动画和属性动画来实现滑动的功能。详见(七)Android动画深入分析

3.2.3 改变布局参数

改变布局参数,也就是改变LayoutParams,也可以实现滑动。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <Button
        android:id="@+id/btn_test"
        android:text="button"
        android:background="@color/green"
        android:layout_width="150dp"
        android:layout_height="100dp" />

</LinearLayout>
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button btnTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnTest = findViewById(R.id.btn_test);
        btnTest.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.btn_test:
                ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) btnTest.getLayoutParams();
                params.width += 100;
                params.leftMargin += 100;
                btnTest.requestLayout();
                break;
            default:
                break;
        }
    }
}

3.2.4 各种滑动方式的对比

1、scrollTo/scrollBy:操作简单,适合对view内容滑动。非平滑
2、动画:操作简单,主要适用于没有交互的view和实现复杂的动画效果
3、改变LayoutParams:操作稍微复杂,适用于有交互的view。非平滑

3.3 弹性滑动

弹性滑动,也就是渐进式滑动。
核心思想:将一次大的滑动分成若干次小滑动并在一个时间段内完成。

3.3.1 使用Scroller

3.3.2 通过动画

可以使用动画来实现弹性滑动的功能。详见(七)Android动画深入分析

3.3.3 使用延时策略

延时策略核心思想时通过发送一系列延时消息从而达到一种渐近式的效果。
(1)Handler+View的postDelayed方法
(2)Thread的sleep方法
对弹性滑动完成总时间有精确要求的使用场景下,使用延时策略是一个不太合适的选择。

3.4 View的事件分发机制

View的两大难点:
1、事件分发机制
2、滑动冲突
点击事件的对象:MotionEvent
点击事件的分发过程:
主要涉及3个方法:
1、dispatchTouchEvent:用于事件分发
2、onInterceptTouchEvent:用于判断是否内部拦截
3、onTouchEvent:用于处理点击事件
伪代码:

public boolean dispatchTouchEvent(MotionEvent me){
    boolean consume = false;
    if (onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

点击事件的传递顺序:Activity -> Window -> ViewGroup -> View
注意事项:
1、事件分发是逐级下发的,目的是将事件传递给一个View。
2、ViewGroup一旦拦截事件,就不往下分发,同时调用onTouchEvent处理事件。

3.5 View的滑动冲突

定义:一般情况下,在一个界面里存在内外两层可同时滑动的情况时,会出现滑动冲突现象。

3.5.1 常见的滑动冲突场景

场景1、内外部滑动方向不一致:如ViewPager内嵌套ListView(实际这么用没问题,因为ViewPager内部已处理过)。
场景2、内外部滑动方向一致:如ScrollView内嵌套ListView(实际上也已被解决)。
场景3、上面两种情况的嵌套

3.5.2 滑动冲突的处理规则

面对上述3个场景,分别有3个处理规则
场景1处理规则、根据滑动的距离或者滑动的角度去判断
场景2处理规则、根据需求在业务上找到突破点
场景3处理规则、根据需求在业务上找到突破点

3.5.3 滑动冲突的解决方式

1、外部拦截法

2、内部拦截法

上一篇 下一篇

猜你喜欢

热点阅读