阅读徐宜生《Android群英传》的笔记——第3章 Android控件架构与自定义控件详解(3.6-3.8)

3.6 自定义 View

在自定义 View 时,我们通常会去重写 onDraw() 方法来绘制 View 的显示内容。如果该 View 还需要使用 wrap_content 属性,那么还必须重写 onMeasure() 方法。另外,通过自定义 attrs 属性,还可以设置新的属性配置值。

在 View 中通常有以下一些比较重要的回调方法:

  • onFinishInflate():从 XML 加载组件后回调。
  • onSizeChanged():组件大小改变时回调。
  • onMeasure():回调该方法来进行测量。
  • onLayout():回调该方法来确定显示的位置。
  • onTouchEvent():监听到触摸事件时回调。

当然,创建自定义 View 的时候,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。这也是 Android 控件架构灵活性的体现。
通常情况下,有以下三种方法来实现自定义的控件:

  1. 对现有控件进行扩展;
  2. 通过组合来实现新的控件;
  3. 重写 View 来实现全新的控件。

3.6.1 对现有控件进行扩展

这是一个非常重要的自定义 View 方法,它可以在原生控件的基础上进行扩展,增加新的功能、修改显示的UI等。一般来说,我们可以在 onDraw() 方法中对原生控件进行扩展。

例子一,以一个 TextView 为例,来看看如何使用扩展原生控件的方法创建新的控件,比如想让一个 TextView 的背景更加丰富,给其多绘制几层背景。程序运行如下图所示:

这里写图片描述

布局代码为:

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

    <com.example.test.CustomTextView
        android:layout_width="80dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:text="测试数据" />

</RelativeLayout>

自定义 View 的代码(CustomTextView)为:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * 自定义 TextView 控件
 * Created by HourGlassRemember on 2016/9/6.
 */
public class CustomTextView extends TextView {

    //画笔
    private Paint mPaint1, mPaint2;

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

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

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

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //初始化画笔1
        mPaint1 = new Paint();
        //设置画笔1的颜色
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        //设置画笔1的风格,Paint.Style.FILL为实心
        mPaint1.setStyle(Paint.Style.FILL);

        //初始化画笔2
        mPaint2 = new Paint();
        //设置画笔2的颜色
        mPaint2.setColor(Color.YELLOW);
        //设置画笔2的风格,Paint.Style.FILL为实心
        mPaint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        /* 在回调父类方法前,实现自己的逻辑,对 TextView 来说即是在绘制文本内容前 **/
        //绘制外层矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
        //绘制内层矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint2);
        //保存画布当前的状态, canvas.save();和 canvas.restore();是相互匹配出现的,restore可以比save少,但不能多。如果restore调用次数比save多,会引发Error。
        //save之后可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作
        canvas.save();
        //绘制文字前平移10像素
        //translate(x,y):x代表在x轴上平移,正值代表向右平移,负值代表向左平移。y代表在Y轴上平移,正值代表向下平移,负值代表向上平移
        canvas.translate(10, 0);
        //父类完成的方法,即绘制文本
        super.onDraw(canvas);
        /* 在回调父类方法后,实现自己的逻辑,对 TextView 来说即是在绘制文本内容后 **/
        //恢复画布之前保存过的状态,防止save后对Canvas执行的操作对后续的绘制有影响。
        canvas.restore();
    }

}

例子二,再来看一个稍微复杂一点的 TextView,比如可以利用 LinearGradient 和 Matrix 来实现一个动态的文字闪动效果,程序运行如下图所示:(这个栗子还不是很理解啊!)

这里写图片描述

要想实现这一个效果,可以充分利用 Android 中 Paint 对象的 Shader 渲染器。通过设置一个不断变化的 LinearGradient,并使用带有该属性的 Paint 对象来绘制要显示的文字。

布局代码为:

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

    <com.example.test.CustomTextView
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:text="测试数据测试数据测试数据" />

</RelativeLayout>

自定义 View 的代码(CustomTextView)为:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.widget.TextView;

/**
* 自定义 TextView 控件

* Created by HourGlassRemember on 2016/9/6.
*/
public class CustomTextView extends TextView {

    //画笔
    private Paint mPaint;
    //线性渐变
    private LinearGradient mLinearGradient;
    //矩阵
    private Matrix mGradientMatrix;
    private int mViewWidth;
    private int mTranslate;

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

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //通过矩阵的方式来不断平移渐变效果
        if (null != mGradientMatrix) {
            mTranslate += mViewWidth / 5;
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            //设置失效时间为100毫秒,即0.1秒
            postInvalidateDelayed(100);
        }
    }

    /**
    * 组件大小改变时回调
    *
    * @param w
    * @param h
    * @param oldw
    * @param oldh
    */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                //获取当前绘制 TextView 的 Paint 对象
                mPaint = getPaint();
                //设置 LinearGradient 属性的值
                //设置 LinearGradient 属性的值
                //参数1:渐变起始点坐标X轴位置
                //参数2:渐变起始点坐标Y轴位置
                //参数3:渐变终点坐标X轴位置
                //参数4:渐变终点坐标Y轴位置
                //参数5:参与渐变效果的颜色集合
                //参数6:定义每个颜色处于的渐变相对位置,这个参数可以为null,为null表示所有的颜色按顺序均匀的分布
                //参数7:平铺方式
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE}, null, Shader.TileMode.CLAMP);
                //给这个 Paint 对象设置 LinearGradient 属性
                mPaint.setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }

}

