自定义view之下载控件,ProgressBar"/>
Android自定义view之下载控件,ProgressBar
请尊重别人的劳动成果,转发文章请注明出处
概述
在开发过程中我们总会遇到一些不同于安卓自带的控件,业内称之为自定义控件,一直没有深入了解自定义VIEW,总觉得好像很厉害的样子,最近公司业务需求(做一个APK文件的下载)需要个性化的展示下载进度条。于是尝试着写一个下载进度条的自定义控件
为了不浪费大家的时间,先上效果图,对于赶时间的哥们来说在这里就是一个分水岭了,如果大家奔着学习自定义控件来的,那你不妨接着看下去
效果如图所示,只是小白不会制作动态图,只能随机截取一张示例
自定义VIEW
在网上看了一些博客,这里抛砖引玉,先总结下自定义View的步骤:
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=""xmlns: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.RuffianProgressBarLinexmlns:custom=""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.RuffianProgressBarLinexmlns:custom=""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" /><TextViewandroid: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="下载进度 " /><Buttonandroid:id="@+id/button"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_centerInParent="true"android:text="再玩一次" /></RelativeLayout>
布局中展示不同的控件形状,同时展示下载进度百分比 注意:一定要引入 xmlns:custom=""我们的命名空间,后面也可以是包路径: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";// 最大值100private 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 调用系统提供的:
/*** 重写计算控件宽高函数*/@Overrideprotected 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) {// 文本宽度+左右paddingfloat textWidth = mRect.width();desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());} else if (type == 1) {// 文本宽度+上下paddingfloat textHeight = mRect.height();desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());}sizeValue = desired;break;case MeasureSpec.UNSPECIFIED:// 不处理break;}return sizeValue;}/*** 重写绘制函数onDraw*/@Overrideprotected 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();}
文章开头说 onMeasure 函数存在特殊情况。这种特殊情况是针对,布局文件中 layout_width , layout_height。
当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:
重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
/*** 重写计算控件宽高函数*/@Overrideprotected 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) {// 文本宽度+左右paddingfloat textWidth = mRect.width();desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());} else if (type == 1) {// 文本宽度+上下paddingfloat 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模拟下载整个流程,使用自定义的下载控件
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;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic 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) {// 更新UIuiHandler.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;}};};}
小弟第一次尝试自定义控件的实现,前辈们要是看见哪里有不正确的地方,还恳请耐心指正,同时小伙伴们在学习过程中遇到什么问题可以留言讨论, 紧急情况发送邮件:632835821@qq
源码下载
更多推荐
Android自定义view之下载控件,ProgressBar
发布评论