博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 高级自定义View实战
阅读量:5969 次
发布时间:2019-06-19

本文共 19973 字,大约阅读时间需要 66 分钟。

hot3.png

     在android组件中主要分为两种:容器(LinearLayout....)和子View(TextView......),但是这些现有的组件往往不能满足app的开发。比如实现一个流式标签,炫酷的进度条显示呢。都需要我们探寻源码,分析和改造成我们想要的效果。这次的主题先从View的自定义入手。

165540_Fapb_2978666.png 

概述:

    针对View的自定义主要从 onMeasure()和onDraw()这两个方法入手。

    onMeasure():测量View的大小
        测量view只要针对我们在xml中wrap_content和match_parent的两种属性。而在onMeasure()对      应的字段是AT_MOST 和EXACTLY。Android默认的实现了EXACTLY的测量也就是精准测量(对应xml属性    match_parent和具体值),对于AT_MOST默认填充父容器,如果要实现包裹那就需要我们自己动手丰衣      足食了。

    onDraw():绘制内容,比如形状,图片啊都在这里实现。

        主要用到类有paint(画笔),canvas(画板)由这两个类,我们就可以随心所欲的画画l咯。

实践出真理:

    “纸上得来终觉浅,绝知此事要躬行“,现在我们就一步一步的用代码来分析自定义View的实践。想了好久我要用啥一个相对简单但大家都熟悉的view作为本文的的入门demo呢?最后决定我们来实现android自带的TextView。

入门demo:

创建一个class类,名为MyTextView  继承View。

