Android自定义View,滑动,事件传递小结

本文只总结知识点 欢迎补充,欢迎纠正。谢谢!

#预备知识


Android控件框架

####1. View树状图

  • Android的View树结构总是以一个ViewGroup开始,包含多个View或ViewGroup
  • View是所有控件的父类
  • ViewGroup是继承自View的容器类抽象类

####2. AndroidUI界面架构图

  • 每个Activity都包含一个Window对象,通常为PhoneWindow
  • PhoneWindow将一个DecorView作为整个窗口的根View,DecorView作为窗口顶层视图封装了一些窗口操作的方法
  • DecorView将内容显示在PhoneWindow上,并通过WindowManagerService来进行接收,并通过Activity对象来回调对应的onClickListener。显示时,将屏幕分成两个部分,TitleView和ContentView。Content是一个id为content的FrameLayout,activity_main.xml就在其中。

坐标体系

View的坐标由它的四个顶点决定,分别对应View的四个属性

获得四个顶点的方式

  • left,getLeft() 左上角的横坐标
  • top,getTop()左上角的纵坐标
  • right,getRight() 右下角的横坐标
  • bottom,getBottom() 右下角的纵坐标

View测量

View测量主要依赖MeasureSpec 测量模式有三种

  • EXACTLY 精确模式
  1. 明确指定数值: layout_width=200dp,layout_height=200dp
  1. layout_width=match_parent,layout_height=match_parent
  • AT_MOST 最大模式
  1. layout_width=warp_content,layout_height=warp_content
  1. 空间大小会随着内容变大而变大,最大为父布局剩余空间
  • UNSPECIFIED

父容器不对View限制大小,要多大给多大,这种情况一般用于系统内部,表示一种测量状态,不用过多关注

