第三章Android 控件架构与 自定义控件详解(Android群英传)

1、Android控件架构
2、View的测量与绘制
3、ViewGroup的测量与绘制
4、自定义控件的三种方式
5、事件的拦截机制

1、Android控件架构

1、控件分为2类:View控件和ViewGroup控件
2、上层控件负责下层控件的测量与绘制,并传递交互事件
3、每棵树的顶部都有一个ViewParent对象,这是控制核心,负责交互管理事件的调度和分配
4、每个Activity都包含一个Window对象,Window对象通常由PhoneWindow来实现
这里写图片描述

DecorView是根View,它把内容显示在PhoneWindow上面,
DecorView监听所有的事件并通过WindowManagerService来进行接收
DecorView通过Activity回调onClick事件
DecorView包含TitleView和ContentView
ContentView是一个ID为content的FramLayout

DecorView的里面是一个垂直的LinearLayout,作为ViewGroup,上面title,下面content
所以requestWindowFeature(Window.FEATURE_NO_TITLE);只能在setContentView之前调用才能生效
(requestWindowFeature实际上是调用的PhoneWindow的requestFeature方法,
如果你把requestWindowFeature放在setContentView之后,我们来看
public boolean requestFeature(int featureId) {
if (mContentParent != null) {
throw new AndroidRuntimeException(“requestFeature() must be called before adding content”);
}
那么mContentParent就有值了,就会抛出异常了)
5、在代码中,onCreate调用setContentView之后,ActivityManagerService会回调onResume方法,最终显示出来

2、View的测量与绘制

2.1、View的测量
1、Android就像那么蒙着眼睛画画的人,你必须精确的告诉它如何去画
2、我们在画一个图形之前,必须知道它的大小和位置,这就是测量
3、Android在onMeasure方法中进行测量,Android为我们提供了一个MeaSureSpec类来进行测量
4、MeaSureSpec是一个32位的int值,高2位为测量的模式,低30位为测量的大小
5、在计算中用位运算的原因是提高运算效率
6、测量模式有三种:

6.1、EXACTLY :精准值模式
当我们将控件设置为具体值时,比如android:layout_width = “100dp”或者为match_parent属性时,系统使用的是EXACTLY模式
6.2、AT MOST:最大值模式
当控件的width或者height属性为wrap_content,控件会随着内容的变化而变化,此时控件的尺寸只要不超过父控件指定的最大尺寸即可
6.3UNSPECIFIED:不指定测量模式,view想多大就多大,通常自定义view的时候使用
View默认的onMeasure方法只支持EXACTLY模式,所以自定义view的时候想要让控件支持warp_cotent属性,要重写onMeasure方法来指定warp_contetn的大小
7、onMeasure方法最终调用的是View的setMeasuredDimension方法,我们要做的就是把测量后的宽高设置给它
完整的代码如下:

package com.zx.heros.chapter3;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by Administrator on 2017/2/8.
 */
public class TeachingView extends View {

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec,
                             int heightMeasureSpec) {
        setMeasuredDimension(
                measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    private int measureWidth(int measureSpec) {
        //从MeasureSpec提取出具体的测量模式和大小
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        //如果是EXACTLY模式,直接使用指定的大小
        //如果是其他模式,那么取我们指定值和specSize的最小值
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(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;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);
        int width = getWidth();
        int height = getHeight();
        Log.d("xys", "width : " + width + " height : " + height);
    }
}
那么你在布局文件中使用这个view的时候:
1、<com.zx.heros.chapter3.TeachingView
    android:layout_width="100dp"
    android:layout_height="100dp"
     />
设置固定值,就是设置的大小
2、<com.zx.heros.chapter3.TeachingView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
     />

设置wrap_content,则会使用你设置的默认大小200
这就是View的测量

2.2、View的绘制

1、测量一个view之后,我们就可以重写onDraw方法进行绘制了
2、在onDraw方法中,提供的有Canvas对象,我们可以直接用它进行绘制
3、其他地方想绘制可以通过Canvas can = new Canvas(bitmap);

3、ViewGroup的测量与绘制

3.1、ViewGroup的测量
1、ViewGroup会管理子View,当ViewGroup大小设置为wrap_cotent时,会根据子view的大小决定自己的大小,其他模式下(match_parent,固定值)根据指定值设置大小
2、子view测量完毕后,ViewGroup会通过调用子view的Layout方法决定布局位置
3、在自定义ViewGroup时会重写onLayout方法来控制子View显示的位置,如果需要支持wrap_content属性,那么也需要重写onMeasure方法给它一个默认result
3.2、ViewGroup的绘制
ViewGroup不需要绘制,但是它会调用dispatchDraw方法来绘制子view,过程是遍历子view,调用子view的绘制方法

4、自定义View

1、原生的view还存在不少bug,更不要提我们自定义的view了
2、了解系统自定义view的过程,帮助我们了解系统的绘图机制,适当情况下,可以通过自定义view创建灵活的布局
3、一般使用onDraw绘制view的显示内容,如果自定义view需要wrap_content属性,那就必须重写onMeasure方法了,通过自定义attr属性,可以自定义新的属性
自定义View几个重要的回调方法:
onFinishInflate :从xml中加载组件后回调
onSizeChanged : 组件大小改变时回调
onMeasure :回调该方法来进行测量
onLayout :回调该方法来确定显示的位置
onTouchEvent : 监听到触摸事件时回调
自定义view时可以选择性回调方法
自定义view通常使用的方法:
1、对现有控件进行扩展
一般情况下,我们在onDraw方法中对原生控件进行扩展
比如:

package com.zx.heros.chapter3;

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

public class MyTextView extends TextView {

    private Paint mPaint1, mPaint2;

    public MyTextView(Context context) {
        super(context);
        initView();
    }

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

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

    private void initView() {
        mPaint1 = new Paint();
        mPaint1.setColor(getResources().getColor(
                android.R.color.holo_blue_light));
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint2 = new Paint();
        mPaint2.setColor(Color.YELLOW);
        mPaint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制外层矩形
        canvas.drawRect(
                0,
                0,
                getMeasuredWidth(),
                getMeasuredHeight(),
                mPaint1);
        // 绘制内层矩形
        canvas.drawRect(
                10,
                10,
                getMeasuredWidth() - 10,
                getMeasuredHeight() - 10,
                mPaint2);
        canvas.save();
        // 绘制文字前平移10像素
        canvas.translate(10, 0);
        // 父类完成的方法,即绘制文本
        super.onDraw(canvas);
        canvas.restore();
    }
}

还有复杂一点的,可以通过LinearGradient Shader 和Matrix来实现,首先在onSizeChange方法中进行初始化工作,根据view的宽度,设置一个LinearGradient 渐变渲染器,然后在onDraw方法中通过矩阵的方式不断平移渐变效果
如下:

package com.zx.heros.chapter3;

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;

public class ShineTextView extends TextView {

    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;
    private Paint mPaint;
    private int mViewWidth = 0;
    private int mTranslate = 0;

    public ShineTextView(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) {
            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);
                mGradientMatrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mGradientMatrix != null) {
            mTranslate += mViewWidth / 5;
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            postInvalidateDelayed(100);
        }
    }
}

