《Android群英传》阅读笔记——第三章:Android控件架构与自定义控件详解

《Android群英传》阅读笔记——第三章:Android控件架构与自定义控件详解


一、Android控件架构


Android中的每个控件都会在界面中占得一块矩形的区域,控件大致分为两类:ViewGroup控件与View控件。ViewGroup控件相当于一个容器可以包含多个View控件,并管理其包含的View控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,这也就是我们常说的控件树了。上层控件负责下层子控件的测量与绘制,并传递交互事件。而我们在Activity中使用的findViewById()方法,就是在控件树中遍历查找View控件。在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有到的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。如下图便是一个View视图树:

通常情况下,当我们要显示一个Activity时,会在Activity生命周期的onCreate()的方法中,setContentView(),然后参数我们都是传R.layout.XXXX的,在调用该方法后,布局内容才能真正地显示出出来,那么setContentView()方法具体做了些什么呢?来看一眼Android界面的架构图:

我们可以看到,每个Activity都包含着一个Window对象,在Android中Window对象通常由PhoneWindow来实现的。PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView作为窗口界面的顶层视图,封装了一些窗口操纵的通用方法,可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上,这里面的所有View监听事件,都通过WindowManagerService来接受,并通过Activity对象来回调相应的onClickListener。在显示上,它将屏幕分成两部分,一个是TitleView,另一个是ContentView。看到这里大家一定看见了一个非常熟悉的界面ContentView。它是一个ID为content的Framelayout,activity_main.xml就是设置在这样一个Framelayout里,我们可以建立一个标准视图树,如下:

该视图树的第二层是一个LinearLayout,作为ViewGroup,这一层的布局结构会根据对应的参数设置不同的布局,如最常用的就是上面一个TitleBar,下面就是Content内容了,而如果我们通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏的话,视图树中就只有Content了(也就是上图中的FrameLayout),这也就解释了为什么调用requestWindowFeature()方法一定要在setContentView()方法之前才能生效的原因了。
而在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调onResume()方法(Activity的生命周期中,onResume()就是和用户交互了),此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终返程界面的绘制。


二、View的测量


在现实中,如果我们要去画一个图形,就必须知道它的大小和位置,同理,在Android中,我们若想绘制一个View,也必须也要先知道绘制该View的大小,这个过程在onMeasure()方法中进行。

    /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

Android给我们提供了一个设计短小精悍的类——MeasureSpec类,通过它来帮助我们测量View, MeasureSpec是一个32位的int值,其中高2位为测量模式,低30为测量的大小,在计算中使用位运算是为了提高并且优化效率。

测量模式
  • EXACTLY

精确值模式。当控件的layout_width属性或者layout_height属性指定为具体数值时,或,指定为match_parent属性时,系统会使用该模式。

  • AT_MOST

最大值模式。当控件的layout_width属性或者layout_height属性指定为wrap_content时,控件的大小会随着该控件的内容或子控件的大小变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸就好。

  • UNSPECIFIED

这个属性不指定其大小测量模式,我们可以按照我们的意愿设置成任意大小,一般不会用到,也不建议用。


View类默认的onMeasure()方法只支持EXACTLY模式,所以在自定义View时若不写onMeasure()方法,则只能使用EXACTLY模式,控件可以响应你指定的具体宽高值以及match_parent属性,而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法。

通过MeasureSpec这一个类,我们就获取了View的测量模式和View想要绘制的大小。有了这些信息,我们就可以控制View最后显示的大小,接下来,我们可以看一个简单的小例子,我们重写onMeasure这个方法:

    /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

windows系统下按住Ctrl查看super.onMeasure()这个方法,可以发现,系统最终还是会调用setMeasuredDimension()这个方法将测量的宽高设置进去从而完成测量工作。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

通过源码我们知道,我们自定义的宽高是如何设置的,下面我们通过这个例子,来讲一下自定义的测量值。


第一步,我们从MeasureSpec类中提取出具体的测量模式和大小:

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

然后我们通过判断测量的模式给出不同的测量值,当specMode为EXACTLY时,直接使用指定的specSize,当为其他两种模式地时候,我们就需要一个默认的值了,特别是wrap_content时,即AT_MOST模式,measureWidth()方法是这样的:

 private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

宽高的设置方法是一样的,所以,当我们在布局中设置match_parent时它就铺满容器了,要是设置wrap_content它就是包裹内容,如果不设置的话,那他就只有200px的大小了。

三、View的绘制


当我们用onMeasure()方法测量完成之后,我们就该重写onDraw()方法来绘制了,这个应该大家都很熟悉吧,首先我们要知道2D绘制的一些相关API:

  • Canvas

什么是Canvas?我们知道,在现实中我们若想画一个图像,则需要纸啦一类的,来画,那么Canvas顾名思义就是画布喽,我们的绘制都是在Canvas上进行的。而onDraw()中有一个参数,就是Canvas canvas对象,使用这个Canvas对象就可以进行绘图了,而在其他地方,通常需要使用代码创建一个Canvas对象,代码如下:

Canvas canvas = new Canvas(bitmap);

当创建一个Canvas对象时,为什么要传进去一个bitmap对象呢?不传入可不可以?其实,不传入也没关系,IDE编译虽然不会报错,但是一般我们不会这样做的,这是因为传进去的bitmap与通过这个bitmap创建的Canvas是紧密相连的,这个过程就是装载画布,这个bitmap用来存储所有绘制在Canvas上的像素信息。所以当我们通过这种方式创建了Canvas对象后,后面调用所有的Canvas.drawXXX方法都发生在这个bitmap上。
如下,是Canvas一些最基本的用法:

//绘制直线
canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);

//绘制矩形
canvas.drawRect(float left, float top, float right, float bottom, Paint paint);

//绘制圆形
canvas.drawCircle(float cx, float cy, float radius, Paint paint);

//绘制字符
canvas.drawText(String text, float x, float y, Paint paint);

//绘制图形
canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);

四、ViewGroup的测量


在前面我们说了,ViewGroup是用来管理View的,顾名思义ViewGroup是老大喽~,既然是老大,那么手下的小弟必定是对老大言听计从的,当ViewGroup的大小为wrap_content时,ViewGroup就会遍历手下的子View,来获取View的大小,从而决定自己的大小,而在其他模式下则会通过具体的值来设置自身的大小。

ViewGroup在遍历所有的子View时,会调用子View的onMeasure()方法来获取测量结果。

当ViewGroup的子View测量完毕后,就需要将子View放到合适的位置,这部分则是由onLayout()来进行的。当ViewGroup在执行onLayout()时,同样也会遍历子View的onLayout()方法,并制定其具体显示的位置,从而来决定其布局位置。

在自定义ViewGroup时,通常会去重写onLayout()方法来控制其子View显示位置的逻辑。同样,若是要支持wrap_content属性,那么我们还是要重写onMeasure()方法的,这点与View是相同的。


五、ViewGroup的绘制


ViewGroup通常情况下不需要绘制,因为它本身就没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么ViewGroup的onDraw()方法都不会被调用。但是!ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是遍历所有的子View,并调用子View的绘制方法来完成绘制的。

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
    }

六、自定义View


虽然Android给我们提供了丰富的组件库来创建丰富的UI效果,同时也提供了非常方便的拓展方法,通过继承Android的系统组件,我们可以非常方便地拓展现有功能,再系统组件的基础上创建新的功能,甚至可以自定义控件,来实现Android系统控件所没有的功能。

适当地使用自定义View,可以丰富应用程序的体验效果,但滥用自定义View则会带来适得其反的效果,所以要慎用哦~,而且,在系统原生控件经过多代版本的迭代后,在如今的版本中,依然还存在不少Bug,就更不要说我们自定义的View了,特别是现在Android ROM的多样性,导致Android的适配变得越来越复杂,很难保证自定义的View在其他手机上也能达到你想要的效果。

当然,了解Android系统自定义View的过程,可以帮助我们了解系统的绘图机制,可以通过自定义View来帮我们创建更加灵活的布局。

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

  • onFinishInflate()
    //从XML加载组件后回调
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }
  • onSizeChanged()
    //组件大小改变时回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }
  • onMeasure()
    //回调该方法进行测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
  • onLayout()
    //回调该方法来确定显示的位置
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }
  • onTouchEvent()
    //监听到触摸事件时回调
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
  • onDraw()
    //绘图
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

上面的方法并不需要都写出来,看个人需要,需要哪个写哪个。

通常情况下,实现自定义View有三种方法:

  • 对现有的控件进行拓展

  • 通过组合来实现新的控件

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

1、对现有的控件进行拓展


先来看一眼效果图,如下:

可以看到,第二个的Hello World,有了背景,且背景还带上了蓝色的描边,那么是怎么实现的呢,首先我们需要一个Canvas,然后需要两根画笔,颜色姑且不说哈,若是大家仔细看的话就会发现我们的Hello World向右移动了一点儿。没错我们就需要一个画布+两根画笔就是实现了这种效果。

我们先分步骤的实现一下:

初始化

        //实例blue_paint画笔
        blue_paint = new Paint();
        //设置颜色
        blue_paint.setColor(getResources().getColor(android.R.color.holo_blue_light));
        //设置画笔style(实心)
        blue_paint.setStyle(Paint.Style.FILL);
        //实例yellow_paint画笔
        yellow_paint = new Paint();
        //设置颜色
        yellow_paint.setColor(Color.YELLOW);
        //设置画笔style(实心)
        yellow_paint.setStyle(Paint.Style.FILL);

其实就是设置下我们的画笔,也没啥。