Ps:Invalidate 和 postInvalidate 的区别
关于 Invalidate 和 postInvalidate 是做什么用的,老外是这样回答的:
Each class which is derived from the View class has the invalidate and the postInvalidate method.
If invalidate gets called it tells the system that the current view has changed and it should be redrawn as soon as possible. As this method can only be called from your UIThread another method is needed for when you are not in the UIThread and still want to notify the system that your View has been changed. The postInvalidate method notifies the system from a non-UIThread and the View gets redrawn in the next eventloop on the UIThread as soon as possible. It is also briefly explained in the SDK documentation.
Just compare invalidate and postInvalidate.
大致的意思是:
只要是 View 的子类,都会从 View 中继承 invalidate 和 postInvalidate 这两个方法。
当 invalidate 方法被调用的时候,就是在告诉系统当前的 View 发生改变,应该尽可能快的来进行重绘。这个方法仅能在UI线程中被调用。
如果想要在工作线程中进行刷新界面,那么其他的方法将会被调用,这个方法就是 postInvalidate 方法。这个方法将会发送消息到主线程,当主线程的消息队列轮询到当前消息的时候,这个方法会被调用。但是需要注意的是,刷新界面并不能保证马上刷新,只是尽可能快的进行刷新,尤其在 postInvalidate 方法中,这种情况会出现。
至于可能有人会问 postInvalidate 是怎么保证线程安全的,那么我们需要看一下 postInvalidate 的源码:

public void postInvalidate() {
        postInvalidateDelayed(0);
    }

public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

3.6.2 创建复合控件

创建复合控件可以很好地创建出具有重用功能的控件集合,这种方式通常需要继承一个合适的 ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的扩展性。
下面就以一个 TopBar为示例,讲解如何创建复合控件。
我们知道为了应用程序风格的统一,很多应用程序都有一些共通的 UI 界面,如果下图中所示的 TopBar 这样的一个标题栏:

这里写图片描述

通常情况下,这些界面都会被抽象出来,形成一个共通的 UI 组件。所有需要添加标题栏的界面都会引用这样的一个 TopBar,而不是每个界面都在布局文件中写这样的一个 TopBar。同时,设计者还可以给 TopBar 增加相应的接口,让调用者能够更加灵活地控制 TopBar,这样不仅可以提高界面的复用率,更能在需要修改 UI 时,做到快速修改,而不需要对每个页面的标题栏都进行修改。
下面我们就来看看该如何创建一个这样的 UI 模板。首先,模板应该具有通用性与可定制性,也就是说,我们需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息,而不是所有的模板都一样,那样就失去了模板的意义。

1、定义属性

为一个 View 提供可自定义的属性非常简单,只需要在 res 资源目录的 values 目录下创建一个 attrs.xml 的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。

<declare-styleable name="MyTopBar">
    <!--标题文字-->
    <attr name="mTitle" format="string" />
    <!-- 标题文字的大小-->
    <attr name="mTitleTextSize" format="dimension" />
    <!--标题文字的颜色-->
    <attr name="mTitleTextColor" format="color" />
    <!--左边文字的颜色-->
    <attr name="mLeftTextColor" format="color" />
    <!--左边文字的背景-->
    <attr name="mLeftBackground" format="reference|color" />
    <!--左边文字-->
    <attr name="mLeftText" format="string" />
    <!--右边文字的颜色-->
    <attr name="mRightTextColor" format="color" />
    <!--右边文字的背景-->
    <attr name="mRightBackground" format="reference|color" />
    <!--右边文字-->
    <attr name="mRightText" format="string" />
</declare-styleable>

我们在代码中通过 标签声明了自定义属性,通过 标签来声明具体的自定义属性,并通过 name 属性来确定引用的名称 ,通过 format 属性来指定属性的类型。
确定好属性后,就可创建一个自定义控件——TopBar,并让它继承 ViewGroup,从而组合一些需要的控件。这里为了简单,我们继承 RelativeLayout。在构造方法中,通过如下的代码来获取在 XML 布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样。

//通过这个方法将你在 attrs.xml 中定义的 declare-styleable 的所有属性的值存储到 TypeArray 中
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTopBar);
//从 TypeArray 中取出对应的值来为要设置的属性赋值
mLeftText = typedArray.getString(R.styleable.MyTopBar_mLeftText);
mLeftTextColor = typedArray.getColor(R.styleable.MyTopBar_mLeftTextColor, 0);
mLeftBackground = typedArray.getDrawable(R.styleable.MyTopBar_mLeftBackground);

mRightText = typedArray.getString(R.styleable.MyTopBar_mRightText);
mRightTextColor = typedArray.getColor(R.styleable.MyTopBar_mRightTextColor, 0);
mRightBackground = typedArray.getDrawable(R.styleable.MyTopBar_mRightBackground);

mTitleText = typedArray.getString(R.styleable.MyTopBar_mTitle);
mTitleColor = typedArray.getColor(R.styleable.MyTopBar_mTitleTextColor, 0);
mTitleSize = typedArray.getDimension(R.styleable.MyTopBar_mTitleTextSize, 10);

2、组合控件

接下来我们就可以来组合控件了,具体实现代码如下所示:

mLeftButton = new Button(context);
mRightButton = new Button(context);
mTextView = new TextView(context);

//为创建的组件元素赋值,值就来源于我们在引用的 xml 文件中给对应属性的赋值
mLeftButton.setText(mLeftText);
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackgroundDrawable(mLeftBackground);