2、通过组合来实现新的控件

2.1、定义属性,在res-values创建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>

然后在自定义view的构造方法中一一获取,获取到之后再设置给相应的组件(组件一个从xml里面引入,也可以new一个新的组件,然后设置宽高);这样的话,你在布局中使用这个自定义view的时候就可以使用attrs.xml里面的TopBar的属性了
下面是完整代码:

package com.imooc.systemwidget;

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.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class TopBar extends RelativeLayout {

    // 包含topbar上的元素:左按钮、右按钮、标题
    private Button mLeftButton, mRightButton;
    private TextView mTitleView;

    // 布局属性,用来控制组件元素在ViewGroup中的位置
    private LayoutParams mLeftParams, mTitlepParams, mRightParams;

    // 左按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mLeftTextColor;
    private Drawable mLeftBackground;
    private String mLeftText;
    // 右按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mRightTextColor;
    private Drawable mRightBackground;
    private String mRightText;
    // 标题的属性值,即我们在atts.xml文件中定义的属性
    private float mTitleTextSize;
    private int mTitleTextColor;
    private String mTitle;

    // 映射传入的接口对象
    private topbarClickListener mListener;

    public TopBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

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

    public TopBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 设置topbar的背景
        setBackgroundColor(0xFFF59563);
        // 通过这个方法,将你在atts.xml中定义的declare-styleable
        // 的所有属性的值存储到TypedArray中
        TypedArray ta = context.obtainStyledAttributes(attrs,
                R.styleable.TopBar);
        // 从TypedArray中取出对应的值来为要设置的属性赋值
        mLeftTextColor = ta.getColor(
                R.styleable.TopBar_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(
                R.styleable.TopBar_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBar_leftText);

        mRightTextColor = ta.getColor(
                R.styleable.TopBar_rightTextColor, 0);
        mRightBackground = ta.getDrawable(
                R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);

        mTitleTextSize = ta.getDimension(
                R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(
                R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title);

        // 获取完TypedArray的值后,一般要调用
        // recyle方法来避免重新创建的时候的错误
        ta.recycle();

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

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

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

        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleTextColor);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setGravity(Gravity.CENTER);

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

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

        mTitlepParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);

        // 按钮的点击事件,不需要具体的实现,
        // 只需调用接口的方法,回调的时候,会有具体的实现
        mRightButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                mListener.rightClick();
            }
        });

        mLeftButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                mListener.leftClick();
            }
        });
    }

    // 暴露一个方法给调用者来注册接口回调
    // 通过接口来获得回调者对接口方法的实现
    public void setOnTopbarClickListener(topbarClickListener mListener) {
        this.mListener = mListener;
    }

    /**
     * 设置按钮的显示与否 通过id区分按钮,flag区分是否显示
     *
     * @param id   id
     * @param flag 是否显示
     */
    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.GONE);
            } else {
                mRightButton.setVisibility(View.GONE);
            }
        }
    }

    // 接口对象,实现回调机制,在回调方法中
    // 通过映射的接口对象调用接口中的方法
    // 而不用去考虑如何实现,具体的实现由调用者去创建
    public interface topbarClickListener {
        // 左按钮点击事件
        void leftClick();
        // 右按钮点击事件
        void rightClick();
    }
}