最重要的部分就是什么时候调用super了,然后我们开始绘制

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制外层
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), blue_paint);
        //绘制内层
        canvas.drawRect(5, 5, getMeasuredWidth() - 5, getMeasuredHeight() - 5, yellow_paint);
        //保存更改的画布
        canvas.save();
        //绘制文字前向右平移10像素
        canvas.translate(10, 0);
        //父类完成方法,即绘制文本
        super.onDraw(canvas);
        canvas.restore();
    }

其实在onDraw()方法中,绘制了两个矩形,然后在第一个矩形中又套了一个矩形,这样就达到了背景+描边的效果了,怎么样简单吧!然后若想将自定义TextView中的文字进行左右移动的话调用 canvas.translate()方法就好喽,第一个参数是控制左右的,向右平移为+,向左平移为-,第二个参数是控制上下的,向下平移为+,向上平移为-,怎么样,还算简单吧!


好的,上完整代码,代码如下:

package com.llx.lenovo.customview;

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

/*
 *  项目名:  CustomView 
 *  包名:    com.llx.lenovo.customview
 *  文件名:   CustomTextView
 *  创建者:   LLX
 *  创建时间:  2017/2/23 20:25
 *  描述:    自定义TextView
 */
public class CustomTextView extends TextView {
    //声明画笔
    private Paint blue_paint, yellow_paint;

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化
        init();
    }

    //初始化
    private void init() {
        //实例blue_paint画笔
        blue_paint = new Paint();
        //设置颜色
        blue_paint.setColor(getResources().getColor(android.R.color.holo_blue_light));
        //设置画笔style(实心)
        blue_paint.setStyle(Paint.Style.FILL);
        //实例yellow_paint画笔
        yellow_paint = new Paint();
        //设置颜色
        yellow_paint.setColor(Color.YELLOW);
        //设置画笔style(实心)
        yellow_paint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制外层
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), blue_paint);
        //绘制内层
        canvas.drawRect(5, 5, getMeasuredWidth() - 5, getMeasuredHeight() - 5, yellow_paint);
        //保存更改的画布
        canvas.save();
        //绘制文字前向右平移10像素
        canvas.translate(10, 0);
        //父类完成方法,即绘制文本
        super.onDraw(canvas);
        canvas.restore();
    }
}

接下来,我们看一个稍微复杂一点儿的,如图:

想实现这个效果并不太难,我们可以借助Android中LinearGradient,Shader,Matrix,来完成,来实现一个闪闪发光的闪动效果,我们充分的利用Shader渲染器,来设置一个不断变化的LinearGradient,首先我们要在onSizeChanged()方法中完成一些初始化操作:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            //获取View测量值
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                //获取画笔对象
                mPaint = getPaint();
                //渲染器
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                        null, Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);
                //矩阵
                matrix = new Matrix();
            }
        }
    }

其中最关键的就是getPaint()方法中获取当前特效他view的paint对象,并且设置LinearGradient属性,最后用矩阵不断平移渐变效果,就实现了这个效果,代码如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (matrix != null) {
            mTranslate += mViewWidth + 5;
            if (mTranslate > 2 * mViewWidth / 5) {
                mTranslate = -mViewWidth;
            }
            matrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(matrix);
            //每隔100毫秒闪动一下
            postInvalidateDelayed(100);
        }
    }

最后,完整代码如下:

package com.llx.lenovo.customview;

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;

/*
 *  项目名:  CustomView 
 *  包名:    com.llx.lenovo.customview
 *  文件名:   CustomTextViewGradient
 *  创建者:   LLX
 *  创建时间:  2017/2/23 21:48
 *  描述:    自定义TextView,文字渐变
 */
public class CustomTextViewGradient extends TextView {
    private int mViewWidth = 0;
    private Paint mPaint;
    //线性渐变
    private LinearGradient mLinearGradient;
    //矩阵
    private Matrix matrix;
    private int mTranslate;

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            //获取View测量值
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                //获取画笔对象
                mPaint = getPaint();
                //渲染器
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                        null, Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);
                //矩阵
                matrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (matrix != null) {
            mTranslate += mViewWidth + 5;
            if (mTranslate > 2 * mViewWidth / 5) {
                mTranslate = -mViewWidth;
            }
            matrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(matrix);
            //每隔100毫秒闪动一下
            postInvalidateDelayed(100);
        }
    }
}

2、创建复合控件


创建一个复合控件可以很好的创建出具有重要功能的控件集合,这种方式经常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组成一个新的合适的控件,通过这种方式创建的控件,我们一般都会给他指定的一些属性,让他具有更强的扩展性,下面就以一个TopBar为例子,讲解如何创建复合控件。


我们还是来看下效果,如下图:

第一种:

第二种:

乍一看,两种其实没什么区别,但是还是稍微有点儿区别的,下面我们先说一下,实现前的准备工作吧,如下:

  • 定义属性

