Android View事件----全解

View

 

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联

View 的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算 View 的大小,需要的话则计算
  • layout: 判断是否需要重新计算 View 的位置,需要的话则计算
  • draw: 判断是否需要重新绘制 View,需要的话则重绘制

MotionEvent

事件说明
ACTION_DOWN手指刚接触到屏幕
ACTION_MOVE手指在屏幕上移动
ACTION_UP手机从屏幕上松开的一瞬间
ACTION_CANCEL触摸事件取消

 

 

 

 

 

 

VelocityTracker

VelocityTracker 可用于追踪手指在滑动中的速度:

view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity = (int) velocityTracker.getXVelocity();
        int yVelocity = (int) velocityTracker.getYVelocity();
        velocityTracker.clear();
        velocityTracker.recycle();
        return false;
    }
});

 

GestureDetector

GestureDetector 辅助检测用户的单击、滑动、长按、双击等行为:

final GestureDetector mGestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) { return false; }

    @Override
    public void onShowPress(MotionEvent e) { }

    @Override
    public boolean onSingleTapUp(MotionEvent e) { return false; }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }

    @Override
    public void onLongPress(MotionEvent e) { }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
});
mGestureDetector.setOnDoubleTapListener(new OnDoubleTapListener() {
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) { return false; }

    @Override
    public boolean onDoubleTap(MotionEvent e) { return false; }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) { return false; }
});
// 解决长按屏幕后无法拖动的问题
mGestureDetector.setIsLongpressEnabled(false);
imageView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
});

 

 

View 的事件分发(重磅!!!!!!!!)

view层的传递分为上层和底层,下面,我们先来说一下上层的传递机制及原理

上图

事件的传递总的来说就是三个方法:

  • 1、分发:dispatchTouchEvent;
  • 2、拦截:onInterceptTouchEvent;
  • 3、处理:onTouchEvent;

自上而下,对view的事件进行分发,从Activity到ViewGroup再到View,如果所有控件都不做处理,则,此事件作废不做处理,事件分发处理图解如上

下面,我们对所有事件进行分析,首先,我们先做一些基础的普及:

1,事件分发的对象

点击事件~~~即touch事件就是事件分发的对象

当用户触摸屏幕时,就会产生Touch事件,而touch事件被封装成MotionEvent对象

事件类型具体动作
MotionEvent.ACTION_DOWN按下View(所有事件的开始)
MotionEvent.ACTION_UP抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE滑动View
MotionEvent.ACTION_CANCEL结束事件(非人为原因)

从手指接触屏幕到离开屏幕所经历的流程图如下:

当用户出发MotionEvent事件开始,中间会经历无数个Move事件,系统会将这个事件交给具体的View去处理

2,事件分发的本质

将事件交给具体View去处理的过程就是事件分发,即事件的传递过程

3,事件分发过程由哪些方法协作完成?

将上面的诉说由一张图表示,如下:

源码分析

 

1,Activity源码分析

/**
  * 源码分析:Activity.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) {

            // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {

                onUserInteraction();
                // ->>分析1

            }

            // ->>分析2
            if (getWindow().superDispatchTouchEvent(ev)) {

                return true;
                // 若getWindow().superDispatchTouchEvent(ev)的返回true
                // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
                // 否则:继续往下调用Activity.onTouchEvent

            }
            // ->>分析4
            return onTouchEvent(ev);
        }


/**
  * 分析1:onUserInteraction()
  * 作用:实现屏保功能
  * 注:
  *    a. 该方法为空方法
  *    b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
  */
      public void onUserInteraction() { 

      }
      // 回到最初的调用原处

/**
  * 分析2:getWindow().superDispatchTouchEvent(ev)
  * 说明:
  *     a. getWindow() = 获取Window类的对象
  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象
  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
  */
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {

        return mDecor.superDispatchTouchEvent(event);
        // mDecor = 顶层View(DecorView)的实例对象
        // ->> 分析3
    }

