总结-自定义View

来源:互联网 发布:癌症临床试验数据研究 编辑:程序博客网 时间:2024/06/01 07:26

接触android已经挺长时间了,却不是很习惯总结东西,总觉得网上已经有了,自己没必要再去写一些重复的难容,高深的自己都没搞明白也没法写。=一直觉得自己的基础掌握的不牢靠,很多细节性的东西慢慢就忘了,很是发愁。因此还是打算总结一些东西,方便以后查看。先从自定义View开始。

1. View的生命周期

一直以来自定义View在我心里就两个步骤。
1.继承View
2.实现其中的方法
从来没有想过它的生命周期,直到。。。看到了一份面试题,让写出View的声明周期,然后才猛然醒悟,原来View也是有生命周期的啊!不只是原来硬记下来的onMeasure(),onDraw()等..

生命周期顾名思义就是从有到无的过程,我们自定义一个LifeCircleView继承自View,重写其中的一些常用方法,观察一下它的执行流程

package com.hank.ok.view;import android.content.Context;import android.graphics.Canvas;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.util.Log;import android.view.View;/** * 类功能描述: * version:${version} */public class LifeCircleView extends View {    public LifeCircleView(Context context) {        this(context,null);    }    public LifeCircleView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        Log.i("LifeCircleView","--------->构造方法");    }    @Override    protected void onFinishInflate() {        Log.i("LifeCircleView","--------->onFinishInflate");        super.onFinishInflate();    }    @Override    protected void onAttachedToWindow() {        Log.i("LifeCircleView","--------->onAttachedToWindow");        super.onAttachedToWindow();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        Log.i("LifeCircleView","--------->onMeasure");        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        Log.i("LifeCircleView","--------->onSizeChanged");        super.onSizeChanged(w, h, oldw, oldh);    }    @Override    protected void onDraw(Canvas canvas) {        Log.i("LifeCircleView","--------->onDraw");        super.onDraw(canvas);    }    @Override    protected void onDetachedFromWindow() {        Log.i("LifeCircleView","--------->onDetachedFromWindow");        super.onDetachedFromWindow();    }}

打印的结果如下:

I/LifeCircleView: --------->构造方法I/LifeCircleView: --------->onFinishInflateI/LifeCircleView: --------->onAttachedToWindowI/LifeCircleView: --------->onMeasureI/LifeCircleView: --------->onMeasureI/LifeCircleView: --------->onSizeChangedI/LifeCircleView: --------->onDrawI/LifeCircleView: --------->onMeasureI/LifeCircleView: --------->onDraw

如果此时用户按了返回键,就会执行onDetachedFromWindow方法
所以从打印的结果我们很容易得出View的执行流程

构造方法->onFinishInflate->onAttachedToWindow->onMeasure->onSizeChange->onDraw->onDetachedFromWindow

所以也就明白了为什么可以在onSizeChanged()方法中获取到View的宽和高,因为这个执行已经测量过了。

2. View的坐标系

如果对坐标系都搞不清楚,就很难进行自定义View 了,所以要先学习View的坐标系,网上也有很多介绍这些内容的文章比如
视图坐标系,

该文章中这张图一目了然的说明了哪些方法是相对于父布局的,哪些是相对于屏幕的,一定要搞清楚啊!!!

image

3. 自定义View的流程

不管是从网上还是书本上学习自定义View,有一点一定会让人印象深刻。一直在说自定义View一定要实现XXX方法。没错,onDraw方法是必须要实现的,不然界面就是一片空白,主要的绘制逻辑就是在onDraw()里完成的,而onMeasure,可以不实现,大不了使用Match_parent属性值或者明确指定View 的大小。onMeasure更重要的是用来设置当我们的属性值使用wrap_content时,View该怎么显示?
所以自定义View第一步还是要重写onMeasure,onDraw方法的。

下面自定义的一个简单的进度条:
这里写图片描述

上边的那个是我们自定义的效果,下边的那个是系统自带的Seekbar

思路:
-只需默认给出进度条的高度,View的高度在onMeasure()方法中根据进度条的高度进行计算
-需要声明4只画笔,一个画进度条背景,一个画当前进度,一个画滑块,还有一个写文字。
-在onMeasure()中设置View在不同模式下的大小。
-在onSizeChanged中根据View的宽高,计算滑块的宽度为高度的4/3,要明白滑块的高度和View的高度是一致的,所以这里直接使用了mBlockWidth=h*4/3
-在onDraw()中进行分别进行绘制。