为一个View提供可自定义的属性很简单,只需要在res->values下新建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。attrs.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="leftTextColor" format="color" />
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />
        <attr name="rightTextColor" format="color" />
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>
</resources>

一步一步来哈,先来说明一下这个attrs.xml的文件是做什么的,我们都知道在原生的控件中,不管是Button还是TextView当我们进行设置的时候都是android:layout_width……这一类格式的(android是命名空间),那么我们的这个attrs.xml是做什么的呢,我们不是自定义View嘛,没错,我们的这个attrs.xml就是做这个的,当我们要用的时候,要有自己的命名空间,然后后面跟的属性就是这个文件中的属性喽,假如说我们新建的命名空间为app,那么我们要引用leftText时,格式就是,app:leftText=”“,就是这种格式的。


我们在代码中通过标签声明了使用自定义属性,然后name相当于ID可以让我们的类可以找到。确定好后,我们新建一个CustomTopBarView类就可以开始搞啦!

CustomTopBarView完整代码如下:

package com.llx.lenovo.customview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

/*
 *  项目名:  CustomView
 *  包名:    com.llx.lenovo.customview
 *  文件名:   CustomTopBarView
 *  创建者:   LLX
 *  创建时间:  2017/2/23 22:39
 *  描述:    自定义TopBarView
 */
public class CustomTopBarView extends RelativeLayout {
    //左边文本颜色
    private int mLeftTextColor;
    //左边背景
    private Drawable mLeftBackground;
    //左边文本
    private String mLeftText;
    //右边文本颜色
    private int mRightTextColor;
    //右边背景
    private Drawable mRightBackgroud;
    //右边文本
    private String mRightText;
    //Title文字大小
    private float mTitleSize;
    //Title颜色
    private int mTitleColor;
    //Title文本
    private String mTitle;
    //TypedArray
    private TypedArray ta;
    //Button
    private Button mLeftButton;
    private Button mRightButton;
    //TextView
    private TextView mTitleView;
    //LayoutParams
    private LayoutParams mLeftParams;
    private LayoutParams mRightParams;
    private LayoutParams mTitlepParams;
    //监听回调接口
    private CustomTopBarClickListener mListener;

    //带参构造方法
    public CustomTopBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (attrs != null) {
            //通过这个方法,从attrs.xml文件下读取读取到的值存储到我们的TypedArray中
            ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
            //读取相对应的属性
            //左边View属性
            mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
            mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
            mLeftText = ta.getString(R.styleable.TopBar_leftText);
            //右边View属性
            mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
            mRightBackgroud = ta.getDrawable(R.styleable.TopBar_rightBackground);
            mRightText = ta.getString(R.styleable.TopBar_rightText);
            //Title属性
            mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
            mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
            mTitle = ta.getString(R.styleable.TopBar_title);
            //获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
            ta.recycle();
        }
        //初始化
        initView(context);
        mRightButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.rightClick();
            }
        });
        mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.leftClick();
            }
        });
    }

    //初始化
    private void initView(Context context) {
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);
        //为创建的mLeftButton赋值
        //文字颜色
        mLeftButton.setTextColor(mLeftTextColor);
        //背景
        mLeftButton.setBackground(mLeftBackground);
        //内容
        mLeftButton.setText(mLeftText);
        //为创建的mRightButton赋值,与mLeftButton同理
        mRightButton.setTextColor(mRightTextColor);
        mRightButton.setBackground(mRightBackgroud);
        mRightButton.setText(mRightText);
        //为创建的mTitleView赋值
        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleColor);
        //文字大小
        mTitleView.setTextSize(mTitleSize);
        //文字Gravity(靠左,居中,靠右等)
        mTitleView.setGravity(Gravity.CENTER);
        //为组件元素设置相应的布局元素
        //左边View
        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //显示位置
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        //将设置好的属性添加到自定义Layout中,并将mLeftButton与mLeftParams绑定,以下同理
        addView(mLeftButton, mLeftParams);
        //右边
        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);
        //Tietle
        mTitlepParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);
    }

    public void setOnTopbarClickListener(CustomTopBarClickListener mListener) {
        this.mListener = mListener;
    }

    public void setButtonVisable(int id, boolean flag) {
        if (flag) {
            if (id == 0) {
                mLeftButton.setVisibility(View.VISIBLE);
            } else {
                mRightButton.setVisibility(View.VISIBLE);
            }
        } else {
            if (id == 0) {
                mLeftButton.setVisibility(View.VISIBLE);
            } else {
                mRightButton.setVisibility(View.VISIBLE);
            }
        }
    }
}

OK,现在我们来仔细的研究一下这个代码哈,虽然,群英传讲的已经够详细的了,但是,无奈,理解能力差,又查了几篇博客,然后自己又仔细的看了下终于看懂了,这个坑终于爬出来了,我们一点点的分析哈。