/**
  * 分析3:mDecor.superDispatchTouchEvent(event)
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
    public boolean superDispatchTouchEvent(MotionEvent event) {

        return super.dispatchTouchEvent(event);
        // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
        // 即 将事件传递到ViewGroup去处理,详细请看ViewGroup的事件分发机制

    }
    // 回到最初的调用原处

/**
  * 分析4:Activity.onTouchEvent()
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
  public boolean onTouchEvent(MotionEvent event) {

        // 当一个点击事件未被Activity下任何一个View接收 / 处理时
        // 应用场景:处理发生在Window边界外的触摸事件
        // ->> 分析5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        
        return false;
        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
    }

/**
  * 分析5:mWindow.shouldCloseOnTouch(this, event)
  */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    // 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
            && isOutOfBounds(context, event) && peekDecorView() != null) {
        return true;
    }
    return false;
    // 返回true:说明事件在边界外,即 消费事件
    // 返回false:未消费(默认)
}
// 回到分析4调用原处

图解如下:

2,ViewGroup事件分发机制

/**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) { 

    ... // 仅贴出关键代码

        // 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {  

            // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
            // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
                    // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
                    // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
                    // c. 关于onInterceptTouchEvent() ->>分析1

                ev.setAction(MotionEvent.ACTION_DOWN);  
                final int scrolledXInt = (int) scrolledXFloat;  
                final int scrolledYInt = (int) scrolledYFloat;  
                final View[] children = mChildren;  
                final int count = mChildrenCount;  

        // 重点分析2
            // 通过for循环,遍历了当前ViewGroup下的所有子View
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  

                    // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                    // 若是,则进入条件判断内部
                    if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                        // 条件判断的内部调用了该View的dispatchTouchEvent()
                        // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
                        if (child.dispatchTouchEvent(ev))  { 

                        mMotionTarget = child;  
                        return true; 
                        // 调用子View的dispatchTouchEvent后是有返回值的
                        // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                        // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                        // 即把ViewGroup的点击事件拦截掉

                                }  
                            }  
                        }  
                    }  
                }  
            }  
            boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                    (action == MotionEvent.ACTION_CANCEL);  
            if (isUpOrCancel) {  
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
            }  
            final View target = mMotionTarget;  

        // 重点分析3
        // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
        if (target == null) {  
            ev.setLocation(xf, yf);  
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
            
            return super.dispatchTouchEvent(ev);
            // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
            // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
            // 此处需与上面区别:子View的dispatchTouchEvent()
        } 

        ... 

}
/**
  * 分析1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
  *     b. 返回false = 不拦截(默认)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    return false;

  } 
  // 回到调用原处

图解如下:

3,View事件的分发机制

/**
  * 源码分析:View.dispatchTouchEvent()
  */
  public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //     1. mOnTouchListener != null
  //     2. (mViewFlags & ENABLED_MASK) == ENABLED
  //     3. mOnTouchListener.onTouch(this, event)
  // 下面对这3个条件逐个分析


/**
  * 条件1:mOnTouchListener != null
  * 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  
    // 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
        
} 

/**
  * 条件2:(mViewFlags & ENABLED_MASK) == ENABLED
  * 说明:
  *     a. 该条件是判断当前点击的控件是否enable
  *     b. 由于很多View默认enable,故该条件恒定为true
  */

/**
  * 条件3:mOnTouchListener.onTouch(this, event)
  * 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
  */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });
    // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
    // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

OnTouch事件