mRightButton.setText(mRightText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackgroundDrawable(mRightBackground);

mTextView.setText(mTitleText);
mTextView.setTextSize(mTitleSize);
mTextView.setTextColor(mTitleColor);
mTextView.setGravity(Gravity.CENTER);

//为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(60, LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT | RelativeLayout.CENTER_VERTICAL, TRUE);
//添加到 ViewGroup中
addView(mLeftButton, mLeftParams);

mRightParams = new LayoutParams(60, LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
addView(mRightButton, mRightParams);

mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTextView, mTitleParams);

既然是 UI 模板,那么每个调用者所需要这些按钮能够实现的功能都是不一样的,因此,不能直接在 UI 模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者,实现过程如下所示:
首先是定义接口:

/**
* 接口对象,实现回调机制,在回调方法中通过映射的接口对象调用接口中的方法,

* 而不用去考虑如何实现,具体的实现由调用者去创建

*/
public interface topbarClickListener {
    void leftClick();

    void rightClick();
}

接着是暴露接口给调用者:

//接口对象
private topbarClickListener mListener;

/**
* 暴露一个方法给调用者来注册接口回调,通过接口来获得回调者对接口方法的实现

*
* @param mListener
*/
public void setOnTopBarClickListener(topbarClickListener mListener) {
    this.mListener = mListener;
}

//为左右按钮添加点击事件,但不去实现具体的逻辑,而是调用接口中相应的点击方法
mLeftButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        if (null != mListener) {
            mListener.leftClick();
        }
    }
});
mRightButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        if (null != mListener) {
            mListener.rightClick();
        }
    }
});

然后是实现接口回调:

//自定义标题栏
private TopBar mTopBar;

//设置监听事件,实现接口回调
mTopBar.setOnTopBarClickListener(new TopBar.topbarClickListener() {
    @Override
    public void leftClick() {
        Toast.makeText(MainActivity.this, "left", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void rightClick() {
        Toast.makeText(MainActivity.this, "right", Toast.LENGTH_SHORT).show();
    }
});

同样可以使用公共方法来动态地修改 UI 模板中的 UI,这样就进一步提高了模板的可定制性,代码如下:

/**
* 设置按钮的显示与否通过 id 区分按钮,flag 区分是否显示

*
* @param id id 为0代表左边的按钮,不为0代表右边的按钮

* @param flag 是否显示的标志

*/
public void setButtonVisible(int id, boolean flag) {
    if (id == 0) {
        mLeftButton.setVisibility(flag ? VISIBLE : GONE);
    } else {
        mRightButton.setVisibility(flag ? VISIBLE : GONE);
    }
}

通过上面的代码,调用者通过下面代码就可以动态地控制按钮的显示。

//控制 TopBar 上组件的状态
mTopBar.setButtonVisible(0, true);
mTopBar.setButtonVisible(1, false);

3、引用 UI 模板

最后一步,自然是在需要使用的地方引用 UI 模板,我们可以将这个 UI 模板写到一个布局文件当中,代码如下:

<com.example.test.MyTopBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="#FFFFFF"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    custom:mLeftBackground="@drawable/back"
    custom:mLeftTextColor="#FFFFFF"
    custom:mRightBackground="@drawable/more"
    custom:mRightTextColor="#FFFFFF"
    custom:mTitle="自定义标题"
    custom:mTitleTextColor="#123412"
    custom:mTitleTextSize="5sp">

</com.example.test.MyTopBar>

通过上面的代码,我们就可以在其他的布局文件中,直接通过标签来引用这个 UI 模板 View了,即:

<include layout="@layout/custom_top_bar" />

3.6.3 重写 View 来实现全新的控件

当 Android 系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义 View 来实现需要的功能。创建一个自定义 View,难点在于绘制控件和实现交互,这也是评价一个自定义 View 优劣的标准之一。通常需要继承 View 类,并重写它的 onDraw()、onMeasure() 等方法来实现绘制逻辑,同时通过重写 onTouchEvent() 等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义 View 的可定制性。
下面通过几个实例,让大家了解下如何创建一个自定义 View,不过为了让程序尽可能简单,这里就不去自定义属性值了。

实例一:弧线展示图

这里写图片描述

这个比例图非常清楚地展示一个项目所占的比例,简洁明了。因此,实现这样一个自定义 View 用在我们的程序中,可以让整个程序实现比较清晰的数据展示效果。那么该如何创建一个这样的自定义 View 呢?很明显,这个自定义 View 其实分为三个部分,分别是中间的圆形、中间显示的文字和外圈的弧线。具体实现的代码如下所示:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

/**
* 自定义弧线展示图

* Created by HourGlassRemember on 2016/9/7.
*/
public class ArcDiagram extends View {

    //屏幕宽度和高度
    private int measureWidth, measureHeight;

    //绘制圆的画笔
    private Paint mCirclePaint;
    //圆心x轴和y轴的坐标
    private float mCircleXY;
    //圆的半径
    private float mRadius;

    //绘制圆弧的画笔
    private Paint mArcPaint;
    //圆弧外轮廓矩形区域
    private RectF mArcRectF;
    //圆弧扫过的角度
    private float mSweepAngle;
    //圆弧扫过的默认值,这里默认值设置为25
    private float mSweepValue = 25;

    //绘制中间文字的画笔
    private Paint mTextPaint;
    //中间的文字
    private String mShowText;
    //中间文字的文字大小
    private float mShowTextSize;

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureWidth = getMeasuredWidth();
        measureHeight = getMeasuredHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制圆——参数:圆心的x轴坐标;圆心的y轴坐标;半径;绘制圆的画笔。
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);

        //绘制弧线——参数:指定圆弧的外轮廓矩形区域;圆弧起始角度,但是为度;圆弧扫过的角度,顺时针方向,单位为度;
        //如果为true时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形;绘制圆弧的画笔。
        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);

        //绘制文字
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView();
    }

    private void initView() {
        //这里为了简单,我们把 View 的绘制长度直接设置为屏幕的宽度
        float length = measureHeight >= measureWidth ? measureWidth : measureHeight;

//        绘制圆的参数
        //计算出圆心的x轴坐标和y轴坐标
        mCircleXY = length / 2;
        //计算出半径大小
        mRadius = length / 4.0f;
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));