大家应该发现了,在该类的属性中,其实可以分成三个主要的,大致就是,左边,中间,右边,因为我们在attrs.xml文件中也就是最多的设置了左边,中间,右边,所以在该类的属性中最多也就是三个喽。

然后看下构造方法,构造方法的代码如下:

    //带参构造方法
    public CustomTopBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (attrs != null) {
            //通过这个方法,从attrs.xml文件下读取读取到的值存储到我们的TypedArray中
            ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
            //读取相对应的属性
            //左边View属性
            mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
            mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
            mLeftText = ta.getString(R.styleable.TopBar_leftText);
            //右边View属性
            mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
            mRightBackgroud = ta.getDrawable(R.styleable.TopBar_rightBackground);
            mRightText = ta.getString(R.styleable.TopBar_rightText);
            //Title属性
            mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
            mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
            mTitle = ta.getString(R.styleable.TopBar_title);
            //获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
            ta.recycle();
        }
        //初始化
        initView(context);
        mRightButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.rightClick();
            }
        });
        mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.leftClick();
            }
        });
    }

在这个构造方法中,我们可以看到参数的类型分别为Context与AttributeSet,Context我们都熟悉哈,所以我来简单的说一下AttributeSet,这个其实…它就是我们刚开始时定义的那个属性,然后传到这里来,我们去用这些属性,毕竟,attrs与layout通过命名空间相关联,那么我们是不是感觉少点儿东西呢?在xml布局中,我们若是使用了该自定义的属性…,那么自定义在哪儿呢?我们并没有敲一行代码,所以不可能实现自定义的,所以,我们就需要attrs与我们的自定义View相关联了,那么在我们的构造方法下的AttributeSet,就弥补了这个空白,然后剩下的…就劳烦各位读者,看下注释吧,我感觉挺清楚的了,嘿嘿,然后构造方法中剩下的方法中就没什么的了哈,但是在initView(context);中还是有点儿东西的,下面我贴一下initView(context)哈,如下:

    //初始化
    private void initView(Context context) {
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);
        //为创建的mLeftButton赋值
        //文字颜色
        mLeftButton.setTextColor(mLeftTextColor);
        //背景
        mLeftButton.setBackground(mLeftBackground);
        //内容
        mLeftButton.setText(mLeftText);
        //为创建的mRightButton赋值,与mLeftButton同理
        mRightButton.setTextColor(mRightTextColor);
        mRightButton.setBackground(mRightBackgroud);
        mRightButton.setText(mRightText);
        //为创建的mTitleView赋值
        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleColor);
        //文字大小
        mTitleView.setTextSize(mTitleSize);
        //文字Gravity(靠左,居中,靠右等)
        mTitleView.setGravity(Gravity.CENTER);
        //为组件元素设置相应的布局元素
        //左边View
        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //显示位置
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        //将设置好的属性添加到自定义Layout中,并将mLeftButton与mLeftParams绑定,以下同理
        addView(mLeftButton, mLeftParams);
        //右边
        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);
        //Tietle
        mTitlepParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);
    }

因为我们在布局中并没有一个Button控件,自然就不能findViewById()了,所以只能new…了,然后它就需要传入一个上下文,直接把构造方法中的那个context拿来用就可以了,哈哈!三个都是一样的,然后设置的属性也就是类似的了,但是,将这个三个控件属性都设置完毕后,还是不可以的,因为我们毕竟继承的是RelativeLayout,所以…这三个控件的位置我们并不能确定,现在就需要我们的布局参数了,即!LayoutParams!实例化相应的对象时,我们必须告诉它,该布局占的空间是多大的,所以各位是不是看它的参数值很熟悉呢,当然自己也可以定义的哈!我们都知道在RelativeLayout中若是不规定某个控件的位置时,它默认的位置就是左上角的,所以第二步我们就要设置显示位置了,当显示的位置设定好以后,还有一个问题,没错!我们该设定的设了,但是…我们还没有将该属性设置到布局中去,所以就调用addView(),让该属性生效,当然我们也要将我们的控件传入进去,毕竟,所有的属性都设置好了,若是没有使用该属性的控件,岂不是很尴尬!所以以下同理喽~

剩下的就没有什么说的了,毕竟大框架都搭好,剩下的就是优化了,下面来看下我们自定义的View到底是什么样子的吧,在layout下新建topbar.xml,并修改如下:

<?xml version="1.0" encoding="utf-8"?>
<com.llx.lenovo.customview.CustomTopBarView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/top_bar"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:orientation="vertical"
    app:leftBackground="@color/colorAccent"
    app:leftText="BACK"
    app:leftTextColor="#Fff"

    app:rightBackground="@color/colorAccent"
    app:rightText="MORE"
    app:rightTextColor="#fff"

    app:title="自定义标题"
    app:titleTextColor="#000"
    app:titleTextSize="12sp" />

