在开发过程中我们总会遇到一些不同于安卓自带的控件,业内称之为自定义控件,一直没有深入了解自定义VIEW,总觉得好像很厉害的样子,最近公司业务需求(做一个APK文件的下载)需要个性化的展示下载进度条。于是尝试着写一个下载进度条的自定义控件
为了不浪费大家的时间,先上效果图,对于赶时间的哥们来说在这里就是一个分水岭了,如果大家奔着学习自定义控件来的,那你不妨接着看下去
效果如图所示,只是小白不会制作动态图,只能随机截取一张示例
1、自定义View的属性
2、在View的构造方法中获得我们自定义的属性
3、#重写onMesure #
4、重写onDraw
第三点使用了不同的符号,想必有特殊的地方,别急,等一下会解释。现在结合我们的需求:下载进度条 最简单的进度条无非两个部分组成
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 四周圆弧度 --> <attr name="cornerRadius" format="dimension" /> <attr name="text" format="string" /> <attr name="textColor" format="color" /> <attr name="textSize" format="dimension" /> <!-- 默认颜色 --> <attr name="defaultColor" format="color" /> <!-- 未下载部分颜色 --> <attr name="undownloadColor" format="color" /> <!-- 已经下载部分颜色 --> <attr name="downloadedColor" format="color" /> <!-- RuffianProgressBarLine --> <declare-styleable name="RuffianProgressBarLine"> <attr name="cornerRadius" /> <attr name="text" /> <attr name="textColor" /> <attr name="textSize" /> <attr name="defaultColor" /> <attr name="undownloadColor" /> <attr name="downloadedColor" /> </declare-styleable> </resources>根据需求,我们定义了字体,字体颜色,字体大小,控件默认颜色,已经下载部分颜色,未下载部分颜色,控件的形状[矩形,圆角矩形],一共7个属性,format是值该属性的取值类型:
一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。
然后在布局中声明我们的自定义View
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.ruffian.android.MainActivity$PlaceholderFragment" > <com.ruffian.android.view.RuffianProgressBarLine xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/progressBarLine1" android:layout_width="100dp" android:layout_height="30dp" android:padding="10dp" custom:cornerRadius="20dp" custom:defaultColor="#9ACF51" custom:downloadedColor="#ec7883" custom:text="下载" custom:textColor="@android:color/white" custom:textSize="16sp" custom:undownloadColor="#cdcdcd" /> <com.ruffian.android.view.RuffianProgressBarLine xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/progressBarLine2" android:layout_width="100dp" android:layout_height="30dp" android:layout_alignParentRight="true" android:padding="10dp" custom:cornerRadius="0dp" custom:defaultColor="#f29b76" custom:downloadedColor="#e55a7f" custom:text="下载" custom:textColor="@android:color/white" custom:textSize="16sp" custom:undownloadColor="#fb9090" /> <TextView android:id="@+id/progressText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/progressBarLine1" android:layout_centerHorizontal="true" android:padding="10dp" android:text="下载进度 " /> <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="再玩一次" /> </RelativeLayout>布局中展示不同的控件形状,同时展示下载进度百分比
注意:一定要引入 xmlns:custom="http://schemas.android.com/apk/res/res-auto"我们的命名空间,后面也可以是包路径:com.ruffian.android.view
2、在View的构造方法中,获得我们的自定义的样式
// 默认 public static final String STATE_DEFAULT = "DEFAULT"; // 安装 public static final String STATE_INSTALL = "INSTALL"; // 暂停 public static final String STATE_STOP = "STOP"; // 下载 public static final String STATE_DOWNLOAD = "DOWNLOAD"; // 打开 public static final String STATE_OPEN = "OPEN"; // 最大值100 private static final float MAX_PROGRESS = 100; /** * 控件四周圆弧角度,0:矩形<br/> * 不设置或者设置为0的情况是矩形,其他情况是圆角矩形 * */ private float mCornerRadius; /** * 文字 */ private String mText = ""; /** * 字体颜色 */ private int mTextColor; /** * 字体大小 */ private int mTextSize; /** * 控件默认颜色 */ private int mDefaultColor; /** * 默认颜色 */ private final String DEF_DEFAULTCOLOR = "#9ACF51"; /** * 未下载部分颜色 */ private int mUnDownloadColor; /** * 默认颜色-下载进度条背景 */ private final String DEF_BACKGROUDCOLOR = "#cdcdcd"; /** * 已经下载部分颜色 */ private int mDownloadedColor; /** * 默认颜色-下载进度 */ private final String DEF_DOWNLOADCOLOR = "#ec7883"; /** * 矩形,绘制文字需要用 */ private Rect mRect; /** * 圆角矩形 */ private RectF mRectF; /** * 画笔,属性值可能改变 */ private Paint mPaint; /** * 文字画笔,初始化之后属性不再改变 */ private Paint mTextPaint; /** * 控件状态 */ private String mState = STATE_DEFAULT; /** * 下载进度{这里根据需求定基础类型,也可以是float[0.0f,1.0f]} */ private int mProgress; public RuffianProgressBarLine(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RuffianProgressBarLine(Context context) { this(context, null); } public RuffianProgressBarLine(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 获取自定义的控件 TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.RuffianProgressBarLine, defStyleAttr, 0); int parameterCount = typedArray.getIndexCount(); for (int i = 0; i < parameterCount; i++) { int attr = typedArray.getIndex(i); switch (attr) { case R.styleable.RuffianProgressBarLine_cornerRadius: mCornerRadius = typedArray.getDimensionPixelSize(attr, 0); break; case R.styleable.RuffianProgressBarLine_text: mText = typedArray.getString(attr); break; case R.styleable.RuffianProgressBarLine_textColor: mTextColor = typedArray.getColor(attr, 0); break; case R.styleable.RuffianProgressBarLine_textSize: mTextSize = typedArray.getDimensionPixelSize(attr, 12); break; case R.styleable.RuffianProgressBarLine_defaultColor: mDefaultColor = typedArray.getColor(attr, Color.parseColor(DEF_DEFAULTCOLOR)); break; case R.styleable.RuffianProgressBarLine_undownloadColor: mUnDownloadColor = typedArray.getColor(attr, Color.parseColor(DEF_BACKGROUDCOLOR)); break; case R.styleable.RuffianProgressBarLine_downloadedColor: mDownloadedColor = typedArray.getColor(attr, Color.parseColor(DEF_DOWNLOADCOLOR)); break; } } typedArray.recycle(); mPaint = new Paint(); mRect = new Rect(); mRectF = new RectF(); // 初始化之后不再改变,直接设置属性 mTextPaint = new Paint(); // 设置抗锯齿,圆滑处理 mTextPaint.setAntiAlias(true); // 设置画笔类型 mTextPaint.setStyle(Style.FILL); // 设置画笔颜色 mTextPaint.setColor(mTextColor); // 设置字体大小 mTextPaint.setTextSize(mTextSize); }我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。
3、我们重写 onDraw,onMesure 调用系统提供的:
/** * 重写计算控件宽高函数 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 获取宽高的设置模式 int withMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取宽高的大小 int withSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 最终宽高 int height = getSizeInMode(heightSize, heightMode, 1); int width = getSizeInMode(withSize, withMode, 0); // 最终设置宽高 setMeasuredDimension(width, height); } /** * 获取不同mode下宽高的实际值<br/> * type[0:宽,1:高] * * @param size初始值 * @param mode设置类型 * @param type * @return * @author Ruffian * @date 2015年12月11日 */ private int getSizeInMode(int size, int mode, int type) { // 返回值 int sizeValue = 0; switch (mode) { case MeasureSpec.EXACTLY: // 设置了明确的值,直接使用 sizeValue = size; break; case MeasureSpec.AT_MOST: // WARP_CONTENT时候,先计算绘制文本的大小 mTextPaint.setTextSize(mTextSize); mTextPaint.getTextBounds(mText, 0, mText.length(), mRect); // 再计算[左右,上下]的padding值 int desired = 0; if (type == 0) { // 文本宽度+左右padding float textWidth = mRect.width(); desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); } else if (type == 1) { // 文本宽度+上下padding float textHeight = mRect.height(); desired = (int) (getPaddingTop() + textHeight + getPaddingBottom()); } sizeValue = desired; break; case MeasureSpec.UNSPECIFIED: // 不处理 break; } return sizeValue; } /** * 重写绘制函数onDraw */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置抗锯齿,圆滑处理 mPaint.setAntiAlias(true); // 设置画笔类型 mPaint.setStyle(Style.FILL); // 绘制控件 canvasViewOnLogic(canvas); } /** * 根据业务逻辑绘制控件 * * @param canvas * @author Ruffian * @date 2015年12月11日 */ private void canvasViewOnLogic(Canvas canvas) { /** * 下载中和暂停状态是特殊情况,需要画两层视图,其他情况只需要一层 */ if (mState.equals(STATE_DOWNLOAD) || mState.equals(STATE_STOP)) { // 暂停状态--下载中状态 // 绘制时mProgress要转化成float类型,区间[0.0f,1.0f] drawDownloadView(canvas, mDownloadedColor, 0, (int) ((mProgress / MAX_PROGRESS) * getWidth())); drawDownloadView(canvas, mUnDownloadColor, (int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth()); } else { // 其他状态 // 设置默认画笔颜色 mPaint.setColor(mDefaultColor); // 设置矩形,宽度是控件大小 mRectF = new RectF(0, 0, getWidth(), getHeight()); // 画底部矩形 canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint); } // 计算文字 mTextPaint.getTextBounds(mText, 0, mText.length(), mRect); // 绘制文字居中 canvas.drawText(mText, getWidth() / 2 - mRect.width() / 2, getHeight() / 2 + mRect.height() / 2, mTextPaint); } /** * 绘制下载状态的view<br/> * 理解:绘制两次相同的view,不同颜色区分,一个绘制前半部分,一部分绘制后半部分 * * @param canvas * @param color * @param startX开始绘制的X * @param endX结束绘制的X * @author Ruffian * @date 2015年12月11日 */ private void drawDownloadView(Canvas canvas, int color, int startX, int endX) { mPaint.setColor(color); // 设置矩形,宽度是控件大小 mRectF = new RectF(0, 0, getWidth(), getHeight()); canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(startX, 0, endX, getMeasuredHeight()); canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint); canvas.restore(); }
当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:
重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
/** * 重写计算控件宽高函数 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 获取宽高的设置模式 int withMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取宽高的大小 int withSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 最终宽高 int height = getSizeInMode(heightSize, heightMode, 1); int width = getSizeInMode(withSize, withMode, 0); // 最终设置宽高 setMeasuredDimension(width, height); } /** * 获取不同mode下宽高的实际值<br/> * type[0:宽,1:高] * * @param size初始值 * @param mode设置类型 * @param type * @return * @author Ruffian * @date 2015年12月11日 */ private int getSizeInMode(int size, int mode, int type) { // 返回值 int sizeValue = 0; switch (mode) { case MeasureSpec.EXACTLY: // 设置了明确的值,直接使用 sizeValue = size; break; case MeasureSpec.AT_MOST: // WARP_CONTENT时候,先计算绘制文本的大小 mTextPaint.setTextSize(mTextSize); mTextPaint.getTextBounds(mText, 0, mText.length(), mRect); // 再计算[左右,上下]的padding值 int desired = 0; if (type == 0) { // 文本宽度+左右padding float textWidth = mRect.width(); desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); } else if (type == 1) { // 文本宽度+上下padding float textHeight = mRect.height(); desired = (int) (getPaddingTop() + textHeight + getPaddingBottom()); } sizeValue = desired; break; case MeasureSpec.UNSPECIFIED: // 不处理 break; } return sizeValue; }
这里特别说明一下 onDraw方法
如果是在矩形的情况下是很简答的一种实现:
先画一个底部的矩形(表示未下载),然后再重新设置画笔颜色再画一个(表示进度)矩形。看起来就能达到下载进度的效果
但是当我们设置属性为 圆角矩形(cornerRadius>0)的时候,我发现效果不是我想要的
运行结果是:这样的,这样的
但是我们想要的是:这样的,这样的
由于刚开始自定义控件,很多属性和用法都不知道怎么用,折腾了好久,后来在网上看到说 canvas 有个 clipRect 的方法,good ,那么修改一下绘制部分的代码就可以了
起初代码
// 暂停状态--下载中状态 // 设置底部矩形颜色 mPaint.setColor(mBackgroudColor); // 设置矩形,宽度是控件大小 mRectF = new RectF(0, 0, getWidth(), getHeight()); // 画底部矩形 canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint); // 设置进度矩形颜色 mPaint.setColor(mDownloadColor); // 设置矩形,宽度是实际进度 mRectF = new RectF(0, 0, (mProgress / MAX_PROGRESS) * getWidth(), getHeight()); // 画进度矩形 canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);
/** * 绘制下载状态的view<br/> * 理解:绘制两次相同的view,不同颜色区分,一个绘制前半部分,一部分绘制后半部分 * * @param canvas * @param color * @param startX开始绘制的X * @param endX结束绘制的X * @author Ruffian * @date 2015年12月11日 */ private void drawDownloadView(Canvas canvas, int color, int startX, int endX) { mPaint.setColor(color); // 设置矩形,宽度是控件大小 mRectF = new RectF(0, 0, getWidth(), getHeight()); canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(startX, 0, endX, getMeasuredHeight()); canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint); canvas.restore(); }
// 暂停状态--下载中状态 // 绘制时mProgress要转化成float类型,区间[0.0f,1.0f] drawDownloadView(canvas, mDownloadedColor, 0, (int) ((mProgress / MAX_PROGRESS) * getWidth())); drawDownloadView(canvas, mUnDownloadColor, (int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth());
activity代码
package com.ruffian.android; import android.annotation.SuppressLint; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import com.ruffian.android.view.RuffianProgressBarLine; @SuppressLint("HandlerLeak") public class MainActivity extends Activity { private RuffianProgressBarLine mProgressBarLine; private Button mButton; private TextView mProgressText;// 下载进度 int mProgress = 0; boolean isLoading = false; private String viewState; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mProgressBarLine = (RuffianProgressBarLine) findViewById(R.id.progressBarLine1); mProgressText = (TextView) findViewById(R.id.progressText); mButton = (Button) findViewById(R.id.button); mProgressBarLine.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { viewState = mProgressBarLine.getState(); if (viewState.equals(RuffianProgressBarLine.STATE_DEFAULT)) { // 下载中 mProgressBarLine .setState(RuffianProgressBarLine.STATE_DOWNLOAD); mProgressBarLine.setText("暂停"); isLoading = true; download(); } else if (viewState .equals(RuffianProgressBarLine.STATE_DOWNLOAD)) { // 暂停 mProgressBarLine .setState(RuffianProgressBarLine.STATE_STOP); mProgressBarLine.setText("继续"); isLoading = false; // download(); } else if (viewState.equals(RuffianProgressBarLine.STATE_STOP)) { // 继续 mProgressBarLine .setState(RuffianProgressBarLine.STATE_DOWNLOAD); mProgressBarLine.setText("暂停"); isLoading = true; // download(); } else if (viewState .equals(RuffianProgressBarLine.STATE_INSTALL)) { // 安装 mProgressBarLine .setState(RuffianProgressBarLine.STATE_INSTALL); mProgressBarLine.setText("安装"); } else if (viewState.equals(RuffianProgressBarLine.STATE_OPEN)) { // 运行 mProgressBarLine .setState(RuffianProgressBarLine.STATE_DOWNLOAD); mProgressBarLine.setText("运行"); } } }); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { mProgress = 0; isLoading = false; mProgressBarLine.setState(RuffianProgressBarLine.STATE_DEFAULT); mProgressBarLine.setText("下载"); mProgressText.setText("下载进度 "); } }); } /** * 下载,暂停 * * @author Ruffian * @date 2015年12月11日 */ public void download() { new Thread() { public void run() { while (mProgress <= 100) { if (mProgress == 100) { // 进度满100,状态改为安装 mProgressBarLine .setState(RuffianProgressBarLine.STATE_INSTALL); mProgressBarLine.setText("安装"); } // 是否正在下载 if (isLoading) { // 更新UI uiHandler.sendMessage(uiHandler.obtainMessage(1001, mProgress)); mProgressBarLine.setProgress(mProgress); mProgress++; } // Log.w("sss", "" + mProgress); try { Thread.sleep(80);// 进度改变速度 } catch (InterruptedException e) { e.printStackTrace(); } } }; }.start(); } /** * 更新UI */ Handler uiHandler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case 1001: int progress = (int) msg.obj; if (progress == 100) { mProgressText.setText("下载完成"); } else { mProgressText.setText(String.valueOf(progress) + "%"); } break; } }; }; }
源码下载