請尊重個人勞動成果,轉載註明出處,謝謝!
http://blog.csdn.net/xiaxiazaizai01/article/details/52355558
這是一個一言不合就手擼一個自定義View的任性時代,因此最近一段時間一直在學習自定義View相關的知識,也看了很多與此相關的博客,有句話叫做不要重複造輪子,別人寫好的直接拿過來改吧改吧,能用就行,但是,要想像那些任性的大牛一樣,分分鐘擼一個自定義View,就得不斷的重複造輪子,學習大神們的設計思路, 站在牛人的肩膀上不斷前行,每篇開篇之前都要囉嗦半天,急性子的童鞋可以直接跳過。看到yissan大牛寫了一篇自定義圓形進度條,思路很清晰,就照着也擼了一遍,果然是酸爽啊,在這裏非常感謝yissan大牛,哈哈。。。爲了讓大家能一遍就看懂,我會把註釋寫的非常非常詳細,秒懂哦,,什麼??你不能秒懂。。註釋都寫的辣麼詳細了,面壁思過去。。哈哈
下面看下效果圖:
1、首先創建View
(1)設置自定義View屬性,通常做法是在res/values裏面創建一個attrs文件夾,來寫我們的自定義屬性,一般我們設置屬性的name時,一般習慣性的將我們自定義的類名作爲name
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 自定義圓形進度條,屬性設置 -->
<declare-styleable name="CustomCircleProgress">
<!-- 默認圓的顏色 -->
<attr name="progress_default_color" format="color"/>
<!-- 進度圓的顏色 -->
<attr name="progress_reached_color" format="color"/>
<!-- 進度條的高度 -->
<attr name="progress_reached_height" format="dimension"/>
<!-- 無進度時(默認圓)的邊框高 -->
<attr name="progress_default_height" format="dimension"/>
<!-- 圓的半徑 -->
<attr name="circle_radius" format="dimension"/>
</declare-styleable>
</resources>
(2)設置完了自定義屬性,下一步當然是在我們的自定義View類中去獲取。(我們都習慣在參數多的構造方法中去獲取自定義屬性,其他構造方法則去通過this去調用,注意這裏是this而不是super,super的話則指向的是父類,這裏我犯了一個常識性錯誤,一鍵生成幾個構造方法,忘了將super改成this,導致獲取屬性的方法沒有被調用執行,大家在調用的時候可以打斷點試試)
public CustomCircleProgress(Context context) {
this(context,null);
}
public CustomCircleProgress(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CustomCircleProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取自定義屬性的值
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CustomCircleProgress);
//默認圓的顏色
mDefaultColor = array.getColor(R.styleable.CustomCircleProgress_progress_default_color, PROGRESS_DEFAULT_COLOR);
//進度條的顏色
mReachedColor = array.getColor(R.styleable.CustomCircleProgress_progress_reached_color, PROGRESS_REACHED_COLOR);
//默認圓的高度
mDefaultHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_default_height, mDefaultHeight);
//進度條的高度
mReachedHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_reached_height, mReachedHeight);
//圓的半徑
mRadius = (int) array.getDimension(R.styleable.CustomCircleProgress_circle_radius, mRadius);
//最後不要忘了回收 TypedArray
array.recycle();
//設置畫筆(new畫筆的操作一般不要放在onDraw方法中,因爲在繪製的過程中onDraw方法會被多次調用)
setPaint();
我們在new我們的畫筆時一般不要在onDraw()方法中去new,因爲view在不斷的繪製過程中onDraw()方法會不斷的被調用,這樣就會造成不停的new我們的畫筆實例。
//設置畫筆
private void setPaint() {
mPaint = new Paint();
//下面是設置畫筆的一些屬性
mPaint.setAntiAlias(true);//抗鋸齒
mPaint.setDither(true);//防抖動,繪製出來的圖要更加柔和清晰
mPaint.setStyle(Paint.Style.STROKE);//設置填充樣式
/**
* Paint.Style.FILL :填充內部
* Paint.Style.FILL_AND_STROKE :填充內部和描邊
* Paint.Style.STROKE :僅描邊
*/
mPaint.setStrokeCap(Paint.Cap.ROUND);//設置畫筆筆刷類型
}
2、處理View的佈局,即測量onMeasure( )
當我們在xml文件中給這個view設置android:layout_width=”“android:layout_height=”“屬性爲固定值、wrap_parent、match_parent 時,表明開發者向ViewGroup溝通表明我需要的空間。ViewGroup收到了開發者對View大小的說明,然後ViewGroup會綜合考慮自己的空間大小以及開發者的請求,然後生成兩個MeasureSpec對象(width與height)傳給View,這兩個對象是ViewGroup向子View提出的要求,就相當於告訴子View:“我已經與你的使用者(開發者)商量過了,現在把我們商量確定的結果告訴你,你的寬度不能違反width MeasureSpec對象的要求,你的高度不能違反height MeasureSpec對象的要求,現在,你趕緊根據這個要求確定下自己要多大空間,只許少,不許多哦。”對於超過ViewGroup爲我們分配的空間時,就需要進行測量處理,然後再將處理後的結果反饋給ViewGroup,如果不是很瞭解的話可以點擊查看上一篇博客,有詳細的說明
/**
* 使用onMeasure方法是因爲我們的自定義圓形View的一些屬性(如:進度條寬度等)都交給用戶自己去自定義了,所以我們需要去測量下
* 看是否符合要求
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int paintHeight = Math.max(mReachedHeight, mDefaultHeight);//比較兩數,取最大值
if(heightMode != MeasureSpec.EXACTLY){
//如果用戶沒有精確指出寬高時,我們就要測量整個View所需要分配的高度了,測量自定義圓形View設置的上下內邊距+圓形view的直徑+圓形描邊邊框的高度
int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius*2 + paintHeight;
//然後再將測量後的值作爲精確值傳給父類,告訴他我需要這麼大的空間,你給我分配吧
heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
}
if(widthMode != MeasureSpec.EXACTLY){
//這裏在自定義屬性中沒有設置圓形邊框的寬度,所以這裏直接用高度代替
int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius*2 + paintHeight;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我們需要考慮開發者有時會給View設置一些padding屬性
3、繪製View,即onDraw()
(1)這裏我們需要繪製默認的內部圓以及表示進度的外層圓弧,根據進度值的變化來繪製圓弧。在繪製外層表示進度的圓弧時,需要首先確定圓弧的外接矩形(進度也就成了內切圓)的座標,如下圖所示
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 這裏canvas.save();和canvas.restore();是兩個相互匹配出現的,作用是用來保存畫布的狀態和取出保存的狀態的
* 當我們對畫布進行旋轉,縮放,平移等操作的時候其實我們是想對特定的元素進行操作,但是當你用canvas的方法來進行這些操作的時候,其實是對整個畫布進行了操作,
* 那麼之後在畫布上的元素都會受到影響,所以我們在操作之前調用canvas.save()來保存畫布當前的狀態,當操作之後取出之前保存過的狀態,
* (比如:前面元素設置了平移或旋轉的操作後,下一個元素在進行繪製之前執行了canvas.save();和canvas.restore()操作)這樣後面的元素就不會受到(平移或旋轉的)影響
*/
canvas.save();
//爲了保證最外層的圓弧全部顯示,我們通常會設置自定義view的padding屬性,這樣就有了內邊距,所以畫筆應該平移到內邊距的位置,這樣畫筆纔會剛好在最外層的圓弧上
//畫筆平移到指定paddingLeft, getPaddingTop()位置
canvas.translate(getPaddingLeft(),getPaddingTop());
mPaint.setStyle(Paint.Style.STROKE);
//畫默認圓(邊框)的一些設置
mPaint.setColor(mDefaultColor);
mPaint.setStrokeWidth(mDefaultHeight);
canvas.drawCircle(mRadius,mRadius,mRadius,mPaint);
//畫進度條的一些設置
mPaint.setColor(mReachedColor);
mPaint.setStrokeWidth(mReachedHeight);
//根據進度繪製圓弧
float sweepAngle = getProgress() * 1.0f / getMax() * 360;
canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius *2), 0, sweepAngle, false, mPaint);//drawArc:繪製圓弧
canvas.restore();
}
我們做個定時器,讓進度條動起來
public class MainActivity extends AppCompatActivity {
private CustomCircleProgress circleProgress;
private int progress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
if(progress >= 100){
progress = 0;
circleProgress.setProgress(0);
}else{
progress = circleProgress.getProgress();
circleProgress.setProgress(++progress);
}
}
};
timer.schedule(task,0,100);
}
}
這樣得到的效果圖是這樣的
有的夥計該說了,爲什麼進度條的起始位置不是從最上面開始的,因爲這裏我設置的canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius 2), 0, sweepAngle, false, mPaint);中我設置的參數爲0,表示圓弧的起始位置從0開始,即X軸的正方向。這裏我們只需將圓弧的起始位置設置成-90度即可,canvas.drawArc(new RectF(0, 0, mRadius 2, mRadius *2), -90, sweepAngle, false, mPaint);我們再來看下效果圖
完美,,哈哈,,,,
//繪製圓
public void drawCircle (float cx, float cy, float radius, Paint paint)
//參數說明
/**
* cx:圓心的x座標。
cy:圓心的y座標。
radius:圓的半徑。
paint:繪製時所使用的畫筆。
*/
(2)接下來,我們開始繪製裏面的暫停(完成)狀態時的三角形,以及開啓狀態時的兩條豎線,首先我們通過枚舉的方式定義這兩種狀態,並提供set/get方法供外界調用。首先我們需要Path mPath = new Path();然後通過mPath.moveTo()確定三角形的第一個點的座標,然後通過mPath.lineTo()鏈接其他幾個點的座標,如果當我們設置畫筆的樣式爲mPaint.setStyle(Paint.Style.STROKE);則我們需要執行close形成封閉的三角形,或者你也可以直接再來一條mPath.lineTo()再將第一個點的座標給連接起來,這樣也形成了一個封閉的三角形。
//通過path路徑繪製三角形
mPath = new Path();
//讓三角形的長度等於圓的半徑(等邊三角形)
triangleLength = mRadius;
//繪製三角形,首先我們需要確定三個點的座標
float firstX = (float) ((mRadius*2 - Math.sqrt(3.0) / 2 * mRadius) / 2);//左上角第一個點的橫座標,根據勾三股四弦五定律,Math.sqrt(3.0)表示開方
//爲了顯示的好看些,這裏微調下firstX橫座標
float mFirstX = (float)(firstX + firstX*0.2);
float firstY = mRadius - triangleLength / 2;
//同理,依次可得出第二個點(左下角)第三個點的座標
float secondX = mFirstX;
float secondY = (float) (mRadius + triangleLength / 2);
float thirdX = (float) (mFirstX + Math.sqrt(3.0) / 2 * mRadius);
float thirdY = mRadius;
mPath.moveTo(mFirstX,firstY);
mPath.lineTo(secondX,secondY);
mPath.lineTo(thirdX,thirdY);
mPath.lineTo(mFirstX,firstY);
然後我們在onDraw()方法中去判斷繪製不同狀態下的view
//有了path之後就可以在onDraw中繪製三角形的End和Starting狀態了
if(mStatus == Status.End){//未開始狀態,畫筆填充三角形
mPaint.setStyle(Paint.Style.FILL);
//設置顏色
mPaint.setColor(Color.parseColor("#01A1EB"));
//畫三角形
canvas.drawPath(mPath,mPaint);
}else{//正在進行狀態,畫兩條豎線
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dp2px(5));
mPaint.setColor(Color.parseColor("#01A1EB"));
canvas.drawLine(mRadius*2/3, mRadius*2/3, mRadius*2/3, 2*mRadius*2/3, mPaint);
canvas.drawLine(2*mRadius - (mRadius*2/3), mRadius*2/3, 2*mRadius - (mRadius*2/3), 2*mRadius*2/3, mPaint);
}
4、處理與用戶的交互
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="com.example.customview.MainActivity">
<com.example.customview.CustomCircleProgress
android:id="@+id/circleProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="10dp"
/>
</RelativeLayout>
最後是我們的MainActivity類
public class MainActivity extends AppCompatActivity {
private CustomCircleProgress circleProgress;
private int progress;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case PROGRESS_CIRCLE_STARTING:
progress = circleProgress.getProgress();
circleProgress.setProgress(++progress);
if(progress >= 100){
handler.removeMessages(PROGRESS_CIRCLE_STARTING);
progress = 0;
circleProgress.setProgress(0);
circleProgress.setStatus(CustomCircleProgress.Status.End);//修改顯示狀態爲完成
}else{
//延遲100ms後繼續發消息,實現循環,直到progress=100
handler.sendEmptyMessageDelayed(PROGRESS_CIRCLE_STARTING, 100);
}
break;
}
}
};
public static final int PROGRESS_CIRCLE_STARTING = 0x110;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);
circleProgress.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(circleProgress.getStatus() == CustomCircleProgress.Status.Starting){//如果是開始狀態
//點擊則變成關閉暫停狀態
circleProgress.setStatus(CustomCircleProgress.Status.End);
//注意,當我們暫停時,同時還要移除消息,不然的話進度不會被停止
handler.removeMessages(PROGRESS_CIRCLE_STARTING);
}else{
//點擊則變成開啓狀態
circleProgress.setStatus(CustomCircleProgress.Status.Starting);
Message message = Message.obtain();
message.what = PROGRESS_CIRCLE_STARTING;
handler.sendMessage(message);
}
}
});
}
}