Android里已經有足夠多的控件供開發者使用,但有時候我們還是會想要一些不一樣的東西,比如一些UI特效,比如一些3D動畫,今天就講講比較basic的東西:自定義控件。
1.效果圖
如果項目里需要一個通用的控件,然后UI給你這樣一個效果圖,你接下來會打算怎么做?
用戶可以按住拖動
點擊要切換的狀態,然后自動滑動到那一端
(本來是沒有這個效果圖的,又不想一張張貼不同的狀態,就畫了一下這個gif圖,關於怎么在ubuntu下畫gif圖,可以看一下下面這篇)
2.分析
看一下有沒有現成的widget,這似乎和android.widget.Switch有點類似,可是Swithc是水平的,水平沒有關系,改成垂直的問題不大,先來嘗試下好了,就先把背景和button的圖片換一下,來看一下結果是怎樣:
額。。。這個切換似乎生硬了點,沒有漸變的動畫。好吧,那還是重新自己寫一個控件吧。
3.創建Andriod自定義控件的步驟
怎么建立一個自定義的控件,說起來並不難,有三個內容需要實現:
3.1新建一個控件類,繼承android.view.View類:
1 public class XXXView extends View { 2 ... 3 protected void onDraw(Canvas canvas) { 4 ... 5 } 6 7 public boolean onTouchEvent(MotionEvent event) { 8 ... 9 } 10 11 public interface OnXXXListener { //狀態回調,同View.OnClickListener 12 public abstract void xxx(); 13 public abstract void xxx(); 14 } 15 }
3.2 在布局文件xml里使用這個控件:
<com.xxx.xxx.XXXView android:id=”@+id/xxx” android:layoutWidth=”...” android:layoutHeight=”...”> </com.xxx.xxx.XXXView>
3.3 在Activity類里獲得這個控件:
1 mXXXView = (XXXView) findViewById(R.id.xxx); 2 mXXXView.setListener(mXXXViewListener);
以上這簡單的3個步驟就是創建和使用控件的內容了,到這里,如果你是個喜歡着急寫代碼的人,你也可以先搭一個程序框架出來跑跑看啦。
4.考慮怎么畫?
4.1拖動
用戶需要能拖動Button,那也就是說我們在控件里需要捕獲用戶的touch event,知道用戶到底是做了什么動作(ACTION_DOWN, ACTION_MOVE, UP), 還有操作的位置在哪里(getX(), getY()).
這些信息從哪里可以知道?--》onTouchEvent()回調!
4.2動畫
動畫的本質就是圖片+位置+時間差。
在效果圖中,用戶也可以點擊一個狀態,讓控件滑動。那這個滑動的過程就是一個動畫的。
圖片我們有,那怎么把圖片畫到Canvas上?-》在onDraw()回調里面畫。在主線程里只要調用invalidate(),就會重新觸發onDraw()的執行。如果我們在一定的時間間隔,在不同的位置重新畫圖片,不就是動畫了?
位置可以從用戶行為獲得,或者自己計算;
時間差,在Android里面控制時間最容易的是什么?當然是Handler啦,因為它可以發送delay的消息。
4.3漸變的實現
效果圖中還有個漸變的過程,這個看起來好像蠻麻煩,其實也好辦。因為有Alpha的存在。我們可以在畫的時候根據不同的位置,設置Paint不同的Alpha值,一個圖片Alpha慢慢減小,另一個圖片Alpha慢慢增大。
ok,分析到這里,就大概知道該怎么做了,在onTouchEvent()回調里,獲得用戶的行為和位置,並記錄下來,在適當的時候發送Message給Handler,或者直接調用invalidate()重新畫。在Handler里,接收到信息,就根據當前的狀態,更新圖片下一個應該出現的位置,然后調用invalidate()觸發重新畫。
5.計算位置
ok,上面已經確定以什么方式做了,接下來就要用到一點點數學的計算了。我們要確定圖片從哪里開始動,動到哪里結束,還有在什么位置開始切換狀態。
先切下圖:
(文字也做成了圖片,其實凡是涉及到文字的都不應該做成圖片,如果有人切換到中文,然后他又不認識on off呢,而且這些文字應該要可設置的才對。這里圖方便就做成圖片了。)
然后就是一些重要坐標位置啦:
圖1 藍色是那個長條的圖片,綠色兩塊是在兩個狀態下Button所在的位置。
圖2 黃色的區域是兩個小的灰色文字圖片
圖3 這個區域就是文字開始切換的區域
6.偽代碼
現在方法也有了,數據也有了,就可以開始寫代碼了。
為了敘述方便,就用偽代碼代替了,下面是最重要的三個部分的偽碼:
處理用戶行為的邏輯:
1 public boolean onTouchEvent(MotionEvent event) { //處理用戶行為 2 case ACTION_DOWN: 3 if (坐標在圖1中藍色區域) { //touch在無效的區域 4 return; 5 } 6 7 if (坐標在圖1中綠色區域中Button在的區域) { //當前狀態是on,就是上面的區域,否則,就是下面的區域 8 獲得坐標與上邊緣的距離gap; 9 } else { 10 設置正在滑動標志; 11 設置動畫的方向,發送Message; //會執行到這里的情況是,比如當前狀態是on,用戶點擊了off那一端,那接下來控件就要自動滑動切換到off狀態。 12 } 13 break; 14 15 case ACTION_MOVE: 16 if (上次Down是在無效區域 | 正在切換狀態) { //此時不用響應Move動作。 17 return; 18 } 19 20 if (根據當前的坐標計算,滑塊將不在背景區域) { 21 return; 22 } 23 24 if (根據當前的坐標計算,在文字交換的區域) { 25 設置交換標記; 26 } 27 記錄滑塊當前位置; 28 invalidate(); 29 break; 30 31 case ACTION_UP: 32 if (上次Down是在無效區域 | 正在切換狀態) { //此時不用響應Up動作。 33 return; 34 } 35 36 取消交換標識; 37 if(根據當前坐標計算,最后的狀態是on) { 38 設置滑塊位置為on狀態時的位置; 39 修改狀態為on; 40 invalidate(); 41 } else { 42 設置滑塊位置為off狀態時的位置; 43 修改狀態為off; 44 invalidate(); 45 } 46 }
處理自動滑動:
1 private Handler mHandler = new Handler() { //用於處理自動滑動那部分邏輯 2 public void handleMessage(Message msg) { 3 if (計數 > 20) { 4 設置當前狀態; 5 設置滑塊的位置; 6 取消正在滑動的標志; 7 計數歸0; 8 return; 9 } 10 11 根據計數,獲得interpolator.getInterpolation;//這里用了AccelerateDecelerateInterpolator,讓動畫有一個加速的效果,其實這么短的距離效果看不出來。 12 計算滑塊的位置; 13 invalidate(); 14 計數+1; 15 sendMessageDelayed(0, 20); //20ms后畫下一幀。 16 } 17 18 };
畫:
1 protected void onDraw(Canvas canvas) { //具體畫的代碼 2 畫背景; 3 4 if (在狀態交換區域) { 5 根據滑塊位置這是Paint的Alpha值; 6 用上面設置的Paint畫那四個小圖; //在狀態交換的時候,四個小圖都是顯示的。 7 } else { 8 根據當前的狀態,畫on滑塊或off滑塊; 9 } 10 }
ok,有上面3部分的內容,基本上就可以了。
下面就是運行起來的效果,(不好表示啦,其實就是效果圖那樣的)
貼個對應的代碼段:
Handler:

1 private Handler mHandler = new Handler() { 2 @Override 3 public void handleMessage(Message msg) { 4 if (drawCount > 20) { 5 if (button_status == STATUS_OFF) { 6 button_status = STATUS_ON; 7 buttonY = buttonTopY; 8 if (listener != null) { 9 listener.slipToTop(); 10 } 11 } else { 12 button_status = STATUS_OFF; 13 buttonY = buttonBottomY; 14 if (listener != null) { 15 listener.slipToBottom(); 16 } 17 } 18 19 isTouchDownAnotherSide = false; 20 drawCount = 0; 21 return; 22 } 23 24 float p; 25 if (isToBottom) { 26 p = (float) (drawCount * 0.05); 27 } else { 28 p = (float) (1 - drawCount * 0.05); 29 } 30 float inter = interpolator.getInterpolation(p); 31 buttonY = buttonTopY + (buttonBottomY - buttonTopY) * inter; 32 33 if (buttonY >= exchangeBeginY && buttonY <= exchangeEndY) { 34 isExchange = true; 35 } else { 36 isExchange = false; 37 } 38 invalidate(); 39 drawCount++; 40 sendEmptyMessageDelayed(0, 20); 41 } 42 };
onDraw():

1 @Override 2 protected void onDraw(Canvas canvas) { 3 // TODO Auto-generated method stub 4 canvas.drawBitmap(mBackBitmap, 0, 0, null); 5 6 if (isExchange) { 7 // in exchange area, we should set alpha 8 Paint mPaint = new Paint(); 9 10 int alpha = (int) (255 - 255 * (buttonY - 25.5) / 50); 11 mPaint.setAlpha(alpha); 12 canvas.drawBitmap(mONBitmap, buttonTopX, buttonY, mPaint); 13 canvas.drawBitmap(mOFFTextBitmap, textBottomX, textBottomY, mPaint); 14 15 mPaint.setAlpha(255 - alpha); 16 canvas.drawBitmap(mOFFBitmap, buttonBottomX, buttonY, mPaint); 17 canvas.drawBitmap(mONTextBitmap, textTopX, textTopY, mPaint); 18 } else { 19 if (getNearLocation(0, buttonY) == STATUS_ON) { 20 canvas.drawBitmap(mONBitmap, buttonTopX, buttonY, null); 21 canvas.drawBitmap(mOFFTextBitmap, textBottomX, textBottomY, null); 22 } else { 23 canvas.drawBitmap(mOFFBitmap, buttonBottomX, buttonY, null); 24 canvas.drawBitmap(mONTextBitmap, textTopX, textTopY, null); 25 } 26 } 27 }
onTouchEvent():

1 @Override 2 public boolean onTouchEvent(MotionEvent event) { 3 // TODO Auto-generated method stub 4 5 float x = event.getX(); 6 float y = event.getY(); 7 switch (event.getAction()) { 8 case MotionEvent.ACTION_DOWN: 9 10 if (isTouchDownAnotherSide) { 11 return true; 12 } 13 14 // check if touch right place 15 if (isOutOfFrontBitmap(x, y)) { 16 isTouchDownValid = false; 17 return true; 18 } 19 20 if (listener != null) { 21 listener.touchedDown(); 22 } 23 24 if (isInFrontBitmap(x, y)) { 25 // touch in current mode 26 Log.e("Slip", "ACTION_DOWN : yes! infrontBitmap"); 27 isTouchDownValid = true; 28 touchDownGap = getGap(x, y); 29 } else { 30 // touch anther side 31 Log.e("Slip", "ACTION_DOWN : no! infrontBitmap"); 32 isTouchDownValid = false; 33 isTouchDownAnotherSide = true; 34 if (button_status == STATUS_ON) { 35 isToBottom = true; 36 mHandler.sendEmptyMessage(0); 37 } else { 38 isToBottom = false; 39 mHandler.sendEmptyMessage(0); 40 } 41 } 42 break; 43 case MotionEvent.ACTION_MOVE: 44 // if touch down wrong place, we ignore next action 45 if (!isTouchDownValid || isTouchDownAnotherSide) { 46 return true; 47 } 48 if (!isInBackBitmap(x, y)) { 49 Log.e("Slip", "ACTION_MOVE : no! isInBackBitmap"); 50 return true; 51 } 52 if (isInExchangeArea(x, y)) { 53 isExchange = true; 54 } else { 55 isExchange = false; 56 } 57 buttonY = y - touchDownGap; 58 this.invalidate(); 59 60 break; 61 case MotionEvent.ACTION_UP: 62 // if touch down wrong place, we ignore next action 63 if (!isTouchDownValid || isTouchDownAnotherSide) { 64 Log.e("Slip", "ACTION_UP : no! isTouchDownValid"); 65 return true; 66 } 67 68 isExchange = false; 69 70 if (getFinalLocation(x, y) == STATUS_ON) { 71 buttonY = buttonTopY; 72 if (button_status != STATUS_ON) { 73 button_status = STATUS_ON; 74 Log.e("Slip", "ACTION_UP : STATUS_ON! getFinalLocation"); 75 if (listener != null) { 76 listener.slipToTop(); 77 } 78 } else { 79 if (listener != null) { 80 listener.touchedUp(); 81 } 82 } 83 this.invalidate(); 84 85 } else { 86 buttonY = buttonBottomY; 87 if (button_status != STATUS_OFF) { 88 button_status = STATUS_OFF; 89 Log.e("Slip", "ACTION_UP : STATUS_OFF! getFinalLocation"); 90 if (listener != null) { 91 listener.slipToBottom(); 92 } 93 } else { 94 if (listener != null) { 95 listener.touchedUp(); 96 } 97 } 98 this.invalidate(); 99 } 100 101 break; 102 default: 103 break; 104 } 105 106 return true; 107 }
Over,Thanks.