3、重写view来实现新的控件

3.1、难点在于绘制控件和实现交互,通常需要继承View类,并重写onDraw onMeasure方法实现绘制逻辑
3.2、我们来实现一个弧线比例展示图
这里写图片描述

这个自定义View分为3个部分,中间的圆形,中间的文字,和外圈的弧线,有了这样的思路,在onDraw中一个一个的去绘制就可以了
1、初始化的时候设置好绘制3种图形的参数
初始化内圆半径和周长;绘制弧线,需要指定其椭圆的外接

package com.imooc.systemwidget;

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

public class CircleProgressView extends View {

    private int mMeasureHeigth;
    private int mMeasureWidth;

    private Paint mCirclePaint;
    private float mCircleXY;
    private float mRadius;

    private Paint mArcPaint;
    private RectF mArcRectF;
    private float mSweepAngle;
    private float mSweepValue = 66;

    private Paint mTextPaint;
    private String mShowText;
    private float mShowTextSize;

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec,
                             int heightMeasureSpec) {
        mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
        mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
        initView();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制圆
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
        // 绘制弧线
        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
        // 绘制文字
        canvas.drawText(mShowText, 0, mShowText.length(),
                mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
    }

    private void initView() {
        //初始化的时候设置好三种图像的参数
        float length = 0;
        if (mMeasureHeigth >= mMeasureWidth) {
            length = mMeasureWidth;
        } else {
            length = mMeasureHeigth;
        }

        mCircleXY = length / 2;
        mRadius = (float) (length * 0.5 / 2);
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(getResources().getColor(
                android.R.color.holo_blue_bright));

        mArcRectF = new RectF(
                (float) (length * 0.1),
                (float) (length * 0.1),
                (float) (length * 0.9),
                (float) (length * 0.9));
        mSweepAngle = (mSweepValue / 100f) * 360f;
        mArcPaint = new Paint();
        mArcPaint.setAntiAlias(true);
        mArcPaint.setColor(getResources().getColor(
                android.R.color.holo_blue_bright));
        mArcPaint.setStrokeWidth((float) (length * 0.1));
        mArcPaint.setStyle(Style.STROKE);

        mShowText = setShowText();
        mShowTextSize = setShowTextSize();
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mShowTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    private float setShowTextSize() {
        this.invalidate();
        return 50;
    }

    private String setShowText() {
        this.invalidate();
        return "Android Skill";
    }

    public void forceInvalidate() {
        this.invalidate();
    }

    public void setSweepValue(float sweepValue) {
        if (sweepValue != 0) {
            mSweepValue = sweepValue;
        } else {
            mSweepValue = 25;
        }
        this.invalidate();
    }
}

无论多么复杂的图形,控件,都是由最基本的图形绘制出来的,当你的脑海中有设计图之后,剩下的就是对坐标的计算了。
2、假如要实现这个图形
这里写图片描述
1、绘制一个一个的矩形
2、每个矩形之间稍微偏移一点距离即可
3、让这些矩形的高度进行随机的变化,通过Math.random()
4、在onDraw方法中调用invalidate方法通知view进行重绘,不过这里不需要每次一绘制完新的矩形就通知view进行重绘,这样会因为刷新速度太快影响效果,所以我们使用延迟重绘postinvalidateDelayed(300),每隔300秒进行一次重绘。
5、如果想更加逼真,可以在绘制矩形的时候给Paint增加一个LinearGradient渐变效果。在onSizeChange中增加。
完整代码如下:

package com.imooc.systemwidget;

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;

public class VolumeView extends 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 VolumeView(Context context) {
        super(context);
        initView();
    }

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

    public VolumeView(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();
        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);
    }

    @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);
        }
        postInvalidateDelayed(300);
    }
}

