請尊重別人的勞動成果,轉發文章請註明出處
概述
在開發過程中我們總會遇到一些不同於安卓自帶的控件,業內稱之爲自定義控件,一直沒有深入瞭解自定義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;
}
};
};
}