//        绘制弧线,需要指定其椭圆的外接矩形
        //获得圆弧外轮廓矩形区域
        mArcRectF = new RectF(length * 0.1f, length * 0.1f, length * 0.9f, length * 0.9f);
        //设置圆弧扫过的默认角度
        mSweepAngle = (mSweepValue / 100f) * 360f;
        mArcPaint = new Paint();
        mArcPaint.setAntiAlias(true);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setStrokeWidth(length * 0.1f);
        mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));

//        绘制文字
        //中间显示文字的内容
        mShowText = "Android Skill";
        //中间显示文字的大小
        mShowTextSize = 50;
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mShowTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    /**
    * 设置扫描过的数值

    *
    * @param sweepValue
    */
    public void setSweepValues(float sweepValue) {
        mSweepValue = sweepValue != 0 ? sweepValue : 25;
        this.invalidate();
    }

}

布局文件代码:

<com.example.test.ArcDiagram
    android:id="@+id/arc_diagram"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:layout_marginTop="250dp" />

MainActivity的代码:

ArcDiagram arcDiagram = (ArcDiagram) findViewById(R.id.arc_diagram);
arcDiagram.setSweepValues(70);

实例二:音频条形图

静态音频条形图
图:静态音频条形图
这个功能是实现类似PC上某些音乐播放器上根据音频音量大小显示的音频条形图。由于只是演示自定义 View 的用法,我们就不去真实地监听音频输入了,随机模拟一些数字即可。具体实现的代码如下所示:

package com.example.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;

/**
* 自定义音频条形图

* Created by HourGlassRemember on 2016/9/7.
*/
public class AudioBarChart extends View {

    //画笔

    private Paint mPaint;
    //音频条形数

    private int mRectCount;
    //每个小条形的宽度

    private int mRectWidth;
    private int mWidth, mHeight;
    private int offset = 5;
    //线性渐变

    private LinearGradient mLinearGradient;

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

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

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

    private void initView() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.FILL);
        mRectCount = 12;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getWidth();
        mHeight = getHeight();
        mRectWidth = (int) (mWidth * 0.6 / mRectCount);
        //给音频条形图增加渐变效果

        mLinearGradient = new LinearGradient(0, 0, mWidth, mHeight, Color.RED, Color.GRAY, Shader.TileMode.CLAMP);
        mPaint.setShader(mLinearGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //下面是一种计算坐标的方法

        for (int i = 0; i < mRectCount; i++) {
            //让每个小矩形的高度随机变化

            float currentHeight = (float) (Math.random() * mHeight);
            canvas.drawRect(mWidth * 0.4f / 2 + mRectWidth * i + offset, currentHeight, mWidth * 0.4f / 2 + mRectWidth * (i + 1), mHeight, mPaint);
        }
        //设置每300毫秒刷新一次 View
        postInvalidateDelayed(500);
    }

}

3.7 自定义 ViewGroup

ViewGroup 存在的目的是为了对其子 View 进行管理,为其子 View 添加显示、响应的规则。因此,自定义 ViewGroup 通常需要重写 onMeasure() 方法来对子 View 进行测量,重写 onLayout() 方法来确定子 View 的位置,重写 onTouch() 方法增加响应事件。
下面通过一个实例,来看看如何自定义一个 ViewGroup。本例准备实现一个类似 Android 原生控件 ScrollView 的自定义 ViewGroup,自定义 ViewGroup 可以实现ScrollView 所具有的上下滑动功能,但是在滑动的过程中,增加一个黏性的效果,即当一个子 View 向上滑动大于一定的距离后,松开手指,它将自动向上滑动,显示下一个子 View,同理,如果滑动距离小于一定的距离,松开手指,它将自动滑动到开始的位置。这个功能就很像手机屏幕向上滑动解锁的功能。下面是代码部分:

自定义 ViewGroup 类:

package com.hourglassremember;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;

/**
* Created by HourGlassRemember on 2016/9/14.
*/
public class MyScrollView extends ViewGroup {

