请尊重别人的劳动成果,转发文章请注明出处
概述
在开发过程中我们总会遇到一些不同于安卓自带的控件,业内称之为自定义控件,一直没有深入了解自定义VIEW,总觉得好像很厉害的样子,最近公司业务需求(做一个APK文件的下载)需要个性化的展示下载进度条。于是尝试着写一个下载进度条的自定义控件
为了不浪费大家的时间,先上效果图,对于赶时间的哥们来说在这里就是一个分水岭了,如果大家奔着学习自定义控件来的,那你不妨接着看下去
效果如图所示,只是小白不会制作动态图,只能随机截取一张示例
自定义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="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;
}
};
};
}