2017/01/18
上海
Write endless of the View
GitHub:https://github.com/ImmortalHalfWu/ViewSample/blob/master/app/src/main/java/com/viewsample/viewsample/views/ProgressButtonView.java
一批狐,樣式及事件分析
按鈕共有三種顯示樣式:
1,初始化狀態(tài)鸳谜,包含內(nèi)外兩個圓膝藕,靜態(tài)。
2咐扭,長按后芭挽,外圈拉伸,內(nèi)圈收縮草描,動態(tài)览绿。
3,外圈拉伸結(jié)束并且內(nèi)圈收縮結(jié)束穗慕,繪制進度條,動態(tài)妻导。
按鈕共有六種事件驅(qū)動:
1逛绵,初始化,控件加載倔韭。
2术浪,單擊,或者說短按寿酌。
3胰苏,短按抬手。
4醇疼,長按硕并。
5,長按抬手秧荆。
6倔毙,進度條加載結(jié)束。
事件與樣式的關(guān)系:
1乙濒,初始化陕赃、短按卵蛉、短按抬手,統(tǒng)一的初始化樣式么库。
2傻丝,長按,外圈拉伸诉儒,內(nèi)圈收縮葡缰,之后繪制進度條。
3允睹,長按抬手运准、進度條加載結(jié)束,恢復為初始化樣式缭受。
事件回調(diào):
1胁澳,短按,短按抬手時回調(diào)米者。
2韭畸,長按按下,判斷為長按后立刻回調(diào)蔓搞。
3胰丁,長按抬起時回調(diào)。
4喂分,進度條結(jié)束時回調(diào)锦庸。
5,長按后蒲祈,手指滑動時回調(diào)甘萧。
二,模塊劃分
1梆掸,StateMachine狀態(tài)機 + 低配MVC + 回調(diào)接口
StateMachine狀態(tài)機 : 描述所有狀態(tài)扬卷,并記錄當下所處狀態(tài)。
View : 測量自身寬高酸钦,繪制圖形怪得,接收觸屏事件。
Model : 記錄數(shù)值卑硫,例如控件寬高徒恋,以及繪制圖形時的數(shù)值。
Controller : 處理觸屏事件拔恰,針對事件修改Model數(shù)據(jù)因谎,并通知View重繪。
CallBackListener:回調(diào)接口颜懊,將控件的不同狀態(tài)及事件傳遞給外部财岔。
流程:
1风皿,初始化StateMachine、Model匠璧、Controller桐款。
2,獲取Model數(shù)值繪制圖形夷恍。
3魔眨,接受觸屏事件。
4酿雪,修改狀態(tài)遏暴。
5,Controller根據(jù)狀態(tài)修改Model繪制數(shù)值指黎。
6朋凉,通知View重繪。
7醋安,如果外部傳入了回調(diào)接口杂彭,則回調(diào)。
三吓揪,自上而下的抽象:
StateMachine亲怠,狀態(tài)機。
/**
* 狀態(tài)機柠辞,描述不同的狀態(tài)
*/
public enum StateMachine{
/**
* 初始化狀態(tài)
*/
STATE_INITIA,
/**
* 短按
*/
STATE_SHORT_CLICK,
/**
* 短按抬手
*/
STATE_SHORT_UP,
/**
* 長按中
*/
STATE_LONG_CLICKING,
/**
* 長按抬手
*/
STATE_LONG_UP,
/**
* 進度條加載結(jié)束
*/
STATE_PROGRESS_OVER,
}
CallBackListener团秽,回調(diào)接口.
/**
* 事件回調(diào)接口
*/
public interface CallBackListener{
/**
* 短按,抬手時回調(diào)
*/
void shortClick();
/**
* 長按叭首,判斷為長按后回調(diào)
*/
void longClick();
/**
* 長按抬起徙垫,與{@link #progressOver()}只有一個會回調(diào)
*/
void longClickUp();
/**
* 進度條結(jié)束 ,與{@link #longClickUp()}只有一個會回調(diào)
*/
void progressOver();
/**
* 手指滑動放棒,只有在長按中滑動才會調(diào)用
* @param event
*/
void move(MotionEvent event);
}
View , 重點在于觸屏與圖形繪制,基于責任劃分的目的己英,View本身應盡量避免不同狀態(tài)下或不同事件下數(shù)值的計算间螟。
//計算寬高,注意到控件是有伸縮效果的损肛,所以得到的寬高應該是拉伸后的寬高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//計算拉伸后的寬高
}
//繪制圖形厢破,并只負責繪制圖形,圖形的參數(shù)數(shù)值不在此處理
protected void onDraw(Canvas canvas) {
//外圓
//進度條
//內(nèi)圓
}
//觸屏監(jiān)聽
public boolean onTouchEvent(MotionEvent event) {
//根據(jù)事件修改View所處的狀態(tài)
}
//銷毀
protected void onDetachedFromWindow(){
//釋放資源
}
Model治拿,記錄繪制數(shù)值摩泪。(為了避免又多又長的變量名所造成的不適,變量以大白話漢字表示)
"控件寬度"
"控件高度"
"初始化時內(nèi)圓半徑" //初始化時的內(nèi)圓半徑
"初始化時外圓半徑" //初始化時的外圓半徑
"長按狀態(tài)內(nèi)圓半徑" //長按狀態(tài)下內(nèi)圓半徑
"長按狀態(tài)外圓半徑" //長按狀態(tài)下外圓半徑
"繪制時使用的內(nèi)圓半徑" //大于等于最小內(nèi)圓半徑劫谅,小于等于最大內(nèi)圓半徑
"繪制時使用的外圓半徑" //大于等于最小外圓半徑见坑,小于等于最大外圓半徑
"進度條寬度"
"進度條弧度"
"進度條最長時間"
"外圓顏色"
"內(nèi)圓顏色"
"進度條顏色"
Controller嚷掠,控制器,根據(jù)狀態(tài)的不同荞驴,對Model數(shù)值進行不同的計算不皆,之后刷新界面。
使用線程作為Controller熊楼,缺點是為了避免浪費CPU霹娄,需要掌控好線程的等待與喚醒,并且有駁于MVC的設(shè)計鲫骗,好處是沒必要在重復繪制形成動畫結(jié)束后反復new Thread()犬耻。
//使用線程作為Controller
public class PoorThread extends Thread{
public void run() {
//針對不同的狀態(tài),進行不同的數(shù)值計算执泰,并刷新界面
}
//線程等待
public void waitPoorThread(){
if ("是否處于等待狀態(tài)") return;
synchronized (PoorThread.this){
try {
"是否處于等待狀態(tài)"= true;
//等待
PoorThread.this.wait();
} catch (InterruptedException e) {
"是否處于等待狀態(tài)"= false;
e.printStackTrace();
}
}
}
//線程喚醒
public void noitfyPoorThread(){
if (!"是否處于等待狀態(tài)") return;
synchronized (PoorThread.this){
//喚醒
PoorThread.this.notify();
"是否處于等待狀態(tài)"= false;
}
}
}
四枕磁,自下而上的實現(xiàn)
1,View層坦胶,寬高計算透典、樣式繪制、接收觸摸事件及釋放資源
寬高計算:
因為樣式核心是圓顿苇,為了省去不必要的麻煩峭咒,所以設(shè)定寬高相等(并不是必須相等)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//screenWid 為屏幕寬度
//screenHei 為屏幕高度
int widMod = MeasureSpec.getMode(widthMeasureSpec);
int heiMod = MeasureSpec.getMode(heightMeasureSpec);
//指定寬
if (widMod == MeasureSpec.EXACTLY){
//獲取指定寬
mWidth = MeasureSpec.getSize(widthMeasureSpec);
}
//指定最大寬度
else if (widMod == MeasureSpec.AT_MOST){
//如果屏幕屏幕寬度/3大于指定最大寬度,則取最大寬度
mWidth = MeasureSpec.getSize(widthMeasureSpec) < screenWid / 3 ? MeasureSpec.getSize(widthMeasureSpec) : screenWid / 3;
}
//未指定寬度
else if (widMod == MeasureSpec.UNSPECIFIED){
//則取屏幕寬度/3
mWidth = screenWid / 3;
}
//高度同上
if (heiMod == MeasureSpec.EXACTLY){
mHeight = MeasureSpec.getSize(heightMeasureSpec);
}else if (heiMod == MeasureSpec.AT_MOST){
mHeight = MeasureSpec.getSize(heightMeasureSpec) < screenHei / 3 ? MeasureSpec.getSize(heightMeasureSpec) : screenHei / 3;
}else if (heiMod == MeasureSpec.UNSPECIFIED){
mHeight = screenHei / 3;
}
//求出寬高后纪岁,需要加上內(nèi)邊距凑队,之后寬高取小(避免橫豎屏切換帶來的麻煩)幔翰,因為控件是正方形漩氨,變量mSize是為了使用方便
mSize = mHeight = mWidth = Math.min(mHeight + getPaddingTop() + getPaddingBottom(),mWidth + getPaddingLeft() + getPaddingRight());
setMeasuredDimension(mWidth,mHeight);
//計算出寬高后,計算Model中數(shù)據(jù)
initValue();
}
//初始化Model數(shù)據(jù)
private void initValue() {
//mSize = "控件寬高取小";
"初始化時外圓半徑"= mSize/3;
"長按狀態(tài)外圓半徑"= mSize/2;
"初始化時內(nèi)圓半徑"= mSize/4;
"長按狀態(tài)內(nèi)圓半徑" = mSize/6;
"繪制時使用的內(nèi)圓半徑"= "初始化時外圓半徑";
"繪制時使用的外圓半徑"= "初始化時內(nèi)圓半徑";
"進度條寬度"= mSize/25;
"進度條弧度"= 0;
}
樣式繪制(代碼里雙引號中的漢字并不是字符串遗增,而是指代某個變量):
View的繪制順序遵循后來居上叫惊,先畫的會被后畫的遮蓋。
//長按狀態(tài)下同初始化狀態(tài)一樣內(nèi)外圓還是存在的做修,只是多了進度條霍狰。
//對于進度條,簡單的實現(xiàn)方式是先畫實心弧饰及,再在弧上畫圓蔗坯,
//圓的顏色為背景色,圓的半徑小于弧的半徑燎含,
//這樣會遮蓋弧的內(nèi)部宾濒,只留外部的一圈,達到弧線的效果屏箍。
//這樣的實現(xiàn)方式無需考慮畫筆的寬度绘梦,也省去了計算寬高時的麻煩橘忱。
//在這個控件中,由底部到頂部的繪制順序是:
//1谚咬,外圓鹦付。 2,弧择卦。 3敲长,遮蓋弧的圓(與外圓顏色相等) 4,內(nèi)圓秉继。
//而進度條是在外圓拉伸結(jié)束祈噪、內(nèi)圓收縮結(jié)束后才出現(xiàn),所以需要加判斷
//1尚辑,外圓辑鲤。
//if(外圓拉伸結(jié)束、內(nèi)圓收縮結(jié)束) { 2杠茬,弧月褥。 3,遮蓋弧的圓(與外圓顏色相等) }
//4瓢喉,內(nèi)圓宁赤。
//畫外圓
mPaint.setColor("外圓的顏色");
canvas.drawCircle(
canvas.getWidth() / 2, //繪制在控件正中
canvas.getHeight() /2,
"繪制時使用的外圓半徑",
mPaint
);
//當前狀態(tài)為長按中,并且進度條弧度大于0栓票,才繪制進度條
if( "如果當前狀態(tài)為長按中" && "進度條弧度" > 0 ){
//畫弧
mPaint.setColor("進度條顏色");
canvas.drawArc(
//RectF 應為成員變量决左,在此簡寫
new RectF(canvas.getWidth()/2- "繪制時使用的外圓半徑",
canvas.getHeight()/2- "繪制時使用的外圓半徑" ,
canvas.getWidth()/2+ "繪制時使用的外圓半徑" ,
canvas.getHeight()/2+ "繪制時使用的外圓半徑" ),
-90.0f,
"進度條弧度",
true,
mPaint
);
//畫遮蓋弧線的圓
mPaint.setColor("外圓的顏色");
//畫在正中心,半徑為長按狀態(tài)下外圓最大半徑 - 進度條寬度走贪,也就是遮蓋的半徑
canvas.drawCircle(
canvas.getWidth() / 2,
canvas.getHeight() /2,
"長按狀態(tài)下外圓最大半徑" - "進度條寬度",
mPaint
);
}
//畫內(nèi)圓
mPaint.setColor("內(nèi)圓的顏色");
canvas.drawCircle(
canvas.getWidth() / 2, //繪制在控件正中
canvas.getHeight() /2,
"繪制時使用的內(nèi)圓半徑",
mPaint
);
觸摸事件處理:
主要任務(wù)是根據(jù)手勢修改控件所處的狀態(tài)佛猛。
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()){
//手指按下
case MotionEvent.ACTION_DOWN:
//如果不是初始狀態(tài),則直接返回坠狡,因為動畫需要時間继找,避免沖突
if("當前狀態(tài)" != StateMachine.STATE_INITIA){
return false;
}
//將狀態(tài)切換為短按
"當前狀態(tài)" = StateMachine.STATE_SHORT_CLICK;
//調(diào)用線程,250毫秒后運行逃沿,如果狀態(tài)還是短按而沒有抬手码荔,則將狀態(tài)切換為長按。
//(此Runnable應為成員變量感挥,在此簡寫)
postDelayed(new Runnable(){
@Override
public void run() {
//如果當前狀態(tài)為短按
if ("當前狀態(tài)"== StateMachine.STATE_SHORT_CLICK){
//切換為長按
"當前狀態(tài)" = StateMachine.STATE_LONG_CLICKING;
//喚醒數(shù)據(jù)處理線程
if (mPoorThread!= null){
mPoorThread.noitfyPoorThread();
}
//如果回調(diào)接口不為空,回調(diào)接口
if (mCallBackListener != null){
mCallBackListener.longClick();
}
}
}
},250);
break;
//手指移動
case case MotionEvent.ACTION_MOVE:
//如果外部傳入回調(diào)接口并且越败,當前狀態(tài)為長按中
if(mCallBackListener!= null
&& "當前狀態(tài)"== StateMachine.STATE_LONG_CLICKING){
//接口回調(diào)
mCallBackListener.move(event);
}
break;
//手指抬起
case MotionEvent.ACTION_UP:
//如果當前狀態(tài)為短按触幼,
if (mStateMachine == StateMachine.STATE_SHORT_CLICK){
//則更當前改狀態(tài)為短按抬起
"當前狀態(tài)"= StateMachine.STATE_SHORT_UP;
//如果傳入了回調(diào)接口
if (mCallBackListener != null){
//回調(diào)
mCallBackListener.shortClick();
}
//回調(diào)結(jié)束后,將當前狀態(tài)切換位初始化
"當前狀態(tài)"= StateMachine.STATE_INITIA;
}
//如果狀態(tài)為長按究飞,
if ("當前狀態(tài)"== StateMachine.STATE_LONG_CLICKING){
//則更改當前狀態(tài)為長按抬起
"當前狀態(tài)"= StateMachine.STATE_LONG_UP;
//回調(diào)結(jié)束
}
break;
}
}
釋放資源置谦,關(guān)閉循環(huán)線程堂鲤,清空成員變量。
@Override
protected void onDetachedFromWindow() {
//銷毀
mStateMachine = null;
if (mPaint != null){
mPaint.reset();
mPaint = null;
}
//mClickIntervalRunnable時判斷長短按的Runnable
if (mClickIntervalRunnable!=null){
mClickIntervalRunnable = null;
}
if (mCallBackListener != null){
mCallBackListener = null;
}
if (mPoorThread != null){
mPoorThread.finishPoorThread();
}
super.onDetachedFromWindow();
}
2媒峡,Controller層瘟栖,根據(jù)當前狀態(tài),修改數(shù)據(jù)及刷新界面
/**
* 控制數(shù)據(jù)谅阿,重復刷新控件
*/
private final class PoorThread extends Thread{
private static final String TAG = ProgressButtonView.TAG+".PoorThread";
//默認死循環(huán)
private boolean "線程開關(guān)"= true;
private boolean "是否處于等待狀態(tài)"= false;
/**
* 刷新間隔ms
*/
private int sleepTime = 13;
PoorThread(){
setName(TAG);
//因為線程實在初始化時new的半哟,當控件狀態(tài)為初始化時,會保持wait签餐,所以可以直接啟動寓涨。
start();
}
@Override
public void run() {
//默認死循環(huán),控件銷毀時停止
while ("線程開關(guān)"){
switch ("當前狀態(tài)"){
case STATE_INITIA://初始化
case STATE_SHORT_CLICK://短按
case STATE_SHORT_UP://短按抬起
//如果是初始化氯檐、短按戒良、短按抬起三種狀態(tài),則wait線程
waitPoorThread();
break;
//長按ing
case STATE_LONG_CLICKING:
//長按后冠摄,拉伸外圈糯崎,收縮內(nèi)圈,如果外圈半徑小于指定最大半徑河泳,或內(nèi)圈半徑大于指定最小半徑沃呢,則修改半徑數(shù)值,并刷新界面
if ("繪制時使用的外圓半徑"< "長按狀態(tài)外圓半徑"|| "繪制時使用的內(nèi)圓半徑"> "長按狀態(tài)內(nèi)圓半徑"){
//增加外圈半徑乔询,每次更改的數(shù)值相同
"繪制時使用的外圓半徑"+= ( "長按狀態(tài)外圓半徑"- "初始化時外圓半徑") / sleepTime;
//確保更改后的數(shù)值<=指定最大半徑樟插,如果大于,則賦值外圓最大半徑
"繪制時使用的外圓半徑"= "繪制時使用的外圓半徑"> "長按狀態(tài)外圓半徑"? "長按狀態(tài)外圓半徑": "繪制時使用的外圓半徑";
//減小內(nèi)圈半徑竿刁,每次更改的數(shù)值相同
"繪制時使用的內(nèi)圓半徑"+= ("長按狀態(tài)內(nèi)圓半徑"- "初始化時內(nèi)圓半徑") / sleepTime;
//確保更改后的數(shù)值>=指定最小半徑黄锤,如果小于,則賦值內(nèi)圈最小半徑
"繪制時使用的內(nèi)圓半徑"= "繪制時使用的內(nèi)圓半徑" < "長按狀態(tài)內(nèi)圓半徑"? "長按狀態(tài)內(nèi)圓半徑": "繪制時使用的內(nèi)圓半徑" ;
}
//如果當前外圈半徑==最大外圈半徑食拜,并且內(nèi)圈半徑==最小內(nèi)圈半徑鸵熟,則說明伸縮動畫結(jié)束
//如果弧線的角度小于360,則進度條還沒加載結(jié)束负甸,繼續(xù)加載進度條
else if ("進度條弧度"< 360){
//則增加弧線角度流强,確保每次更改數(shù)值相同
"進度條弧度"+= 360.0f / "進度條最長時間"* sleepTime;
//確保角度<=360
"進度條弧度"= "進度條弧度">360 ? 360 : "進度條弧度";
}
//拉伸動畫結(jié)束,弧線角度>=360呻待,則進度條加載結(jié)束
else if ("進度條弧度">= 360){
//狀態(tài)切換為進度條加載結(jié)束
"當前狀態(tài)"= StateMachine.STATE_PROGRESS_OVER;
}
break;
//長按抬手與進度條結(jié)束兩種狀態(tài)的處理方式一樣打月,收縮外圈,拉伸內(nèi)圈蚕捉,以動畫的形式過度到初始狀態(tài)
case STATE_PROGRESS_OVER://進度條加載結(jié)束
case STATE_LONG_UP://長按抬手
//進度條寬度為0奏篙,也就相當于不繪制
"進度條弧度"= 0;
//抬手或進度條結(jié)束,外圓收縮,內(nèi)圓拉伸秘通,判斷外圓是否大于初始值为严,內(nèi)院是否小于初始值,如果是肺稀,則更改數(shù)據(jù)
if ("繪制時使用的外圓半徑"> "初始化時外圓半徑"|| "繪制時使用的內(nèi)圓半徑" < "初始化時內(nèi)圓半徑"){
//確保外圓半徑每次更改的數(shù)值相同
"繪制時使用的外圓半徑"+= ("初始化時外圓半徑" - "長按狀態(tài)外圓半徑") / sleepTime;
//確保外圓半徑更改后的數(shù)值<=指定數(shù)值
"繪制時使用的外圓半徑" = "繪制時使用的外圓半徑" < "初始化時外圓半徑"? "初始化時外圓半徑": "繪制時使用的外圓半徑" ;
//確保每次更改的數(shù)值相同
"繪制時使用的內(nèi)圓半徑"+= ("初始化時內(nèi)圓半徑"- "長按狀態(tài)內(nèi)圓半徑" ) / sleepTime;
//確保更改后的數(shù)值>=指定數(shù)值
"繪制時使用的內(nèi)圓半徑"= "繪制時使用的內(nèi)圓半徑"> "初始化時內(nèi)圓半徑"? "初始化時內(nèi)圓半徑": "繪制時使用的內(nèi)圓半徑";
}
//伸縮動畫結(jié)束
else{
//如果接口不為空
if (mCallBackListener != null){
//如果當前狀態(tài)為進度條結(jié)束
if ("當前狀態(tài)"== StateMachine.STATE_PROGRESS_OVER){
//狀態(tài)為進度條結(jié)束第股,回調(diào)
mCallBackListener.progressOver();
}else{
//否則就是長按抬手,回調(diào)
mCallBackListener.longClickUp();
}
}
//如果內(nèi)外圓都恢復為初始大小话原,則將狀態(tài)切換為初始狀態(tài)
"當前狀態(tài)"= StateMachine.STATE_INITIA;
}
break;
}
try {
sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
//刷新界面
postInvalidate();
}
}
/**
* 銷毀線程
*/
public void finishPoorThread(){
"線程開關(guān)"= false;
noitfyPoorThread();
}
//線程等待
public void waitPoorThread(){
if ("是否處于等待狀態(tài)") return;
synchronized (PoorThread.this){
try {
"是否處于等待狀態(tài)"= true;
//等待
PoorThread.this.wait();
} catch (InterruptedException e) {
"是否處于等待狀態(tài)" = false;
e.printStackTrace();
}
}
}
//線程喚醒
public void noitfyPoorThread(){
if (!"是否處于等待狀態(tài)") return;
synchronized (PoorThread.this){
//喚醒
PoorThread.this.notify();
"是否處于等待狀態(tài)"= false;
}
}
}