    //ViewGroup 的左上点与屏幕左上点的差值,正值代表 ViewGroup 的左上点位于屏幕左上点的上面,反之为下面
    private int mStart;
    //手指最后一次那个坐标点在屏幕的值,数值越大代表离屏幕左上点越远,反之越近
    private int mLastY;
    //View 滑动的辅助类,可以实现一种惯性的滚动过程和回弹效果,要注意的是,Scroller 本身不会去移动 View,它只是一个移动计算辅助类,
    //用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过 View 的 scrollTo、scrollBy 方法完成 View 的移动
    private Scroller mScroller;
    //屏幕高度
    private int mScreenHeight;

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

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

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(getContext());
    }

    /**
    * 初始化
    *
    * @param context
    */
    private void init(Context context) {
        //初始化屏幕高度
        //获得窗口管理器
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //新建一个 DisplayMetrics
        DisplayMetrics dm = new DisplayMetrics();
        //从窗口管理器中获得数据放到 DisplayMetrics 中
        wm.getDefaultDisplay().getMetrics(dm);
        //获得实际显示的像素值并赋值给 mScreenHeight
        mScreenHeight = dm.heightPixels;
        //创建一个滚动类 Scroller
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //使用遍历的方式来测量每一个子 View 的大小
        for (int i = 0; i < getChildCount(); i++) {
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //获得子 View 的个数
        int childCount = getChildCount();
        //MarginLayoutParams 主要用于定于和边缘的空白
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        //设置 ViewGroup 的高度,即子 View 的高度乘以子 View 的个数,因为每个子 View 都是充满屏幕的
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        //使用遍历的方式来设定子View 需要放置的位置
        for (int i = 0; i < childCount; i++) {
            //获得所有的子 View
            View childView = getChildAt(i);
            //如果 View 的显示状态为 gone 时,不计算它的位置
            if (childView.getVisibility() != View.GONE) {
                //主要修改每个子 View 的 top 和 bottom 属性,让他们能依次排列下来
                childView.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //手指触碰时在 y 轴方向的位置
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://手指按下时
                //手指按下时,最新位置是当前位置y
                mLastY = y;
                //getScrollX():代表 ViewGroup 在水平方向上的滚动距离,getScrollX()>0表示向左滚动,getScrollX()<0表示向右滚动
                //getScrollY():代表 ViewGroup 在垂直方向上的滚动距离,getScrollY()>0表示向上滚动,getScrollY()<0表示向下滚动
                //记录手指按下时 ViewGroup 当前的滚动距离
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE://手指滑动时
                //首先判断 mScroller 是否已经完成动作了,没有完成动作的话,立即停止动画
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                //计算本次滑动了多大的距离,即当前手指的坐标减去上一次停止时的坐标
                int dy = mLastY - y;
                //如果 ViewGroup 当前的滚动距离小于0 ,就代表 ViewGroup 的左上点位于屏幕左上点的下方,此时的状态应该是显示第一页,
                // 并向下拉,这时候需要复位,因为第一页下滑后还是第一页,故设置本次滑动的距离为0
                if (getScrollY() < 0) {
                    dy = 0;
                }
                //如果 ViewGroup 当前的滚动距离大于 ViewGroup 能够滚动的最大距离,就代表 ViewGroup 的左上点位于屏幕左上点的上方,
                //此时的状态应该是显示最后一页,并向上拉,这时候需要复位,因为最后一页上滑后还是最后一页,故设置本次滑动的距离为0。
                //注:ViewGroup 能够滚动的最大距离就是:(子 View 的高度-1) * 屏幕高度,因为每个子 View 都是占满一个屏幕的
                //(子 View 的高度-1) * 屏幕高度 与 getHeight() - mScreenHeight 是等价的,因为 getHeight() 就是 ViewGroup 的高度
                if (getScrollY() > getHeight() - mScreenHeight) {
                    dy = 0;
                }
                //执行滑动(跟着手指滑动)
                //scrollBy() 方法是在上一次偏移的情况下,进行偏移,偏移到 x=0,y=dy 的位置
                scrollBy(0, dy);
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP://手指抬起时
                int dScrollY = checkAlignment();
                //startScroll(int startX,int startY,int dx,int dy):x 轴方向偏移 dx 距离,
                //y 轴方向偏移 dy 距离,滚动会使用缺省值 250ms 作为持续时间
                if (dScrollY > 0) {//上滑
                    //上滑的距离若是小于 1/6 的话,就复位,复位需要的滑动距离就是手指按下到手指抬起时的这段距离
                    //上滑的距离若是大于等于 1/6 的话,就滑向下一页,滑动的距离相当于先复原再滑动一个屏幕的距离,上滑的时候偏移量为正
                    mScroller.startScroll(0, getScrollY(), 0, dScrollY < mScreenHeight / 6 ? -dScrollY : -dScrollY + mScreenHeight);
                } else {//下滑
                    //下滑的距离若是小于 1/6 的话,就复位,
                    //下滑的距离若是大于等于 1/6 的话,就滑向上一页,滑动的距离相当于先复原再滑动一个屏幕的距离,下滑的时候偏移量为负
                    mScroller.startScroll(0, getScrollY(), 0, -dScrollY < mScreenHeight / 6 ? -dScrollY : -dScrollY - mScreenHeight);
                }
                break;
        }
        postInvalidate();
        return true;
    }

    /**
    * 计算滑动距离
    *
    * @return
    */
    private int checkAlignment() {
        //记录手指抬起时,当前 ViewGroup 的滚动距离
        int mEnd = getScrollY();
        //mStart是手指按下时,当前 ViewGroup 的滚动距离
        //isUp 是记录两次 ViewGroup 的滚动距离之差,大于0记为true表示向上滑动,反之记为false表示向下滑动
        boolean isUp = mEnd - mStart > 0 ? true : false;
        //将当前 ViewGroup 的滚动距离取余,获得的值就是它相对于前面的距离
        //lastPrev 可正可负,正数代表上滑,越上滑数值越大,负数代表下滑,越下滑数值越小
        int lastPrev = mEnd % mScreenHeight;
        //lastPrev 与 lastNext 的和是 mScreenHeight
        int lastNext = mScreenHeight - lastPrev;
        return isUp ? lastPrev : -lastNext;
    }

    /**
    * 由父视图调用来请求子视图根据偏移值 mScrollX,mScrollY 重新绘制
    */
    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断 mScroller 是否滚动完成
        if (mScroller.computeScrollOffset()) {
            //从当前位置滚动到 x=0.y=mScroller.getCurrY() 的位置
            scrollTo(0, mScroller.getCurrY());
            //重绘
            postInvalidate();
        }
    }

}