package huangzhibo.com.learndemo.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.view.View;import huangzhibo.com.learndemo.R;/** * Created by HuangZhiBo on 2017/8/20/020. */public class MyTextView extends View {    private String text;    private int paintColor = Color.BLACK;    public MyTextView(Context context) {        super(context);    }    public MyTextView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        /**实现AT_MOST的测量*/    }    public void setText(String info) {        text = info;    }    public void setTextColor(int color) {        paintColor = color;    }    @Override    protected void onDraw(Canvas canvas) {        if (text == null) {  //文本为空            return;        }        Paint paint = new Paint();        paint.setColor(paintColor);        paint.setAntiAlias(true); //抗锯齿        paint.setTextSize(30);  //文本大小        canvas.drawText(text,this.getWidth()/2-getTextWidth(paint,text)/2,this.getHeight()/2,paint);    }    /**     * 测量文字宽度     * @param paint     * @param str     * @return     */    public static int getTextWidth(Paint paint, String str) {        int w= 0;        if (str != null && str.length() > 0) {            int len = str.length();            float[] widths = new float[len];            paint.getTextWidths(str, widths);            for (int j = 0; j < len; j++) {                w+= (int) Math.ceil(widths[j]);            }        }        return w;    }}

xml布局

代码都有相应的注释,这里就不在废话,这里对于onMeasure并没有处理,采用默认。注意看layout_width和layout_height的属性值为wrap_content。上文我说道,如果对于onMearsure并未处理,那么view将填充父容器。为了看清楚边界,view设置了一个背景。结果不出所料,那么接下我们就来实现Android对于wrap_content的测量。

效果图:

 

在对于onMeasure的AT_MOST,我查了下网上很多资料都是直接给个固定值,然后通过比较返回最大值,这样根本就是治标不治本,我们要的效果是完全的TextView效果。那么问题来了,要实现包裹效果,也就需要确定宽和高。而这个宽和高就是文本的宽和高加上内边距(pading)。思路有了那就好办了。

package huangzhibo.com.learndemo.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.view.View;import huangzhibo.com.learndemo.R;import static android.os.Build.VERSION_CODES.M;/** * Created by HuangZhiBo on 2017/8/20/020. */public class MyTextView extends View {    private String text;    private int paintColor = Color.BLACK;    private Paint mPaint;    public MyTextView(Context context) {        super(context);    }    public MyTextView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        mPaint=new Paint();        mPaint.setColor(paintColor);        mPaint.setAntiAlias(true); //抗锯齿        mPaint.setTextSize(30);  //文本大小    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int result = 200;        /**实现AT_MOST的测量*/       setMeasuredDimension(meaSureWidth(widthMeasureSpec),meaSureHeight(heightMeasureSpec));    }    /**     * 测量宽度     * @param measureSpec     * @return     */    private int meaSureWidth(int measureSpec){        int result=0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if (specMode==MeasureSpec.AT_MOST){            /**文本宽度+左右内边距*/            int v = (int)mPaint.measureText(text) + getPaddingLeft() + getPaddingRight();            result= Math.min(v, specSize);        }        return result;    }    /**     * 测量宽度     * @param measureSpec     * @return     */    private int meaSureHeight(int measureSpec){        int result=0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if (specMode==MeasureSpec.AT_MOST){            /**文本高度+上下内边距*/            int v = (int) (-mPaint.ascent() + mPaint.descent())  + getPaddingTop() + getPaddingBottom();            result= Math.min(v, specSize);        }        return result;    }    public void setText(String info) {        text = info;    }    public void setTextColor(int color) {        paintColor = color;    }    @Override    protected void onDraw(Canvas canvas) {        if (text == null) {  //文本为空            return;        }        /**注意 drawText中的x,y分别指的是文字的左边位置,文字baseLine的位置*/        canvas.drawText(text,getPaddingLeft(),this.this.getHeight()-mPaint.descent()-getPaddingBottom(),mPaint);    }    /**     * 测量文字宽度     * @param paint     * @param str     * @return     */    public static int getTextWidth(Paint paint, String str) {        int w= 0;        if (str != null && str.length() > 0) {            int len = str.length();            float[] widths = new float[len];            paint.getTextWidths(str, widths);            for (int j = 0; j < len; j++) {                w+= (int) Math.ceil(widths[j]);            }        }        return w;    }}

效果图:

到这里,我们已经完全解决了AT_MOST的测量了。不过对于以上的代码还需要解释下:

  • (-mPaint.ascent() + mPaint.descent()) 这个为啥能得到文字高度?原理可以看下这篇文章
    (这个必须掌握)

基本的自定义view,我相信到这里你已经差不多掌握了,那么如何达到炉火纯青,信手拈来的境界呢?没有捷径,就是熟能生巧,接下来实现几个效果View。

仿华为记步View和常见的验证码View:

                           

华为记步View代码如下:

package huangzhibo.com.learndemo.view;import android.animation.ValueAnimator;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.RectF;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.util.Log;import android.view.View;import huangzhibo.com.learndemo.R;/** * Created by HuangZhiBo on 2017/7/31/031. */public class StepView extends View {    /*圆弧宽度*/    private float borderWidth = 38f;    /* 画步数的数值的字体大小*/    private float numberTextSize = 0;    /**     * 开始绘制圆弧的角度     */    private float startAngle = 135;    /**     * 终点对应的角度和起始点对应的角度的夹角     */    private float angleLength = 270;    /**     * 所要绘制的当前步数的红色圆弧终点到起点的夹角     */    private float currentAngleLength = 0;    private String stepNumber;    /**     * 动画时长     */    private int animationLength = 3000;    public StepView(Context context) {        super(context);    }    public StepView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        /*中心点坐标*/        float centerX = (getWidth()) / 2;        /*指定圆弧的外轮廓矩形区域*/        RectF rectF = new RectF(0 + borderWidth, borderWidth, 2 * centerX - borderWidth, 2 * centerX - borderWidth);        /*绘制红色圆弧*/        drawArcYellow(canvas, rectF);        /*绘制蓝色走过步数*/        drawArcRed(canvas, rectF);        /*文字*/        drawTex(canvas, rectF);    }    private void drawTex(Canvas canvas, RectF rectF) {        Paint paint = new Paint();        paint.setColor(getResources().getColor(R.color.colorAccent));        paint.setAntiAlias(true);        paint.setTextSize(32);        int length = String.valueOf(currentAngleLength).length();        canvas.drawText(currentAngleLength + "", rectF.centerX() - 70, getHeight() / 2, paint);    }    /**     * 1.绘制总步数的黄色圆弧     *     * @param canvas 画笔     * @param rectF  参考的矩形     */    private void drawArcYellow(Canvas canvas, RectF rectF) {        Paint paint = new Paint();        paint.setColor(getResources().getColor(R.color.colorAccent));        /** 结合处为圆弧*/        paint.setStrokeJoin(Paint.Join.ROUND);        /** 设置画笔的样式 Paint.Cap.Round ,Cap.SQUARE等分别为圆形、方形*/        paint.setStrokeCap(Paint.Cap.ROUND);        /** 设置画笔的填充样式 Paint.Style.FILL  :填充内部;Paint.Style.FILL_AND_STROKE  :填充内部和描边;  Paint.Style.STROKE  :仅描边*/        paint.setStyle(Paint.Style.STROKE);        /**抗锯齿功能*/        paint.setAntiAlias(true);        /**设置画笔宽度*/        paint.setStrokeWidth(38f);        canvas.drawArc(rectF, startAngle, angleLength, false, paint);    }    /**     * 2.绘制当前步数的蓝色圆弧     *     * @param canvas     * @param rectF     */    private void drawArcRed(Canvas canvas, RectF rectF) {        Paint paint = new Paint();        /**设置结合处的样子,Miter:结合处为锐角, Round:结合处为圆弧:BEVEL:结合处为直线。*/        paint.setStrokeJoin(Paint.Join.ROUND);        paint.setStyle(Paint.Style.STROKE);//设置填充样式        /*** 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式         Cap.ROUND,或方形样式Cap.SQUARE   */        paint.setStrokeCap(Paint.Cap.ROUND);        paint.setAntiAlias(true);//抗锯齿功能        paint.setStrokeWidth(borderWidth);//设置画笔宽度        paint.setColor(getResources().getColor(R.color.colorPrimary));        canvas.drawArc(rectF, startAngle, currentAngleLength, false, paint);    }    /**     * 所走的步数进度     *     * @param totalStepNum  设置的步数     * @param currentCounts 所走步数     */    public void setCurrentCount(int totalStepNum, int currentCounts) {        stepNumber = currentCounts + "";/**如果当前走的步数超过总步数则圆弧还是270度,不能成为园*/        if (currentCounts > totalStepNum) {            currentCounts = totalStepNum;        }/**所走步数占用总共步数的百分比*/        float scale = (float) currentCounts / totalStepNum;/**换算成弧度最后要到达的角度的长度-->弧长*/        float currentAngleLength = scale * angleLength;/**开始执行动画*/        setAnimation(0, currentAngleLength, animationLength);    }    /**     * 为进度设置动画     * ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,     * 而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。     * 它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,     * 我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,     * 那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。     *     * @param last     * @param current     */    private void setAnimation(float last, float current, int length) {        ValueAnimator progressAnimator = ValueAnimator.ofFloat(last, current);        progressAnimator.setDuration(length);        progressAnimator.setTarget(currentAngleLength);        progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @Override            public void onAnimationUpdate(ValueAnimator animation) {                currentAngleLength = (float) animation.getAnimatedValue();                invalidate();            }        });        progressAnimator.start();    }}

代码里面也注释很多了,这里就不在过多解释

常见的验证码View代码:

package huangzhibo.com.learndemo.utils.commonedite;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Paint;import android.support.annotation.ColorRes;import android.support.v4.content.ContextCompat;import android.support.v7.widget.AppCompatEditText;import android.text.Editable;import android.text.TextPaint;import android.text.TextWatcher;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.util.TypedValue;import android.view.WindowManager;import huangzhibo.com.learndemo.R;/** * Created by HuangZhiBo on 2017/7/11/011. */public class VerificationCodeEditText extends AppCompatEditText implements VerificationAction, TextWatcher {    private int mFigures;    private int mVerCodeMargin;    private int mBottomSelectedColor; //底部选种颜色    private int mBottomNormalColor;  //未选中颜色    private float mBottomLineHeigth; //底部高度    private int mSeleceBackgroundColor; //选中的背景颜色    private OnVerificationCodeChangedListener onCodeChangeListener;    private int mCurrentPosition = 0;    private int mEachRectLength = 0;    private Paint mSelectBackGroundPaint;    private Paint mNormalBackGroundPaint;    private Paint mBottomSelectdPaint;    private Paint mBottomNormalPaint;    public VerificationCodeEditText(Context context) {        this(context, null);    }    public VerificationCodeEditText(Context context, AttributeSet attrs) {        this(context, null,0);    }    public VerificationCodeEditText(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initAttrs(attrs);        setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); //防止出现下划线        initPaint();        setFocusableInTouchMode(true);        super.addTextChangedListener(this);    }    /**     * 初始化pain     */    private void initPaint() {        mSelectBackGroundPaint = new Paint();        mSelectBackGroundPaint.setColor(mSeleceBackgroundColor);        mNormalBackGroundPaint = new Paint();        mNormalBackGroundPaint.setColor(getColor(android.R.color.transparent));        mBottomSelectdPaint = new Paint();        mBottomSelectdPaint.setColor(mBottomSelectedColor);        mBottomNormalPaint = new Paint();        mBottomNormalPaint.setColor(mBottomNormalColor);        mBottomSelectdPaint.setStrokeWidth(mBottomLineHeigth);        mBottomNormalPaint.setStrokeWidth(mBottomLineHeigth);    }    private void initAttrs(AttributeSet attrs) {        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.EditText);        mFigures = ta.getInteger(R.styleable.EditText_figures, 4);        mVerCodeMargin = (int) ta.getDimension(R.styleable.EditText_verCodeMargin, 10);        mBottomSelectedColor = ta.getColor(R.styleable.EditText_bottomLineSelectedColor, getCurrentTextColor());        mBottomNormalColor = ta.getColor(R.styleable.EditText_bottomLineNormalColor, getColor(android.R.color.holo_red_dark));        mBottomLineHeigth = ta.getDimension(R.styleable.EditText_bottomLineHeight, dp2px(5));        mSeleceBackgroundColor = ta.getColor(R.styleable.EditText_selectedBackgroundColor, getColor(android.R.color.holo_red_dark));        ta.recycle();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int widthResult = 0, heighResult = 0;        //最终宽度        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        if (widthMode == MeasureSpec.EXACTLY) {            widthResult = widthSize;        } else {            widthResult = getScreenWidth(getContext());        }        //每个矩形的宽度        mEachRectLength = (widthResult - (mVerCodeMargin * (mFigures - 1))) / mFigures;        //最终高度        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        if (heightMode == MeasureSpec.EXACTLY) {            heighResult = heightSize;        } else {            heighResult = mEachRectLength;        }        setMeasuredDimension(widthResult, heighResult);    }    @Override    protected void onDraw(Canvas canvas) {        mCurrentPosition = getText().length();        int width = mEachRectLength - getPaddingLeft() - getPaddingRight(); //每个矩形宽度        int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); //整体高度        //绘制每个矩形        for (int i = 0; i < mFigures; i++) {            canvas.save();            int start = width * i + i * mVerCodeMargin;            int end = width + start;            //画一个矩形            if (i == mCurrentPosition) {//选中                canvas.drawRect(start, 0, end, height, mSelectBackGroundPaint);            } else {                canvas.drawRect(start, 0, end, height, mNormalBackGroundPaint);            }            canvas.restore();        }        //绘制文字        String value = getText().toString();        for (int i = 0; i < value.length(); i++) {            canvas.save();            int start = width * i + i * mVerCodeMargin;            float x = start + width / 2;            TextPaint paint = getPaint();            paint.setTextAlign(Paint.Align.CENTER);            paint.setColor(getCurrentTextColor());            Paint.FontMetrics fontMetrics = paint.getFontMetrics();            float baseLine = (height - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;//top 是个负数            canvas.drawText(String.valueOf(value.charAt(i)), x, baseLine, paint);            canvas.restore();        }        //绘制底线        for (int i = 0; i < mFigures; i++) {            canvas.save();            float lineY = height - mBottomLineHeigth / 2;            int start = width * i + i * mVerCodeMargin;            int end = width + start;            if (i < mCurrentPosition) {                canvas.drawLine(start, lineY, end, lineY, mSelectBackGroundPaint);            } else {                canvas.drawLine(start, lineY, end, lineY, mBottomNormalPaint);            }            canvas.restore();        }    }    /**     * 获取手机屏幕的宽度     */    static int getScreenWidth(Context context) {        DisplayMetrics metrics = new DisplayMetrics();        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        wm.getDefaultDisplay().getMetrics(metrics);        return metrics.widthPixels;    }    @Override    final public void setCursorVisible(boolean visible) {        super.setCursorVisible(false);//隐藏光标的显示    }    @Override    public void beforeTextChanged(CharSequence s, int start, int count, int after) {        mCurrentPosition = getText().length();        postInvalidate();    }    @Override    public void onTextChanged(CharSequence s, int start, int before, int count) {        mCurrentPosition = getText().length();        postInvalidate();        if (onCodeChangeListener != null) {            onCodeChangeListener.onVerCodeChanged(getText(), start, before, count);        }    }    @Override    public void afterTextChanged(Editable s) {        mCurrentPosition = getText().length();        postInvalidate();        if (getText().length() == mFigures) {            if (onCodeChangeListener != null) {                onCodeChangeListener.onInputCompleted(getText());            }        } else if (getText().length() > mFigures) {            getText().delete(mFigures, getText().length());        }    }    @Override    public void setFigures(int figures) {        mFigures = figures;        postInvalidate();    }    @Override    public void setVerCodeMargin(int margin) {        mVerCodeMargin = margin;        postInvalidate();    }    @Override    public void setBottomSelectedColor(@ColorRes int bottomSelectedColor) {        mBottomSelectedColor = bottomSelectedColor;        postInvalidate();    }    @Override    public void setBottomNormalColor(@ColorRes int bottomNormalColor) {        mBottomNormalColor = bottomNormalColor;        postInvalidate();    }    @Override    public void setSelectedBackgroundColor(@ColorRes int selectedBackground) {        mSeleceBackgroundColor = selectedBackground;        postInvalidate();    }    @Override    public void setBottomLineHeight(int bottomLineHeight) {        mBottomLineHeigth=bottomLineHeight;        postInvalidate();    }    @Override    public void setOnVerificationCodeChangedListener(OnVerificationCodeChangedListener listener) {        this.onCodeChangeListener=listener;    }    /**     * 返回颜色     */    private int getColor(@ColorRes int color) {        return ContextCompat.getColor(getContext(), color);    }    /**     * dp转px     */    private int dp2px(int dp) {        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,                getResources().getDisplayMetrics());    }}

对于自定义验证码的View,需要考虑超出文字字数处理,文字变化监听处理,相对于这个demo来说还是有一点难度,需要大家慢慢的去理解和消化。这里把几个要点总结下,以便更好的去理解这个实现逻辑

  • 初始界面,绘画一个矩形(分为选中和未选中),底部横线,绘制文字
  • 文字输入监听,重新绘制
  • 文字超出处理

以上的demo已经放在网上仓库,需要的话可以点击

这个demo有我最近在整理的项目常用的工具类和框架,会不断完善。

转载于:https://my.oschina.net/huangzhi1bo/blog/1517689

你可能感兴趣的文章
node+express+mongDB实现简单登录注册
查看>>
个人博客 SEO 优化(2):站内优化
查看>>
从源码全面剖析 React 组件更新机制
查看>>
十个你需要在 PHP 7 中避免的坑
查看>>
spring-MVC源码解读(一)
查看>>
[译]php和curl_multi_exec
查看>>
java对象关系映射ROM
查看>>
Hystrix指标窗口实现原理
查看>>
【419天】跃迁之路——程序员高效学习方法论探索系列(实验阶段176-2018.03.31)...
查看>>
LeetCode 200. Number of Islands
查看>>
做一个合格的前端,gulp资源大集合
查看>>
常用符号的英文名
查看>>
Laravel - Artisan 个人常用总结
查看>>
重温一遍数据结构之单链表(golang版)
查看>>
使用git修复线上指定版本的问题
查看>>
【Node核心模块HTTP】
查看>>
CSS > 关于雪碧图预处理和后处理方案的讨论
查看>>
从 JavaScript 到 TypeScript 5 - 路由进化
查看>>
微信小程序实践_1前言
查看>>
我开发中总结的小技巧
查看>>