OK,我们在该节的开头时,就说了,要新建一个命名空间,故:

xmlns:app="http://schemas.android.com/apk/res-auto"

名字可以随便起哈,前提是,要符合规矩,哈哈!

然后很简答的一步了,修改MainActivity.java如下:

public class MainActivity extends AppCompatActivity {

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

其实就是修改了,setContentView中的参数,将加载的布局改为我们刚刚新建的topbar了,然后运行后,就是我们的那个第一种效果,如下:

然后第二种呢,也是这样的,就是将topbar,include到activity_main中了,activity_main.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <include
        layout="@layout/topbar"
        android:layout_width="match_parent"
        android:layout_height="40dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="Hello World!"
        android:textSize="30sp" />

    <com.llx.lenovo.customview.CustomTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="Hello World!"
        android:textSize="30sp" />

    <com.llx.lenovo.customview.CustomTextViewGradient
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="Hello World!"
        android:textSize="30sp" />

    <com.llx.lenovo.customview.CustomTopBarView
        android:id="@+id/custom_top_bar_view"
        android:layout_width="wrap_content"
        android:layout_height="40dp" />
</LinearLayout>

然后修改MainActivity.java的代码如下:

public class MainActivity extends AppCompatActivity {

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

这样运行的效果就是第二种了,如下:

OK,这个就是实现自定义控件的第二种方式,通过组合的方式实现新的控件了。下面我们来看下第三种方式是怎么实现控件的,继续看书吧。

3、重写View来实现全新的控件


当Android系统原生的控件无法满足我们的需求时,我们可以继承原有的控件进行修改,也可以将几个控件进行组合,当然也可以继承View来创建一个新的View喽。先看下效果:

我们就来实现一下它哈,不难的,首先,新建一个CustomCircleView类,代码如下:

package com.llx.lenovo.customview;

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

/*
 *  项目名:  CustomView
 *  包名:    com.llx.lenovo.customview
 *  文件名:   CustomCircleView
 *  创建者:   LLX
 *  创建时间:  2017/2/25 17:41
 *  描述:    自定义半弧圆
 */
public class CustomCircleView extends View {
    //圆的直径
    private int mCircleXY;
    //屏幕高宽
    private int w, h;
    //圆的半径
    private float mRadius;
    //圆的画笔
    private Paint mCirclePaint;
    //弧线的画笔
    private Paint mArcPaint;
    //文本画笔
    private Paint mTextPaint;
    //需要显示的文字
    private String mShowText = "李林雄";
    //文字大小
    private int mTextSize = 50;
    //圆心扫描的弧度
    private float mSweepAngle = 270;

    public CustomCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //获取屏幕高宽
        DisplayMetrics dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);
        w = dm.widthPixels;
        h = dm.heightPixels;
        //初始化
        init();
    }

    //初始化
    private void init() {
        mCircleXY = w / 2;
        mRadius = (float) (w * 0.5 / 2);
        //画笔实例化
        mCirclePaint = new Paint();
        //画笔设置颜色
        mCirclePaint.setColor(Color.BLUE);

        mArcPaint = new Paint();
        //设置线宽
        mArcPaint.setStrokeWidth(100);
        //设置空心
        mArcPaint.setStyle(Paint.Style.STROKE);
        //设置颜色
        mArcPaint.setColor(Color.BLUE);

        mTextPaint = new Paint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(mTextSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制矩形
        RectF mArcRectF = new RectF((float) (w * 0.1), (float) (w * 0.1), (float) (w * 0.9), (float) (w * 0.9));
        //绘制圆
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
        //绘制弧线
        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
        //绘制文本
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mTextSize / 4), mTextPaint);
    }

    public void setSweepValues(float sweepValues) {
        if (sweepValues != -0) {
            mSweepAngle = sweepValues;
        } else {
            //如果没有,我们默认设置
            mSweepAngle = 30;
        }
        //一定要刷新
        invalidate();
    }
}

这样,我们就实现了该节开头的那个效果图了,但是,大家应该会发现,还有一个public的方法,那么这个方法是做什么的呢?修改一下MainActivity.java的代码吧,修改如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.custom_cyrcle_view);
        CustomCircleView customCircleView = (CustomCircleView) findViewById(R.id.cicle_view);
        customCircleView.setSweepValues(180);
    }
}

为了演示方便一些,我单独把我们的这个自定义的View放到一个新的布局中了,这里其实不是重要的,重要的是后面那个调用该public的代码,我们还是看下效果图吧,然后,你就懂了,如下图:

嘿嘿,相信大家都看懂了哈,调用该方法,并将参数设为180,那么外部的扇形角度就是180了,当然我们可以设任意角度哈!

实现音频条形图