#一、自定义View ##分类

  • 继承View,重写onDraw方法
  • 继承已有View(比如TextView
  • 继承ViewGroup实现特殊的Layout
  • 继承已有的ViewGroup(比如LinearLayout

##一般步骤 ###1. 在res/values/ 下建立一个attrs.xml文件,声明我们的自定义属性

"1.0" encoding="utf-8"?>  
  
    "titleText" format="string" />  
    "titleTextColor" format="color" />  
    "titleTextSize" format="dimension" />  
    <declare-styleable name="CustomTitleView">  
        "titleText" />  
        "titleTextColor" />  
        "titleTextSize" />  
    declare-styleable>  
复制代码

###2. 继承View(或其他)重写构造方法

    public CustomView(Context context)  
    {  
        this(context, null);  
    }  
  
    /** 
     * 获得我自定义的样式属性 
     *  
     * @param context 
     * @param attrs 
     * @param defStyle 
     */  
    public CustomView(Context context, AttributeSet attrs)  
    {  
        super(context, attrs, defStyle);  
        /** 
         * 获得我们所定义的自定义样式属性 
         */  
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);  
        int n = a.getIndexCount();  
        for (int i = 0; i < n; i++)  
        {  
            int attr = a.getIndex(i);  
            switch (attr)  
            {  
            case R.styleable.CustomView_titleText:  
                mTitleText = a.getString(attr);  
                break;  
            case R.styleable.CustomView_titleTextColor:  
                // 默认颜色设置为黑色  
                mTitleTextColor = a.getColor(attr, Color.BLACK);  
                break;  
            case R.styleable.CustomView_titleTextSize:  
                // 默认设置为16sp,TypeValue也可以把sp转化为px  
                mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(  
                        TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
                break;  
  
            }  
  
        }  
        a.recycle();  
    } 
复制代码

几个点

  • 单参数构造是直接new的时候会调用
  • 从xml中申明,并通过findViewById实例化会调用第两个参数的构造方法
  • 通过TypedArray解析自定义属性,完成时候记得回收
  • 解析自定义属性时get的类型与定义时的format对应

###3. 测量onMeasure 确定View大小 以自定义View实现文字绘制为例:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int wSpecMode=MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize=MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode=MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize=MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        int textWidth=(int)mPaint.measureText(mText)+getPaddingLeft()+getPaddingRight();
        int textHeight=(int)(-mPaint.ascent() + mPaint.descent())+getPaddingTop()+getPaddingBottom();

        if(wSpecMode==AT_MOST&&hSpecMode==AT_MOST){
            width=textWidth;
            height=textHeight;
        }else if(wSpecMode==AT_MOST){
            width=textWidth;
            height=hSpecSize;
        }else if(hSpecMode==AT_MOST){
            width=wSpecSize;
            height=textHeight;
        }

        width=Math.min(width,wSpecSize);
        height=Math.min(height,hSpecSize);

        setMeasuredDimension(width,height);

    }
复制代码

几个点

  • 当直接继承ViewViewGroup重写onMeasure时,注意Viewwidthheightwarp_content时,需要特殊处理,否则默认为父布局剩余空间。

    Why? ** View自身的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams(也就是xml中设置的layout_width=warp_content,或代码中获取ViewLayoutParams设置宽高)共同决定。**

    • View设置宽高为具体数值时,无论父容器的MeasureSpec是什么,ViewMeasureSpec都是EXACTLY,宽高为LayoutParams中的大小。
    • View设置宽高为match_parent时,①.父布局的MeasureSpecEXACTLY时,ViewMeasureSpec也为EXACTLY,大小为父容器剩余空间;②.父布局的MeasureSpecAT_MOST时,ViewMeasureSpec也为AT_MOST,大小不会超过父容器剩余空间;
    • View设置宽高为warp_content时,无论父容器的MeasureSpec是什么,ViewMeasureSpec都是AT_MOST,并且大小不能超过父容器剩余空间 注:依据Android开发艺术探索
  • 如需支持Padding需要在测量时计算
  • 继承ViewGroup,如需支持Margin需要在测量时计算
  • View的生命周期与Activity不是同步,所以在ActivityonResume及之前的生命周期方法中获取View的宽高是不靠谱的

获取方法

  1. 重写onWindowFocusChanged在这个方法中获取
  2. view.post(runnable)
  3. ViewTreeObserver 4.view.measure(int widthMeasureSpec,int heightMeasureSpec) 不建议,因为这个方法要区分LayoutParams,在这不具体阐述
  • 计算类TextView的自定义布局的高度时,需知FontMetrics这个类:

FontMetrics有五个float类型值:

  • leading 留给文字音标符号的距离

  • ascentbaseline线到最高的字母顶点到距离,负值

  • topbaseline线到字母最高点的距离加上ascent|top|=|ascent|+|leading|

  • descentbaseline线到字母最低点到距离

  • bottomtop类似,系统为一些极少数符号留下的空间。topbottom总会比ascentdescent大一点的就是这些少到忽略的特殊符号

###4. 布局onLayout 确定View位置

 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }
复制代码

几个点

  • 容器类自定义View布局时需处理Margin

###5. 绘制onDraw 想要绘制一个view,需要什么?

  • 保存像素的Bitmap
  • 管理绘制请求的Canvas
  • 绘画的原始基本元素,例如矩形,线,文字,Bitmap
  • 拥有颜色和风格信息的画笔

综合来说就是:画笔Paint,画布Canvas,画什么:text,bitmap,path...

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //文字的x轴坐标
    float stringWidth = mPaint.measureText(text);
    float x = (getWidth() - stringWidth) / 2;
    //文字的y轴坐标
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    float y = getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
    canvas.drawText(text, x, y, mPaint);
}
复制代码

几个点

  • 绘制区域关注Rect,RectF
  • 文字是从baseline开始绘制
  • 考虑padding
  • 尽量不要在onDraw中构造对象
  • 绘制时需要用到的一些类
  • 绘制文字:FontMetrics(文字度量)
  • 绘制图像:ColorMatrix(图像色彩),PorterDuffXfermore(两个图像间的混合显示模式), Shader (着色器), Matrix(图形处理)
  • 绘制路径:Path(路径),PathEffect(路径效果), Bezier (贝塞尔曲线), PathMeasure (辅助计算Path的计算器)
  • 继承ViewGroup处理滑动、拖动辅助类:ViewDragHelper(可以实现各种不同的的滑动、拖动)