/**
  * 源码分析:View.onTouchEvent()
  */
  public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  

    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
         
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  

    // 若该控件可点击,则进入switch判断中
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

                switch (event.getAction()) { 

                    // a. 若当前的事件 = 抬起View(主要分析)
                    case MotionEvent.ACTION_UP:  
                        boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  

                            ...// 经过种种判断,此处省略

                            // 执行performClick() ->>分析1
                            performClick();  
                            break;  

                    // b. 若当前的事件 = 按下View
                    case MotionEvent.ACTION_DOWN:  
                        if (mPendingCheckForTap == null) {  
                            mPendingCheckForTap = new CheckForTap();  
                        }  
                        mPrivateFlags |= PREPRESSED;  
                        mHasPerformedLongPress = false;  
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                        break;  

                    // c. 若当前的事件 = 结束事件(非人为原因)
                    case MotionEvent.ACTION_CANCEL:  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                        removeTapCallback();  
                        break;

                    // d. 若当前的事件 = 滑动View
                    case MotionEvent.ACTION_MOVE:  
                        final int x = (int) event.getX();  
                        final int y = (int) event.getY();  
        
                        int slop = mTouchSlop;  
                        if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                                (y < 0 - slop) || (y >= getHeight() + slop)) {  
                            // Outside button  
                            removeTapCallback();  
                            if ((mPrivateFlags & PRESSED) != 0) {  
                                // Remove any future long press/tap checks  
                                removeLongPressCallback();  
                                // Need to switch from pressed to not pressed  
                                mPrivateFlags &= ~PRESSED;  
                                refreshDrawableState();  
                            }  
                        }  
                        break;  
                }  
                // 若该控件可点击,就一定返回true
                return true;  
            }  
             // 若该控件不可点击,就一定返回false
            return false;  
        }

/**
  * 分析1:performClick()
  */  
    public boolean performClick() {  

        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
            // 只要我们通过setOnClickListener()为控件View注册1个点击事件
            // 那么就会给mOnClickListener变量赋值(即不为空)
            // 则会往下回调onClick() & performClick()返回true
        }  
        return false;  
    }  

图解如下:

总结

一张图解释上面我们说的

自定义View

自定义view分为以下几种方式:

  • 自定义组合控件:多个控件组合成一个,方便多处服用
  • 继承系统View控件:继承自TextView等系统控件,在系统控件的基础功能上进行扩展
  • 继承 View:不复用系统控件逻辑,继承View进行功能定义
  • 继承系统 ViewGroup:继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展
  • 继承 View ViewGroup:不复用系统控件逻辑,继承ViewGroup进行功能定义

所有的自定义控件,都会有构造方法,我们先用一个实例来展示并说明一下他们的构造方法的作用吧,随便写一个类,继承View,代码如下:

package com.xunfei.myapplication.view;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * Create by wangyongzheng on 20-12-9
 * package:com.xunfei.myapplication.view
 */
public class MyTextView extends View {
    public MyTextView(Context context) {
        super(context);
    }


    /**
     * 在xml布局文件中使用时自动调用
     * @param context
     */
    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    /**
     * 只有在API版本>21时才会用到
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

下面,我们分别来说说上面说到的几个自定义View的构造方法吧

1,自定义组合控件

老规矩,还是通过一个实例代码来解释

自定义View代码

package com.xunfei.myapplication.view;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.xunfei.myapplication.R;

import androidx.annotation.RequiresApi;

/**
 * Create by wangyongzheng on 20-12-9
 * package:com.xunfei.myapplication.view
 */
public class MyView1 extends RelativeLayout {
    /** 标题 */
    private TextView mTitle;
    /** 描述 */
    private TextView mDesc;

    public MyView1(Context context) {
        super(context);
    }

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

    public MyView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
    }

    protected void initView(Context context){
        View rootView = LayoutInflater.from(context).inflate(R.layout.my_view1, this);
        mDesc = rootView.findViewById(R.id.text1);
        mTitle = rootView.findViewById(R.id.text2);
    }
}

布局代码:

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

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:includeFontPadding="false"
        android:maxLines="2"
        android:text="title" />

    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/feed_item_com_cont_title"
        android:ellipsize="end"
        android:includeFontPadding="false"
        android:maxLines="2"
        android:text="desc" />

</merge>

剩下的就是在布局文件中使用自定义的布局了

此自定义组合控件的目的就是为了减少重复布局的使用次数,将可重复使用的布局拿出来,自定义一个布局,方便复用

 

2,继承系统控件的自定义VIew

