总结-自定义View

接触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: --------->onFinishInflate
I/LifeCircleView: --------->onAttachedToWindow
I/LifeCircleView: --------->onMeasure
I/LifeCircleView: --------->onMeasure
I/LifeCircleView: --------->onSizeChanged
I/LifeCircleView: --------->onDraw
I/LifeCircleView: --------->onMeasure
I/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,之后移动的时候,再加上移动的距离就可以了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值