###注意几点

  • 尽量不要在View中使用Handler
  • View中如果有线程或者动画,需要及时停止,否则有可能造成内存泄漏,在onDetachedFromWindow中处理
  • 处理好焦点传递
  • 处理滑动及滑动冲突

#二、View滑动 ##1. 触摸、滑动相关

  • MotionEvent 触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
复制代码

用于报告(鼠标、笔、手指,轨迹球)运动事件 ACTION_DOWN(按下),ACTION_UP(抬起),ACTION_MOVE(移动),ACTION_CANCEL(取消)

  • TouchSlop 最小距离
ViewContfiguration.get(getConetxt()).getScaledTouchSlop()
复制代码

TouchSlop是系统识别最小的滑动距离,是一个常量值。当手指在屏幕滑动距离小于这个值时,系统不会将动作视为滑动。这个常量值的具体大小和设备也有关,不同的屏幕分辨率,可能会不一样 利用这个临界值,可以将一些不想要的手指操作给过滤掉

  • VelocityTracker速度追踪
public class ScrollerActivity extends AppCompatActivity {
    private VelocityTracker velocityTracker;
    private final String TAG = "ScrollerActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroller);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取VelocityTracker
        velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        //计算滑动速度
        velocityTracker.computeCurrentVelocity(1000);//计算速度
        float xVelocity = velocityTracker.getXVelocity();
        float yVelocity = velocityTracker.getYVelocity();
        Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != velocityTracker){
            velocityTracker.clear();//重置
            velocityTracker.recycle();//回收内存
        }
    }
}
复制代码

用于追踪手指在滑动过程中的速度,包括水平速度和竖直方向的速度 滑动速度值的正负取决于是否与坐标系方向一致 滑动速度是相对一定时间的

  • GestureDetector手势监控
public class ScrollerActivity extends AppCompatActivity {
    private Toast toast;
    private GestureDetector mGestureDetector;

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

    /**
     * 初始化 GestureDetector
     */
    private void initGestureDetector() {
        mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
        //解决屏幕长按后无法拖动
        mGestureDetector.setIsLongpressEnabled(false);
    }

    private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发
            showToast("轻触一下");
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发
            showToast("轻触未松开");
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为
            showToast("单击");
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动
            // 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为
            showToast("拖动");
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {//长按
            showToast("长按");
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            //按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
            showToast("快速滑动");
            return false;
        }
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean consume = mGestureDetector.onTouchEvent(event);
        return consume;
    }

    /**
     * Toast
     */
    private void showToast(String str) {
        if (null == toast) {
            toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
        } else {
            toast.setText(str);
        }
        toast.show();
    }
}
复制代码

用于辅助检测单击、滑动、长按、双击 GestureDetector.setOnDoubleTapListener(onDoubleTapListener)可以实现双击 在OnGestureListener内onDown(),onSingleTapUp(),onScroll(),onFling()方法都有一个boolean类型的返回值,这个值表示是否消费事件

  • Scroller弹性滑动对象
public class ScrollerView extends LinearLayout {
    private Scroller mScroller;

    public ScrollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initScroller();
    }

    /**
     * 初始化Scroller
     */
    private void initScroller() {
        mScroller = new Scroller(getContext());
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {//判断Scroller是否执行完毕
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    public void smoothScrollTo(int destX, int destY) {
        //计算相对于左上角的偏移量
        final int deltaX = getScrollX() - destX;
        final int deltaY = getScrollY() - destY;
        //在1000ms内滑向destX destY
        mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
        invalidate();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                smoothScrollTo((int) event.getX(), (int) event.getY());
                break;
            case MotionEvent.ACTION_UP://恢复左上角
                mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
                invalidate();
                break;
        }
        return true;
    }
}
复制代码