package com.hank.ok.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Point;import android.support.annotation.Nullable;import android.text.TextPaint;import android.util.AttributeSet;import android.view.View;import android.view.WindowManager;/** * 类功能描述: * version:${version} */public class HorizontalProgress extends View {    private int mScreenWidth;//屏幕宽度    private static final int mProgressHeight = 16;//进度条高度    private Paint mPaintBg;    private Paint mPaintCurrent;    private Paint mPaintBlock;    private TextPaint mPaintText;    private int mBlockWidth, mProgressWidth;    private int mCurrentProgress = 0;//当前进度    public HorizontalProgress(Context context) {        super(context);        init();    }    public HorizontalProgress(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        init();    }    private void init() {        mScreenWidth = getScreenSize().x;        mPaintBg = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        mPaintBg.setStyle(Paint.Style.FILL);        mPaintBg.setStrokeWidth(mProgressHeight);        mPaintBg.setStrokeJoin(Paint.Join.ROUND);        mPaintBg.setColor(Color.GRAY);        mPaintCurrent = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        mPaintCurrent.setStyle(Paint.Style.FILL);        mPaintCurrent.setStrokeWidth(mProgressHeight);        mPaintCurrent.setStrokeJoin(Paint.Join.ROUND);        mPaintCurrent.setColor(Color.BLUE);        mPaintText = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        mPaintText.setTextSize(30);        mPaintText.setColor(Color.WHITE);        mPaintBlock = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);        mPaintBlock.setStyle(Paint.Style.FILL);        mPaintBlock.setStrokeWidth(10);        mPaintBlock.setStrokeJoin(Paint.Join.ROUND);        mPaintBlock.setColor(Color.RED);        setLayerType(LAYER_TYPE_HARDWARE, mPaintBlock);    }    /**     * 计算屏幕宽高,存放在Point中     *     * @return Point 含有屏幕宽高信息     */    private Point getScreenSize() {        Point point = new Point();        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);        wm.getDefaultDisplay().getSize(point);        return point;    }    /**     * 在此计算滑块的宽度以及进度条的最大宽度     * <p>     * 该方法是会多次调用的     *     * @param w    View的宽度     * @param h    View的高度     * @param oldw     * @param oldh     */    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        mBlockWidth = h * 4 / 3;//计算滑块的宽度,高度和View高度一致        mProgressWidth = w - mBlockWidth;//进度条的最大宽度=View的宽度-滑块的宽度    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getMode(heightMeasureSpec);        int heightResult = 0;        if (heightMode == MeasureSpec.EXACTLY) {            heightResult = heightSize;        } else {            //如果为没有明确指定View的高度并且使用的模式不是EXACTLY,为设置一个默认的高度            heightResult = mProgressHeight * 5;        }        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int widthResult = 0;        if (widthMode == MeasureSpec.EXACTLY) {            widthResult = widthSize;        } else {            //如果为没有明确指定View的宽度并且使用的模式不是EXACTLY,就设置View的宽度为屏幕宽度            widthResult = mScreenWidth;        }        setMeasuredDimension(widthResult, heightResult);    }    /**     * 入口     *     * @param progress 当前进度值     */    public void setProgress(int progress) {        mCurrentProgress = progress;        invalidate();    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        drawBackground(canvas);        drawProgress(canvas);        drawBlock(canvas);        drawText(canvas);    }    /**     * 因为文字一直都处于滑块的中间显示,所以文字的位置可以根据滑块的位置来确定     *     * @param canvas     */    private void drawText(Canvas canvas) {        float centerX = mBlockWidth / 2 + mCurrentProgress * 1.0f / 100 * mProgressWidth;//滑块中心点x坐标        String text = mCurrentProgress + "%";        float textWidth = mPaintText.measureText(text);        float startX = centerX - textWidth / 2;        float baseline = getHeight() * 2 / 3;        canvas.drawText(text, startX, baseline, mPaintText);    }    /**     * 绘制滑块,只需要关注滑块的中心点坐标,即等于当前进度的坐标     *     * @param canvas     */    private void drawBlock(Canvas canvas) {        float centerX = mBlockWidth / 2 + mCurrentProgress * 1.0f / 100 * mProgressWidth;        float left = centerX - mBlockWidth / 2;        float top = 0;        float right = centerX + mBlockWidth / 2;        float bottom = getBottom();        canvas.drawRect(left, top, right, bottom, mPaintBlock);    }    private void drawProgress(Canvas canvas) {        float left = mBlockWidth / 2;        float right = mCurrentProgress * 1.0f / 100 * mProgressWidth;        float top = getHeight() / 2 - mProgressHeight / 2;        float bottom = getHeight() / 2 + mProgressHeight / 2;        canvas.drawRect(left, top, right, bottom, mPaintCurrent);    }    /**     * 进度条背景的长度=View的宽度-滑块的宽度     * <p>     * 起始X坐标:滑块宽度/2     * 终点X坐标=View的宽度-滑块宽度/2     * <p>     * Y坐标一直垂直居中,也就是getHeigth()/2-进度条的高度/2     *     * @param canvas     */    private void drawBackground(Canvas canvas) {        float left = mBlockWidth / 2;        float right = getWidth() - mBlockWidth / 2;        float top = getHeight() / 2 - mProgressHeight / 2;        float bottom = getHeight() / 2 + mProgressHeight / 2;        canvas.drawRect(left, top, right, bottom, mPaintBg);    }}

主要的难点就在于对滑块坐标的计算上了,
float centerX = mBlockWidth / 2 + mCurrentProgress * 1.0f / 100 * mProgressWidth;
这个公式用来计算滑块的中心点坐标,left,right啥的都根据这个值+-滑块的宽度/2进行计算的。这个公式也不费解,以为滑块的默认起始位置是从View的最左边开始的,他的中心坐标的起始位置就是mBlockWidth/2,之后移动的时候,再加上移动的距离就可以了