我们来做一个小练习哈,就是一个最常见的一个动画——音频条形图,哈哈,对它都不陌生吧,高高低低的,由于我们只是演示下自定义View的用法,所以就不用真实的数据了哈,就随机模拟一些数据就好了,这个比上面那个还难一点,但是,万变不离其宗啊,只要心中有图,那么就是写写算算,对对坐标了,也不难,我们先来看下最终效果,如下图:

可以看到,每个线条都在变化,想实现这个View其实也不太难,新建一个CustomMusicRectangularFigure类,代码如下:

package com.llx.lenovo.customview;

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;

import static android.R.attr.offset;

/*
 *  项目名:  CustomView 
 *  包名:    com.llx.lenovo.customview
 *  文件名:   CustomMusicRectangularFigure
 *  创建者:   LLX
 *  创建时间:  2017/2/26 2:42
 *  描述:    实现音频条形图
 */
public class CustomMusicRectangularFigure extends View {
    //View宽
    private int mWidth;
    //矩形的宽度
    private int mRectWidth;
    //矩形的高度
    private int mRectHeight;
    //画笔
    private Paint mPaint;
    //矩形的数量
    private int mRectCount;

    private int offset = 5;
    //随机数
    private double mRandom;
    //线性渐变效果
    private LinearGradient mLinearGradient;

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //遍历绘制矩形
        for (int i = 0; i < mRectCount; i++) {
            //获取随机数
            mRandom = Math.random();
            //当前矩形的高度,通过随机数决定
            float currentHeight = (float) (mRectHeight * mRandom);
            //开始绘制
            canvas.drawRect(
                    (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight,
                    (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint);
        }
        //300毫秒后通知onDraw进行View重绘
        postInvalidateDelayed(300);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //获取View的宽
        mWidth = getWidth();
        //获取View的高
        mRectHeight = getHeight();
        mRectWidth = (int) (mWidth * 0.6 / mRectCount);
        //设置线性渐变的属性
        mLinearGradient = new LinearGradient(0, 0, mRectWidth, mRectHeight, Color.YELLOW, Color.BLUE, Shader.TileMode.CLAMP);
        //通过画笔将线性渐变绘制到每个小矩形上面
        mPaint.setShader(mLinearGradient);
    }
}

OK,其实注释已经写的很详细了,但是,还是简略的说一下吧,说一下整体的思路哈,首先我们要自定义一个View,那么该View的宽和高是一定要知道的,要不然我们怎么决定可以画多少个矩形?每个矩形的高度又是多少,所以,对View的测量是很有必要的,故源码中,用mWidth代表了View的宽,高的话就用mRectHeight代替了,当我们知道了这些后,那么就该规定要有多少个小矩形了,当然我们可以根据测量到的View的宽来/矩形的宽,决定共有多少个小矩形,这里我就用mRectCount代替了哈,毕竟逻辑都是相同的,画的时候都要遍历进行画的,很显然只拥有这些值我们是远远不能让它动起来的,毕竟现在高度是固定的,那么我们该怎么样让它动起来呢,若想动,那就就需要我们的随机数啦,所以就用到了Math.random()来获取随机数,这样大致框架就完成了,但是,这样它只画一次啊,虽然有随机数的加入了,但是只有当多次重绘的时候,随机数的作用才能体现出来呢,所以就用到了我们的另一个方法postInvalidateDelayed(300),这个方法可以喽,300ms后通知onDraw(),进行重绘,由于我们用了随机数,这样它就可以动起来呢,然后主要的方法,主要的代码是那些呢?如下:

  //遍历绘制矩形
        for (int i = 0; i < mRectCount; i++) {
            //获取随机数
            mRandom = Math.random();
            //当前矩形的高度,通过随机数决定
            float currentHeight = (float) (mRectHeight * mRandom);
            //开始绘制
            canvas.drawRect(
                    (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight,
                    (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint);
        }
        //300毫秒后通知onDraw进行View重绘
        postInvalidateDelayed(300);

主要的代码就是这些了,其实一直是这些代码在起作用了,然后我们的线性渐变体现在哪里呢,我们都知道,一个矩形上,若是只有一种颜色,那就太单调了,所以线性渐变就是让这个自定义View更美观些吧,代码也不多,注释都有写哦~嘿嘿,大家去看注释吧,哈哈!

七、自定义ViewGroup


ViewGroup在前文中,我们已经说过了,它就相当于一个大人,管着下面的一群小孩,那么大人管小孩,那是吃喝拉撒都管的,那么ViewGroup相对于子View的话,也亦如此,大小、位置、监听,这是最基本的了,那么相对应的我们要重写的方法,也就是onMeasure()测量,onLayout()位置,onTouchEvent()响应事件,这里我们实现一个类似系统ScrollView的效果,首先,我们测量:

    //测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取子View的数量
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View childView = getChildAt(i);
            //测量子View
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

接下来我们要对子View的位置进行计算放置,首先我们应该保证每一个View放置的时候都是全屏,这样我们在滑动的时候,可以比较好地实现后面的效果,我们这样来设置ViewGroup的高度:

    //位置
    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int childCount = getChildCount();
        //布局参数
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        //设置ViewGroup高度
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        for (int j = 0; j < childCount; j++) {
            View child = getChildAt(i);
            //如果子View是可显示状态
            if (child.getVisibility() != View.GONE) {
                //放置子View
                child.layout(1, i * mScreenHeight, i2, (i + 1) * mScreenHeight);
            }
        }
    }

若想完整的显示子View的内容,那么管理子View的ViewGroup就必须>=所有的子View的高度之和!另一个就是,空间不能浪费,毕竟我们的手机屏幕就那么大,所以在ViewGroup管理下的子View只有是显示状态的给分配位置,其他的不是显示状态的则不分配位置!


在代码中主要还是修改每个View的top和bottom属性,可以让它们有序的排列下来,然后我们就可以滑动它们啦,代码如下:

    //滑动
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                int dy = mLastY - y;
                if (getScrollY() < 0) {
                    dy = 0;
                }
                if (getScrollY() > getHeight() - mScreenHeight) {
                    dy = 0;
                }
                scrollBy(0, dy);
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                if (dScrollY > 0) {
                    if (dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
                    }
                } else {
                    if (-dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
                    }
                }
                break;
        }
        postInvalidate();
        return true;
    }