布局文件:

package com.hourglassremember;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;

/**
* Created by HourGlassRemember on 2016/9/14.
*/
public class MyScrollView extends ViewGroup {

    //ViewGroup 的左上点与屏幕左上点的差值,正值代表 ViewGroup 的左上点位于屏幕左上点的上面,反之为下面
    private int mStart;
    //手指最后一次那个坐标点在屏幕的值,数值越大代表离屏幕左上点越远,反之越近
    private int mLastY;
    //View 滑动的辅助类,可以实现一种惯性的滚动过程和回弹效果,要注意的是,Scroller 本身不会去移动 View,它只是一个移动计算辅助类,
    //用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过 View 的 scrollTo、scrollBy 方法完成 View 的移动
    private Scroller mScroller;
    //屏幕高度
    private int mScreenHeight;

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

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

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(getContext());
    }

    /**
    * 初始化
    *
    * @param context
    */
    private void init(Context context) {
        //初始化屏幕高度
        //获得窗口管理器
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //新建一个 DisplayMetrics
        DisplayMetrics dm = new DisplayMetrics();
        //从窗口管理器中获得数据放到 DisplayMetrics 中
        wm.getDefaultDisplay().getMetrics(dm);
        //获得实际显示的像素值并赋值给 mScreenHeight
        mScreenHeight = dm.heightPixels;
        //创建一个滚动类 Scroller
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //使用遍历的方式来测量每一个子 View 的大小
        for (int i = 0; i < getChildCount(); i++) {
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //获得子 View 的个数
        int childCount = getChildCount();
        //MarginLayoutParams 主要用于定于和边缘的空白
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        //设置 ViewGroup 的高度,即子 View 的高度乘以子 View 的个数,因为每个子 View 都是充满屏幕的
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        //使用遍历的方式来设定子View 需要放置的位置
        for (int i = 0; i < childCount; i++) {
            //获得所有的子 View
            View childView = getChildAt(i);
            //如果 View 的显示状态为 gone 时,不计算它的位置
            if (childView.getVisibility() != View.GONE) {
                //主要修改每个子 View 的 top 和 bottom 属性,让他们能依次排列下来
                childView.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //手指触碰时在 y 轴方向的位置
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://手指按下时
                //手指按下时,最新位置是当前位置y
                mLastY = y;
                //getScrollX():代表 ViewGroup 在水平方向上的滚动距离,getScrollX()>0表示向左滚动,getScrollX()<0表示向右滚动
                //getScrollY():代表 ViewGroup 在垂直方向上的滚动距离,getScrollY()>0表示向上滚动,getScrollY()<0表示向下滚动
                //记录手指按下时 ViewGroup 当前的滚动距离
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE://手指滑动时
                //首先判断 mScroller 是否已经完成动作了,没有完成动作的话,立即停止动画
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                //计算本次滑动了多大的距离,即当前手指的坐标减去上一次停止时的坐标
                int dy = mLastY - y;
                //如果 ViewGroup 当前的滚动距离小于0 ,就代表 ViewGroup 的左上点位于屏幕左上点的下方,此时的状态应该是显示第一页,
                // 并向下拉,这时候需要复位,因为第一页下滑后还是第一页,故设置本次滑动的距离为0
                if (getScrollY() < 0) {
                    dy = 0;
                }
                //如果 ViewGroup 当前的滚动距离大于 ViewGroup 能够滚动的最大距离,就代表 ViewGroup 的左上点位于屏幕左上点的上方,
                //此时的状态应该是显示最后一页,并向上拉,这时候需要复位,因为最后一页上滑后还是最后一页,故设置本次滑动的距离为0。
                //注:ViewGroup 能够滚动的最大距离就是:(子 View 的高度-1) * 屏幕高度,因为每个子 View 都是占满一个屏幕的
                //(子 View 的高度-1) * 屏幕高度 与 getHeight() - mScreenHeight 是等价的,因为 getHeight() 就是 ViewGroup 的高度
                if (getScrollY() > getHeight() - mScreenHeight) {
                    dy = 0;
                }
                //执行滑动(跟着手指滑动)
                //scrollBy() 方法是在上一次偏移的情况下,进行偏移,偏移到 x=0,y=dy 的位置
                scrollBy(0, dy);
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP://手指抬起时
                int dScrollY = checkAlignment();
                //startScroll(int startX,int startY,int dx,int dy):x 轴方向偏移 dx 距离,
                //y 轴方向偏移 dy 距离,滚动会使用缺省值 250ms 作为持续时间
                if (dScrollY > 0) {//上滑
                    //上滑的距离若是小于 1/6 的话,就复位,复位需要的滑动距离就是手指按下到手指抬起时的这段距离
                    //上滑的距离若是大于等于 1/6 的话,就滑向下一页,滑动的距离相当于先复原再滑动一个屏幕的距离,上滑的时候偏移量为正
                    mScroller.startScroll(0, getScrollY(), 0, dScrollY < mScreenHeight / 6 ? -dScrollY : -dScrollY + mScreenHeight);
                } else {//下滑
                    //下滑的距离若是小于 1/6 的话,就复位,
                    //下滑的距离若是大于等于 1/6 的话,就滑向上一页,滑动的距离相当于先复原再滑动一个屏幕的距离,下滑的时候偏移量为负
                    mScroller.startScroll(0, getScrollY(), 0, -dScrollY < mScreenHeight / 6 ? -dScrollY : -dScrollY - mScreenHeight);
                }
                break;
        }
        postInvalidate();
        return true;
    }

    /**
    * 计算滑动距离
    *
    * @return
    */
    private int checkAlignment() {
        //记录手指抬起时,当前 ViewGroup 的滚动距离
        int mEnd = getScrollY();
        //mStart是手指按下时,当前 ViewGroup 的滚动距离
        //isUp 是记录两次 ViewGroup 的滚动距离之差,大于0记为true表示向上滑动,反之记为false表示向下滑动
        boolean isUp = mEnd - mStart > 0 ? true : false;
        //将当前 ViewGroup 的滚动距离取余,获得的值就是它相对于前面的距离
        //lastPrev 可正可负,正数代表上滑,越上滑数值越大,负数代表下滑,越下滑数值越小
        int lastPrev = mEnd % mScreenHeight;
        //lastPrev 与 lastNext 的和是 mScreenHeight
        int lastNext = mScreenHeight - lastPrev;
        return isUp ? lastPrev : -lastNext;
    }

    /**
    * 由父视图调用来请求子视图根据偏移值 mScrollX,mScrollY 重新绘制
    */
    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断 mScroller 是否滚动完成
        if (mScroller.computeScrollOffset()) {
            //从当前位置滚动到 x=0.y=mScroller.getCurrY() 的位置
            scrollTo(0, mScroller.getCurrY());
            //重绘
            postInvalidate();
        }
    }

}

