1、概述
多看多学涨姿势, github真是个宝库
这个项目主要是实现数字进度条效果
github地址在https://github.com/daimajia/NumberProgressBar
感谢开源作者!
梳理主要知识点:
【1】熟悉自定义view的流程
【2】实现原理
【3】android中的view坐标系使用
【4】onMeasure优雅的方法书写
【5】canvas中drawText方法注意点
【6】代码的可读性非常强
2、项目要点分析
【熟悉自定义view的流程】
自定义view需要多多看别写的精彩代码,不过流程基本都是一致的在我的自定义View入门中有详细介绍按照这个思路去分析自定义view即可
【本项目实现原理】
该项目比较基础,适合作为入门学习项目,作者主要将自定义控件分为3大区域
mReachedRectF——Text区域(可以选择没有)——mUnreachedRectF
(该控件支持没有text区域),主要是通过控制mReachedRectF和mUnreachedRectF的坐标来不断地刷新ui来实现移动效果,没有使用到动画
自定义view 步骤 之获取自定义属性
这里作者直接写到其中一个构造方法中
public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); default_reached_bar_height = dp2px(1.5f); default_unreached_bar_height = dp2px(1.0f); default_text_size = sp2px(10); default_progress_text_offset = dp2px(3.0f); //load styled attributes. final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar, defStyleAttr, 0); mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color); mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color); mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color); mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size); mReachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_reached_bar_height, default_reached_bar_height); mUnreachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_unreached_bar_height, default_unreached_bar_height); mOffset = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_offset, default_progress_text_offset); int textVisible = attributes.getInt(R.styleable.NumberProgressBar_progress_text_visibility, PROGRESS_TEXT_VISIBLE); if (textVisible != PROGRESS_TEXT_VISIBLE) { mIfDrawText = false; } setProgress(attributes.getInt(R.styleable.NumberProgressBar_progress_current, 0)); setMax(attributes.getInt(R.styleable.NumberProgressBar_progress_max, 100)); attributes.recycle(); initializePainters(); }
这里作者开始初始化自定义的属性,通常我们可以单独使用一个函数放在每个构造器下面
然后就是onMeasure,这里比较优雅,
其中EXACTLY主要针对match_parent/具体参数
ATMOST主要针对wrap_content情况这里做了处理,有时候我们如果把布局文件的宽和高写成wrap_content,若此时父布局也为AT_MOST此时显示的就是父布局的PraentSize
因此我们支持设置wrap_content时候需要重写onMeasure方法,下面也是做了处理(AT_MOST)
鸿洋大神的自定义view博文对此也做了说明http://blog.csdn.net/lmj623565791/article/details/24252901
而UNSPECIFIED往往用于系统内部的测量通常只需要关注ATMOST和EXACTLY
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false)); } private int measure(int measureSpec, boolean isWidth) { int result; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom(); if (mode == MeasureSpec.EXACTLY) { result = size; } else { result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight(); result += padding; if (mode == MeasureSpec.AT_MOST) { if (isWidth) { result = Math.max(result, size); } else { result = Math.min(result, size); } } } return result; }
onDrawer方法的可读性更是6666
有时候我们也要注意写出“可以说话的代码”,注意函数的封装
@Override protected void onDraw(Canvas canvas) { if (mIfDrawText) { calculateDrawRectF(); } else { calculateDrawRectFWithoutProgressText(); } if (mDrawReachedBar) { canvas.drawRect(mReachedRectF, mReachedBarPaint); } if (mDrawUnreachedBar) { canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint); } if (mIfDrawText) canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint); }
这段代码我们可以清晰的看出作者的逻辑 要是设置了数字显示执行calculateDrawRectF()否则执行calculateDrawRectFWithoutProgressText()
这俩个函数就是开始处理前文提到的mReachedRectF和mUnreachedRectF俩个矩形的位置变化,这里需要熟悉一下android中的坐标系和点击位置的获取
看一下有文字时候计算俩个区域的情况主要集中处理了开始阶段和最终阶段的文字未知的特殊情况
private void calculateDrawRectF() { mCurrentDrawText = String.format("%d", getProgress() * 100 / getMax()); mCurrentDrawText = mPrefix + mCurrentDrawText + mSuffix;//转换成字符串 mDrawTextWidth = mTextPaint.measureText(mCurrentDrawText);//测量出文字的长度 if (getProgress() == 0) { mDrawReachedBar = false; mDrawTextStart = getPaddingLeft();//起始位置(右) } else { mDrawReachedBar = true; mReachedRectF.left = getPaddingLeft(); mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f; mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() - mOffset + getPaddingLeft(); mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f; mDrawTextStart = (mReachedRectF.right + mOffset);//实际中右位置 } //mTextPaint.descent() + mTextPaint.ascent() baseLine到字体最高+baseLine到字体最低=实际字体高度 mDrawTextEnd = (int) ((getHeight() / 2.0f) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f)); if ((mDrawTextStart + mDrawTextWidth) >= getWidth() - getPaddingRight()) { //文字终点位置重置文字起始位置和mReachedRectF矩形的right mDrawTextStart = getWidth() - getPaddingRight() - mDrawTextWidth; mReachedRectF.right = mDrawTextStart - mOffset; } float unreachedBarStart = mDrawTextStart + mDrawTextWidth + mOffset; if (unreachedBarStart >= getWidth() - getPaddingRight()) { //没有到最终点 mDrawUnreachedBar = false; } else { mDrawUnreachedBar = true; mUnreachedRectF.left = unreachedBarStart; mUnreachedRectF.top = getHeight() / 2.0f -mUnreachedBarHeight / 2.0f; mUnreachedRectF.right = getWidth() - getPaddingRight(); mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f; } }
这里主要提下mTextPaint.descent() + mTextPaint.ascent()的这里参照的距离都是baseline,这样可以计算出整个字体的高度,具体可以参考
http://mikewang.blog.51cto.com/3826268/871765/ 详细讲解了androd的字体属性和测量
还有canvas.drawText方法中xy坐标其实是baseline的位置,这在校准字体位置的时候很有用!