继承系统的控件可以分为继承 View子类(如 TextView 等)和继承 ViewGroup 子类(如 LinearLayout 等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

例子如下:

package com.example.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Shader;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;


import static android.support.v4.content.ContextCompat.getColor;

/**
 * 包含分割线的textView
 * 文字左右两边有一条渐变的分割线
 * 样式如下:
 * ———————— 文字 ————————
 */
public class DividingLineTextView extends TextView {
    /** 线性渐变 */
    private LinearGradient mLinearGradient;
    /** textPaint */
    private TextPaint mPaint;
    /** 文字 */
    private String mText = "";
    /** 屏幕宽度 */
    private int mScreenWidth;
    /** 开始颜色 */
    private int mStartColor;
    /** 结束颜色 */
    private int mEndColor;
    /** 字体大小 */
    private int mTextSize;


    /**
     * 构造函数
     */
    public DividingLineTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mTextSize = getResources().getDimensionPixelSize(R.dimen.text_size);
        mScreenWidth = getCalculateWidth(getContext());
        mStartColor = getColor(getContext(), R.color.colorAccent);
        mEndColor = getColor(getContext(), R.color.colorPrimary);
        mLinearGradient = new LinearGradient(0, 0, mScreenWidth, 0,
                new int[]{mStartColor, mEndColor, mStartColor},
                new float[]{0, 0.5f, 1f},
                Shader.TileMode.CLAMP);
        mPaint = new TextPaint();
    }

    public DividingLineTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DividingLineTextView(Context context) {
        this(context, null);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        int len = getTextLength(mText, mPaint);
        // 文字绘制起始坐标
        int sx = mScreenWidth / 2 - len / 2;
        // 文字绘制结束坐标
        int ex = mScreenWidth / 2 + len / 2;
        int height = getMeasuredHeight();
        mPaint.setShader(mLinearGradient);
        // 绘制左边分界线,从左边开始:左边距15dp, 右边距距离文字15dp
        canvas.drawLine(mTextSize, height / 2, sx - mTextSize, height / 2, mPaint);
        mPaint.setShader(mLinearGradient);
        // 绘制右边分界线,从文字右边开始:左边距距离文字15dp,右边距15dp
        canvas.drawLine(ex + mTextSize, height / 2,
                mScreenWidth - mTextSize, height / 2, mPaint);
    }

    /**
     * 返回指定文字的宽度,单位px
     *
     * @param str   要测量的文字
     * @param paint 绘制此文字的画笔
     * @return 返回文字的宽度,单位px
     */
    private int getTextLength(String str, TextPaint paint) {
        return (int) paint.measureText(str);
    }

    /**
     * 更新文字
     *
     * @param text 文字
     */
    public void update(String text) {
        mText = text;
        setText(mText);
        // 刷新重绘
        requestLayout();
    }


    /**
     * 获取需要计算的宽度,取屏幕高宽较小值,
     *
     * @param context context
     * @return 屏幕宽度值
     */
    public static int getCalculateWidth(Context context) {
        int height = context.getResources().getDisplayMetrics().heightPixels;
        // 动态屏幕宽度,在折叠屏手机上宽度在分屏时会发生变化
        int Width = context.getResources().getDisplayMetrics().widthPixels;

        return Math.min(Width, height);
    }
}

 

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

   // ...... 跟前面一样忽视
    <com.example.myapplication.DividingLineTextView
        android:id="@+id/divide"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />

</LinearLayout>

 

 

直接继承View

直接继承 View 会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写 onDraw 外还需要对 onMeasure 方法进行重写。

public class RectView extends View{
    //定义画笔
    private Paint mPaint = new Paint();

    /**
     * 实现构造方法
     * @param context
     */
    public RectView(Context context) {
        super(context);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(Color.BLUE);

    }

}

 重写 draw 方法,绘制正方形,注意对 padding 属性进行设置:

复制代码