其实别看代码这么多,但还是很好理解的,我们应该都知道,手在屏幕上划,有三个状态,从开始到结束,依次是:手指落下,手指移动,手指抬起。然后它们这些动作在onTouchEvent中就分别对应着:ACTION_DOWN,ACTION_MOVE,ACTION_UP;然后我们处理相应的逻辑就好了,就是当手指落下时做什么,手指移动时做什么,当手指离开了又该做什么!大家应该能看懂的哈,虽然代码多了些,但是,逻辑还是很好理解的!


最后别忘记加下面这个方法哈:

    //滚动
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
        }
    }

这个方法其实就计算滚动的,不难理解!嘿嘿…

八、事件拦截机制分析


这章讲的是一个事件拦截机制的一些基本概念,当Android系统扑捉到用户的各种输入事件之后,如何准确的传递给真正需要这个事件的控件呢?其实Android提供了一套非常完善的事件传递,处理机制,来帮助开发者完成准确的事件分配和处理。

要想了解拦截机制,我们首先要知道什么事触摸事件,一般MotionEvent提供的手势,我们常用的几个DOWN,MOVE,UP…

在MotionEvent中封装了很多东西,比如获取坐标点event.getX()和getRawX()获取。

但是我们这次讲的是事件拦截,那什么才是事件拦截,打个比方View和ViewGroup都要这个事件,这里就产生了事件拦截,其实就是一层层递减的意义,一般ViewGroup我们需要重写三个方法:

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

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

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

而对于我们的View就只需要两个了:

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

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

额,这概念还是有点儿繁琐的,毕竟刚学Android不久,所以这个…推荐大家看一篇博客http://blog.csdn.net/chunqiuwei/article/details/41084921,很详细的,我们用四张图来大致的了解一下哈(以下图片来自原上面的博主哈,尊重博主)

得到点击事件的log:

我们就可以很明确的知道他们的执行过程了:

还有两种情况,就是A拦截:

B拦截:

这里大致的简写了以下,因为我觉得上面那篇博客总结的比我好,我们通过前面的几种情况分析得到了大致的思想,在后面的学习,我们应该结合源码,你才会有更深的认识,如果是初学者,还是不建议看源码,会头大的。

奥~,第三章写完了,说实话,从第三章学了不少东西呢,首先我知道一个自定义View应该经过哪些步骤才能实现,希望以后真遇到自定义View的情况,不会着急吧,为以后找工作,还有工作做准备,打基础,虽然现在Android工作很难找了,但是,也不能气馁嘛,总不能因为找不到工作,市场的饱和就放弃自己喜欢的Android啊,对吧。

本篇博客有借鉴程序员刘某人的群英传笔记,嘿嘿,尤其是最后这一节,其实就是照抄了,嘿嘿。在这里推荐下我们伟大的群主几个初级的自定义的View哈,链接如下:

Android绘图机制(一)——自定义View的基础属性和方法
Android绘图机制(二) - 自定义查看绘制形,圆形,三角形,扇形,椭圆形,曲线,文字和图片的坐标
Android绘图机制(三)——自定义View的实现方式以及半弧圆新控件

好了,今天的任务圆满完成了,可以去睡觉了…嘿嘿

大家若是有什么不懂的,可以在下面评论区中留言哈,我看到后会回的,另外对android有兴趣的同学可以加我们程序员刘某人的群:555974449,群里面有很多大神的,而且很热情,很热心的,大家不懂的可以问的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值