这个例子告诉我们:创建自定义view要一步一步来,从基本效果开始慢慢增加功能,不论多么复杂的自定义view ,它一定是慢慢迭代起来的功能

5、自定义ViewGroup
1、自定义ViewGroup的目的是对子view进行管理,对子view添加显示、响应的规则
2、因此自定义ViewGroup需要重写onMeasure方法对子view进行测量;重写onLayout确定子view的位置,重写onTouchEvent增加响应事件
下面我们来自定义一个类似ScrollView来实现回弹的一个效果
1、让自定义ViewGroup先实现ScrollView的功能
2、实现黏性效果
具体代码如下:

package com.imooc.systemwidget;

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;

public class MyScrollView extends ViewGroup {

    private int mScreenHeight;
    private Scroller mScroller;
    private int mLastY;
    private int mStart;
    private int mEnd;

    public MyScrollView(Context context) {
        super(context);
        initView(context);
    }

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

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

    private void initView(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(
                Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        mScreenHeight = dm.heightPixels;
        mScroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean changed,
                            int l, int t, int r, int b) {
        int childCount = getChildCount();
        // 2、设置ViewGroup的高度:即子View的个数乘以屏幕的高度
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        //3、通过遍历来设定每个子View需要放置的位置,直接通过子view的layout方法,
        //并将具体的位置通过参数传递进去即可
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                child.layout(l, i * mScreenHeight,
                        r, (i + 1) * mScreenHeight);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec,
                             int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //1、放置ViewGroup的子view,使用遍历的方式来通知子View进行测量
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View childView = getChildAt(i);
            measureChild(childView,
                    widthMeasureSpec, heightMeasureSpec);
        }
    }