引用的资源文件 style.xml:

<style name="img_scroll_view">//CustomScrollView 中的 ImageView 的样式
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:scaleType">fitCenter</item>
</style>

这里大家注意一个问题,就是在布局文件中如果用 RelativeLayout 作为父布局的话,会出现只显示第一张图片,然后不能够下滑了,原因本人也木有找到啦,如果正在阅读这边博客的你知道的话,麻烦刘留言下。所以还是用 LinearLayout 作为父布局,或者用自定义 ViewGroup 直接作为父布局也是可以的。

相关知识点补充:

注意,Android中的坐标与数学中的笛卡尔坐标是相反的,x轴与y轴都是相反的。
Scroller类的基本使用流程可以总结如下:
(1)首先通过Scroller类的startScroll()开始一个滑动动画控制,里面进行了一些轨迹参数的设置和计算;
(2)在调用startScroll()的后面调用invalidate();引起视图的重绘操作,从而触发ViewGroup中的computeScroll()被调用;
(3)在computeScroll()方法中,先调用Scroller类中的computeScrollOffset()方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算出的当前滑动的偏移坐标,调用View的scrollTo()方法进行滑动控制,最后也需要调用invalidate();进行重绘。

3.8 事件拦截机制分析

当 Android 系统捕获到用户的各种输入事件后,如何准确地传递给真正需要这个事件的控件呢? Android 给我们提供了一整套完善的事件传递、处理机制, 来帮助开发者完成准确的事件分配与处理。
要了解触摸事件的拦截机制,首先要了解什么是触摸事件?顾名思义,触摸事件就是捕获触摸屏幕后产生的事件。当点击一个按钮时,通常就会产生两个或者三个事件——按钮按下,这是事件一;如果不小心滑动一点,这就是事件二;当手抬起,这是事件三。Android 为触摸事件封装了一个类 MotionEvent。其实,只是要重写触摸相关的方法,参数一般都含有 MotionEvent,可见它的重要性。
在 MotionEvent 里面封装了不少好东西,比如触摸点的坐标,可以通过 event.getX() 方法和 event.getRawX() 方法取出坐标点;再比如获得点击的事件类型,可以通过不同的 Action(如 MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE)来进行区分,并实现不同的逻辑。
如此看来,触摸事件还是比较简单的,其实就是一个动作类型加坐标而已。但是我们知道,Android 的 View 结构是树形结构,也就是说,View 可以放在 ViewGroup 里面,通过不同的组合来实现不同的样式。那么问题就来了,View 放在一个 VIewGroup 里面,这个 ViewGroup 又放在另一个 ViewGroup 里面,甚至还有可能继续嵌套,一层层叠起来。可我们的触摸事件就一个,到底该分给谁呢?同一事件,子 View 和父 ViewGroup 都有可能想要进行处理。因此,这就产生了“事件拦截”这个“霸气”的称呼。
下面是一个例子,假设你所在的公司,有一个总经理,级别最高;他下面有一个部长,级别次之;最底层,就是干活的你,没有级别。现在董事会交给总经理一项任务,总经理将这项任务布置给部长,部长又把任务安排给了你。而当你好不容易干完活了,你就把任务交给部长,部长觉得任务完成得不错,于是就签了他的名字交给总经理,总经理看了也觉得不错,就也签了名字交给董事会。这样,一个任务就顺利完成了。如果大家能非常清楚地理解这样一个场景,那么对于事件拦截机制,你就超过了40%的开发者了。
一个总经理 —— MyViewGroupA,最外层的 ViewGroup
一个部长 —— MyViewGroupB,中间的 ViewGroup
一个干活的你 —— MyView,在最底层
本实例的整个布局结构如下所示:
这里写图片描述
(事件拦截实例)

这里写图片描述
(实例布局结构)

代码非常简单,只是重写了事件拦截和处理的几个方法,并给它加上一些 Log 而已。
代码如下:
1、MyViewGroupA:

package com.example.test;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
* Created by HourGlassRemember on 2016/9/18.
*/
public class MyViewGroupA extends LinearLayout {

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("HourGlassRemember", "MyViewGroupA dispatchTouchEvent " + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("HourGlassRemember", "MyViewGroupA onInterceptTouchEvent " + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("HourGlassRemember", "MyViewGroupA onTouchEvent " + event.getAction());
        return super.onTouchEvent(event);
    }

}

2、MyViewGroupB:

package com.example.test;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
* Created by HourGlassRemember on 2016/9/18.
*/
public class MyViewGroupB extends LinearLayout {

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("HourGlassRemember", "MyViewGroupB dispatchTouchEvent " + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("HourGlassRemember", "MyViewGroupB onInterceptTouchEvent " + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("HourGlassRemember", "MyViewGroupB onTouchEvent " + event.getAction());
        return super.onTouchEvent(event);
    }

}

3、MyView:

package com.example.test;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
* Created by HourGlassRemember on 2016/9/18.
*/
public class MyView extends View {

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("HourGlassRemember", "MyView onTouchEvent " + event.getAction());
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("HourGlassRemember", "MyView dispatchTouchEvent " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

}

MainActivity 的布局文件 activity_main.xml:

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

    <com.example.test.MyViewGroupA
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#F00">

        <com.example.test.MyViewGroupB
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="#0F0">

            <com.example.test.MyView
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:background="#00F" />
        </com.example.test.MyViewGroupB>
    </com.example.test.MyViewGroupA>

</RelativeLayout>

从上面的代码中可以看到,ViewGroup 级别比较高,比 View 多了一个方法 —— onInterceptTouchEvent(),这个方法看名字就能猜到是事件拦截的核心方法。我们先不修改任何返回值,只是点击一下 View,然后看 Log 会怎样记录我们的操作和程序的响应。
点击 View 后的 Log 如下所示:
这里写图片描述

可以看见,正常情况下,事件的传递顺序是:
总经理(MyViewGroupA)->部长(MyViewGroupB)->你(View)。事件传递的时候,先执行 dispatchTouchEvent() 方法,再执行 onInterceptTouchEvent() 方法。
事件的处理顺序是:
你(MyView)->部长(MyViewGroupB)->总经理(MyViewGroupA )。事件处理都是执行 onTouchEvent() 方法。
事件传递的返回值非常容易理解:True,拦截不继续;False,不拦截,继续流程。
事件处理的返回值也类似:True,处理了,不用审核了;False,给上级处理。
初始情况下,返回值都是 false。
这里为了能够方便大家理解事件拦截的过程,在事件传递中,我们只关心 onInterceptTouchEvent() 方法,而 dispatchTouchEvent() 方法虽然是事件分发的第一步,但一般情况下,我们不太会去改写这个方法,所以暂时不管这个方法。可以把上面的整个事件过程整理成如下图所示的一张图:
事件处理过程
(事件处理过程)
相信大家只要把 MyView 想成自己,就能充分地理解事件分发、拦截、处理的整个流程了。
下面我们稍微改动一下,假设总经理(MyViewGroupA )发现这个任务太简单了,觉得自己完成就可以了,完全没必要再找下属。因此事件就被总经理(MyViewGroupA)使用 onInterceptTouchEvent() 方法把事件给拦截了,即让 MyViewGroupA 的 onInterceptTouchEvent() 方法返回 True,我们再来看一下 Log:
这里写图片描述

跟我们设想的一样,总经理(MyViewGroupA)把所有的事情都干了,没后面的人的事了。同理,我们让部长(MyViewGroupB)也来当一次好人,即让部长(MyViewGroupB)使用 onInterceptTouchEvent() 方法返回 True,把事件拦截下来,Log 会是以下这样的:
这里写图片描述

可以看到,这次部长(MyViewGroupB)当了好人,你(MyView)就不用干活了。那么这两种情况也可以整理成下面的两张图:
这里写图片描述
(总经理(MyViewGroupA)的拦截事件)

这里写图片描述
(部长(MyViewGroupB)的拦截事件)

对事件的分发、拦截,现在大家应该比较清楚了,下面我们再看看事件的处理。先来看看底层人民 —— 你(MyView)。最开始的时候讲了,当你处理完任务后向上级报告,需要上级的确认,所以你的事件处理返回 False。那么突然有一天你受不了老板的压迫,罢工不干了,那么你的任务就没人做了,也就不用报告上级了,所以就直接返回 True。现在再来看看 Log:
这里写图片描述

可以看见,事件传递跟以前一样,但是事件处理,到你(MyView)这就结束了,因为你返回 True,表示不用向上级汇报了。这时,我们同样来整理下关系图:
这里写图片描述

你(MyView)终于翻身做了主,决定了自己的命运。但是,如果部长(MyViewGroupB)看到了你的报告,觉得太丢人,不敢给总经理(MyViewGroupA)看,所以他就偷偷地返回了 True,这个事件也就到此为止了,即部长(MyViewGroupB)将自己的 onTouchEvent() 方法返回 True,Log 如下所示:
这里写图片描述

他们的关系图如下所示:
这里写图片描述

通过对前面几种情况的分析,相信大家能比较容易地了解事件的分发、拦截、处理事件的流程了。在后面的学习中,结合源码,你会更加深入地理解,为什么流程会是这样的?初学者在学习的时候,最好先对流程有一个大致的认识之后,再去接触源码,这样就不会一头雾水,摸不着头脑,从而丧失学习的兴趣。

猜你喜欢

转载自blog.csdn.net/u010102829/article/details/70856684