/**
     * 重写draw方法
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取各个编剧的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //获取绘制的View的宽度
        int width = getWidth()-paddingLeft-paddingRight;
        //获取绘制的View的高度
        int height = getHeight()-paddingTop-paddingBottom;
        //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
    }

在 View 的源码当中并没有对 AT_MOST 和 EXACTLY 两个模式做出区分,也就是说 View 在 wrap_content 和 match_parent 两个模式下是完全相同的,都会是 match_parent,显然这与我们平时用的 View 不同,所以我们要重写 onMeasure 方法。

/**
     * 重写onMeasure方法
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //处理wrap_contentde情况
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, 300);
        }
    }

整个过程大致如下,直接继承 View 时需要有几点注意:

  1. 在 onDraw 当中对 padding 属性进行处理。

  2. 在 onMeasure 过程中对 wrap_content 属性进行处理。

  3. 至少要有一个构造方法。

继承ViewGroup

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <com.example.myapplication.MyHorizonView
        android:layout_width="wrap_content"
        android:background="@color/colorAccent"
        android:layout_height="400dp">

        <ListView
            android:id="@+id/list1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorAccent" />

        <ListView
            android:id="@+id/list2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary" />

        <ListView
            android:id="@+id/list3"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimaryDark" />

    </com.example.myapplication.MyHorizonView>

    <TextView
        android:id="@+id/text"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:clickable="true"
        android:focusable="true"
        android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" />

    <com.example.myapplication.MyTextView
        android:id="@+id/myview"
        android:layout_width="1dp"
        android:layout_height="2dp"
        android:clickable="true"
        android:enabled="false"
        android:focusable="true"
        app:testAttr="520"
        app:text="helloWorld" />

    <com.example.myapplication.RectView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <com.example.myapplication.MyView1
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <com.example.myapplication.DividingLineTextView
        android:id="@+id/divide"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />


</LinearLayout>

一个 ViewGroup 里面放入 3 个 ListView,注意 ViewGroup 设置的宽是 wrap_conten,在测量的时候,会对 wrap_content 设置成与父 View 的大小一致,具体实现逻辑可看后面的代码。

代码比较多,我们结合注释分析。

 

public class MyHorizonView extends ViewGroup {

    private static final String TAG = "HorizontaiView";
    private List<View> mMatchedChildrenList = new ArrayList<>();


    public MyHorizonView(Context context) {
        super(context);
    }

    public MyHorizonView(Context context, AttributeSet attributes) {
        super(context, attributes);
    }

    public MyHorizonView(Context context, AttributeSet attributes, int defStyleAttr) {
        super(context, attributes, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                int childWidth = child.getMeasuredWidth();
                // 因为是水平滑动的,所以以宽度来适配
                child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mMatchedChildrenList.clear();
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // 如果不是确定的的值,说明是 AT_MOST,与父 View 同宽高
        final boolean measureMatchParentChildren = heightSpecMode != MeasureSpec.EXACTLY ||
                widthSpecMode != MeasureSpec.EXACTLY;
        int childCount = getChildCount();
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                final LayoutParams layoutParams = child.getLayoutParams();
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                if (measureMatchParentChildren) {
                    // 需要先计算出父 View 的高度来再来测量子 view
                    if (layoutParams.width == LayoutParams.MATCH_PARENT
                            || layoutParams.height == LayoutParams.MATCH_PARENT) {
                        mMatchedChildrenList.add(child);
                    }
                }
            }
        }

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            // 如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            // 如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
            setMeasuredDimension(getMeasuredWidth(), heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            // 如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
            setMeasuredDimension(widthSpecSize, getMeasuredHeight());
        }

        for (int i = 0; i < mMatchedChildrenList.size(); i++) {
            View matchChild = getChildAt(i);
            if (matchChild.getVisibility() != View.GONE) {
                final LayoutParams layoutParams = matchChild.getLayoutParams();
                // 计算子 View 宽的 MeasureSpec
                final int childWidthMeasureSpec;
                if (layoutParams.width == LayoutParams.MATCH_PARENT) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
                }
                // 计算子 View 高的 MeasureSpec
                final int childHeightMeasureSpec;
                if (layoutParams.height == LayoutParams.MATCH_PARENT) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.height);
                }
                // 根据 MeasureSpec 计算自己的宽高
                matchChild.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
}

这里我们只是重写了两个绘制过程中的重要的方法:onMeasure 和 onLayout 方法。

对于 onMeasure 方法具体逻辑如下:

  1. super.onMeasure 会先计算自定义 view 的大小;

  2. 调用 measureChild 对 子 View 进行测量;
  3. 自定义 view 设置的宽高参数不是 MeasureSpec.EXACTLY 的话,对于子 View 是 match_parent 需要额外处理,同时也需要对 MeasureSpec.AT_MOST 情况进行额外处理。

  4.  当自定义View 的大小确定后,在对子 View 是 match_parent 重新测量;

上述的测量过程的代码也是参考 FrameLayout 源码的,具体可以参看文章:

对于 onLayout 方法,因为是水平滑动的,所以要根据宽度来进行layout。

到这里我们的 View 布局就已经基本结束了。但是要实现 Viewpager 的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义 View 的时候也是会经常用到的

private void init(Context context) {
        mScroller = new Scroller(context);
        mTracker = VelocityTracker.obtain();
    }

    /**
     * 因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。
     * 重写onInterceptTouchEvent,对横向滑动事件进行拦截
     *
     * @param event
     * @return
     */
    @Override
    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;//必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
                break;
            case MotionEvent.ACTION_MOVE:
                intercepted = Math.abs(x - mLastX) > Math.abs(y - mLastY);
                break;
        }
        Log.d(TAG, "onInterceptTouchEvent: intercepted " + intercepted);
        mLastX = x;
        mLastY = y;
        return intercepted ? intercepted : super.onInterceptHoverEvent(event);
    }

    /**
     * 当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
     * 重写onTouchEvent方法
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                Log.d(TAG, "onTouchEvent: deltaX " + deltaX);

                // scrollBy 方法将对我们当前 View 的位置进行偏移
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: " + getScrollX());
                // getScrollX()为在X轴方向发生的便宜,mChildWidth * currentIndex表示当前View在滑动开始之前的X坐标
                // distance存储的就是此次滑动的距离
                int distance = getScrollX() - mChildWidth * mCurrentIndex;
                //当本次滑动距离>View宽度的1/2时,切换View
                if (Math.abs(distance) > mChildWidth / 2) {
                    if (distance > 0) {
                        mCurrentIndex++;
                    } else {
                        mCurrentIndex--;
                    }
                } else {
                    //获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
                    mTracker.computeCurrentVelocity(1000);
                    float xV = mTracker.getXVelocity();
                    //当X轴加速度>50时,也就是产生了快速滑动,也会切换View
                    if (Math.abs(xV) > 50) {
                        if (xV < 0) {
                            mCurrentIndex++;
                        } else {
                            mCurrentIndex--;
                        }
                    }
                }

                //对currentIndex做出限制其范围为【0,getChildCount() - 1】
                mCurrentIndex = mCurrentIndex < 0 ? 0 : mCurrentIndex > getChildCount() - 1 ? getChildCount() - 1 : mCurrentIndex;
                //滑动到下一个View
                smoothScrollTo(mCurrentIndex * mChildWidth, 0);
                mTracker.clear();

                break;
        }

        Log.d(TAG, "onTouchEvent: ");
        mLastX = x;
        mLastY = y;
        return super.onTouchEvent(event);
    }

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

    private void smoothScrollTo(int destX, int destY) {
        // startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
        mScroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
        // invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
        invalidate();
    }

    // 重写computeScroll方法
    @Override
    public void computeScroll() {
        super.computeScroll();
        // 当scroller.computeScrollOffset()=true时表示滑动没有结束
        if (mScroller.computeScrollOffset()) {
            // 调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            // 没有滑动结束,继续刷新View
            postInvalidate();
        }
    }

复制代码至此,View的大部分知识点已经总结完毕了,如果还有什么遗漏,后面大家评论我再补充吧。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值