    @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:
                //4、只要使用scrollby(0,dy)方法,让手指滑动的时候,让ViewGroup的所有子view也跟着滚动dy即可
                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:
                //增加ViewGroup的黏性效果,手指离开后ViewGroup的黏性效果,很自然的想到onTouchEvent的ACTION_UP事件和Scroller
                //在ACTION_UP事件中判断手指滑动的距离,如果超过一定距离,则使用Scroller类来平滑移动到下一个view;如果
                //小于一定距离,则回滚到原来的位置
                //记录触摸终点
                int dScrollY = checkAlignment();
                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;
    }
   private int checkAlignment() {
        int mEnd = getScrollY();
        boolean isUp = ((mEnd - mStart) > 0) ? true : false;
        int lastPrev = mEnd % mScreenHeight;
        int lastNext = mScreenHeight - lastPrev;
        if (isUp) {
            //向上的
            return lastPrev;
        } else {
            return -lastNext;
        }
    }
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }
}

通过以上操作,我们就能在onTouchEvent中实现滚动的逻辑和黏性的逻辑

6、事件拦截机制分析

当Android系统捕获到用户的各种输入事件后,如何传递给需要这个事件的控件呢?
下面我们就来看看事件拦截机制分析
1、想要了解触摸事件的拦截机制,首先要了解什么事触摸事件?
比如按钮按下:按下一个事件,不小心滑动一点事件二,抬起事件三
2、Android为触摸事件封装了一个类MotionEvent,如果重写OnTouchEvent或者跟触摸相关的方法,它的参数就是MotionEvent。所以要牢记这个类
3、MotionEvent封装了不少好东西:1、触摸点的坐标event.getX()2、事件类型down、up、move
由此看来触摸事件还是比较简单的,动作类型+坐标而已
那么问题来了,viewGroup嵌套ViewGroup嵌套view,触摸事件就一个,该分配给谁呢?所以就有了事件拦截
举个栗子:

总经理:最外层的ViewGroup
部长:中间的ViewGroup
干活的你:MyView,最底层的view
1、总经理布置任务给部长
2、部长布置任务给你
3、你把任务完成交给了部长的任务,部长签字
4、部长交给总经理,总经理签字
这样一个任务就完成了,事件传递机制也是这样
对于ViewGroup来说:
重写:dispatchTouchEvent(派遣)、onInterceptTouchEvent(拦截)、onTouchEvent(触摸)
对于View来说:
重写:dispatchTouchEvent、onTouchEvent
可以看出ViewGroup级别比较高,比view多了一个方法:事件拦截的核心方法onInterceptTouchEvent

正常情况下:
事件传递的顺序是:总经理(外层ViewGroup)–>部长(中间ViewGroup)–>你(最底层的View)
事件传递的时候,先dispatchTouchEvent再onInterceptTouchEvent
事件处理的顺序是:你(最底层的View)–>部长(中间ViewGroup)–>总经理(外层ViewGroup)

事件处理的时候,都是执行onTouchEvent方法
事件传递的返回值很好理解:True:拦截,不继续 false不拦截,继续流程
事件处理的返回值也是:True:处理了,不用审核了,False:给上级处理
初始情况下返回值都是false
为了好理解事件拦截的过程,我们只关心onInterceptTouchEvent

下面我们继续上面的案例说明:
情况1、如果总经理觉得这个任务太简单了,自己可以完成,没必要找下属,那么事件就被总经理拦截了。
即:(外层ViewGroup的onInterceptTouchEvent返回true)
情况2、如果部长觉得没必要让下属解决,自己就解决了,那么事件就被部长拦截了。
即:(中间层的ViewGroup的onInterceptTouchEvent返回true)

对于事件的分发,拦截,大家都清楚了吧,下面我们来看看事件的处理
正常情况下你处理完事件要向上级汇报,需要上级确认,所以你就返回了false
情况1、如果你罢工不干了,那么你就不用报告上级了,就直接返回True
即:你(最底层的view)的onTouchEvent返回了true
情况2、你汇报的报告部长觉得太丢人,不敢给总经理看,就偷偷的返回的true,整个事件结束
即:部长(中间的viewGroup)的onTouchEvent返回了true

总结:从大到小派发事件,从小到大处理事件,初学者在学习的时候,最好先对流程有个大致认识之后,再去接触源码,这样就不会一头雾水,从而丧失学习的兴趣。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值