用于实现View的弹性滑动。Scroller本身无法实现弹性滑动,需要配合ViewcomputeScroll()方法

  • ViewDragHelper ``ViewGroup中拖动、滑动view的辅助类
public class DragView extends LinearLayout {
    private ViewDragHelper mViewDragHelper;

    public DragView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initDragHelper();
    }

    private void initDragHelper() {
        mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
    }

    /**
     *  ViewDragHelper回调接口
     */
    private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {//可以用来指定哪一个childView可以拖动
            return true;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
            return left;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
            return top;
        }
    };

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {//拦截事件
        return mViewDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {//消费事件
        //将触摸事件传递给`ViewDragHelper`,必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
}
复制代码

##2. 滑动冲突 ###常见的滑动冲突场景

  • 外部滑动方向与内部滑动方向不一致
  • 外部滑动方向与内部滑动方向一致
  • 上面两种情况嵌套

###解决办法

  • 内部拦截
  • 外部拦截

#####1. 内部拦截

内部拦截法指的是父容器不拦截任何事件,所有的事件都传递给childView,根据需要,childView来选择是否消费,需要配合requestDisallowInterceptTouchEvent()方法。重写childViewdispatchTouchEvent()方法 在ACTION_DOWN中,使用parent.requestDisallowInterceptTouchEvent(true),让父容器不拦截ACTION_DOWN事件,ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标记位控制

伪代码

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.requestDisallowInterceptTopuchEvent(false);
               }
          break;
          case MotionEvent.ACTION_UP:
               break;
          break;
       }
       mLastX = x ;
       mLastY = y ;
       return super.dispatchTouchEvent(event);
}
复制代码

#####2. 外部拦截

点击事件都会先经过父容器的拦截处理,如果父容器需要处理此事就拦截,否则就不进行拦截。重写父容器的onInterceptTouchEvent()方法

  • 首先,在ACTION_DOWN中,父容器必须返回false,不拦截ACTION_DOWN事件。因为一旦拦截了ACTION_DOWN后续的ACTION_MOVEACTION_UP都会又父容器来处理,这样事件就无法传递给childView
  • 其次,在ACTION_MOVE中,可以根据需要来进行拦截,需要就返回true,否则就false
  • 最后,在ACTION_UP中,返回false(如果父容器在ACTION_UP中,返回了truechildView就不会再收到ACTION_UP事件,childViewonClick事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP,即使在ACTION_UP中返回falseACTION_UP还是由父容器处理)

伪代码

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.Move:
                  if(父容器需要当前点击事件){
                     intercepted = true;
                  }else{
                     intercepted = false; 
                  }
             break;
             case MotionEvent.ACTION_UP:
                  intercepted = false;
             break;
      }
      mLastXIntercept = x;
      mLastYIntercept = y;
      return intercepted;
}

复制代码

#三、事件分发 ##1. 主要方法 先来看一张图

事件分发

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
复制代码

返回结果表示是否拦截当前事件。返回true,拦截;false,不拦截 事件分发的第一步,当事件传递到当前View一定会调用。返回结果受此ViewonTouchEvent()方法和下级childViewdispachTouchEvent影响。虽然是事件分发第一步,但绝多数情况不推荐直接修改这个方法

事件拦截

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
复制代码

返回结果用来判断是否拦截某个事件。这个方法只存在于ViewGroup中 如果当前view拦截了某个事件,在同一个事件的序列中,此方法便不会被再次调用

事件消费

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
复制代码

返回结果表示是否消费了事件。true,消费了,不用在审核了;false,不消费,给父容器处理

##2. 主要流程 首先来看一张图

  • 如果事件不被中断的话,整个流程呈U型
  • 传递顺序 Activity -> Window -> ViewGroup -> View
  • 消费顺序 Activity <- Window <- ViewGroup <- View
  • View设置的onTouchListener()优先级高于onTouchEvent()onClickListener()优先级比onToucnEvent()

你可能感兴趣的:(Android自定义View,滑动,事件传递小结)