做出一個SwitchButton的效果,並詳細學習一下onDraw(Canvas canvas)方法的使用
程式碼的靈感和原理主要來自於 http://blog.csdn.net/singwhatiwanna/article/details/9254309這篇文章!
1.效果
iphone上有開關控制元件,很漂亮,其實android4.0以後也有switch控制元件,但是隻能用在4.0以後的系統中,這就失去了其使用價值,而且我覺得它的介面也不是很好看。最近看到了百度魔拍上面的一個控制元件,覺得很漂亮啊,然後反編譯了下,儘管沒有混淆過,但是還是不好讀,然後就按照自己的想法寫了個,功能和百度魔拍類似。
效果圖入下:
2.原理
繼承自view類,override其onDraw函式,把兩個背景圖(一個灰的一個紅的)和一個開關圖(圓開關)通過canvas畫出來;同時override其onTouchEvent函式,實現滑動效果;最後開啟一個執行緒做動畫,實現緩慢滑動的效果。
3. 程式碼
程式碼SwitchButton如下:
佈局檔案的程式碼如下:package net.loonggg.switchbutton; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; public class SwitchButton extends View { public static final int SWITCH_OFF = 0;// 關閉狀態 public static final int SWITCH_ON = 1;// 開啟狀態 public static final int SWITCH_SCROLING = 2;// 滾動狀態 // 用於顯示的文字,預設為開啟和關閉 private String mOnText = "開啟"; private String mOffText = "關閉"; /** * SwitchButton切換開關的狀態,預設為關閉狀態 */ private int mSwitchStatus = SWITCH_OFF; private int mBmpWidth = 0; private int mBmpHeight = 0; private int mThumbWidth = 0; private OnSwitchChangedListener listener; private int mSrcX = 0, mDstX = 0; private boolean mHasScrolled = false;// 表示是否發生過滾動 // 開關狀態圖 Bitmap mSwitch_off, mSwitch_on, mSwitch_thumb; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); public SwitchButton(Context context) { this(context, null); } public SwitchButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public SwitchButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } /** * 初始化圖片 */ private void initView() { Resources res = getResources(); mSwitch_off = BitmapFactory.decodeResource(res, R.drawable.switch_off_bg); mSwitch_on = BitmapFactory.decodeResource(res, R.drawable.switch_on_bg); mSwitch_thumb = BitmapFactory.decodeResource(res, R.drawable.switch_thumb); mBmpWidth = mSwitch_on.getWidth(); mBmpHeight = mSwitch_on.getHeight(); mThumbWidth = mSwitch_thumb.getWidth(); } @Override public void setLayoutParams(LayoutParams params) { params.width = mBmpWidth; params.height = mBmpHeight; super.setLayoutParams(params); } /** * 設定開關上面的文字 * * @param onText * 控制元件開啟時要顯示的文字 * @param offText * 控制元件關閉時要顯示的文字 */ public void setText(String onText, String offText) { mOnText = onText; mOffText = offText; invalidate();// 使整個檢視無效,重新繪製,用來重新整理View } /** * 設定開關的狀態 * * @param flag * true為開,false為關閉 */ public void setStatus(boolean flag) { mSwitchStatus = flag ? SWITCH_ON : SWITCH_OFF; } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mSrcX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: mDstX = Math.max((int) event.getX(), 10); mDstX = Math.min(mDstX, 62); if (mSrcX == mDstX) return true; mHasScrolled = true; TranslateAnimationRunnable move_runnable = new TranslateAnimationRunnable( mSrcX, mDstX, 0); new Thread(move_runnable).start(); mSrcX = mDstX; break; case MotionEvent.ACTION_UP: if (mHasScrolled == false)// 如果沒有發生過滑動,就意味著這是一次單擊過程 { mSwitchStatus = Math.abs(mSwitchStatus - 1); int xFrom = 10, xTo = 62; if (mSwitchStatus == SWITCH_OFF) { xFrom = 62; xTo = 10; } TranslateAnimationRunnable runnable = new TranslateAnimationRunnable( xFrom, xTo, 1); new Thread(runnable).start(); } else { invalidate(); mHasScrolled = false; } // 狀態改變的時候 回撥事件函式 if (listener != null) { listener.onSwitchChanged(this, mSwitchStatus); } break; default: break; } return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 繪圖的時候 內部用到了一些數值的硬編碼,其實不太好, // 主要是考慮到圖片的原因,圖片周圍有透明邊界,所以要有一定的偏移 // 硬編碼的數值只要看懂了程式碼,其實可以理解其含義,可以做相應改進。 mPaint.setTextSize(14);// 設定畫筆文字大小 mPaint.setTypeface(Typeface.DEFAULT_BOLD);// 設定文字字型 if (mSwitchStatus == SWITCH_OFF) { drawBitmap(canvas, null, null, mSwitch_off); drawBitmap(canvas, null, null, mSwitch_thumb); mPaint.setColor(Color.rgb(105, 105, 105)); canvas.translate(mSwitch_thumb.getWidth(), 0); canvas.drawText(mOffText, 0, 20, mPaint); } else if (mSwitchStatus == SWITCH_ON) { drawBitmap(canvas, null, null, mSwitch_on); int count = canvas.save(); canvas.translate(mSwitch_on.getWidth() - mSwitch_thumb.getWidth(), 0); drawBitmap(canvas, null, null, mSwitch_thumb); mPaint.setColor(Color.WHITE); canvas.restoreToCount(count); canvas.drawText(mOnText, 17, 20, mPaint); } else { mSwitchStatus = mDstX > 35 ? SWITCH_ON : SWITCH_OFF; // 首先畫出開啟按鈕樣式 drawBitmap(canvas, new Rect(0, 0, mDstX, mBmpHeight), new Rect(0, 0, (int) mDstX, mBmpHeight), mSwitch_on); mPaint.setColor(Color.WHITE); canvas.drawText(mOnText, 17, 20, mPaint); // 在畫出關閉按鈕的樣式 int count = canvas.save(); canvas.translate(mDstX, 0); drawBitmap(canvas, new Rect(mDstX, 0, mBmpWidth, mBmpHeight), new Rect(0, 0, mBmpWidth - mDstX, mBmpHeight), mSwitch_off); canvas.restoreToCount(count); // 畫出關閉的文字 count = canvas.save(); // clipRect()擷取畫布中的一個區域,在這個區域的基礎上畫上關閉的文字 canvas.clipRect(mDstX, 0, mBmpWidth, mBmpHeight); canvas.translate(mThumbWidth, 0); mPaint.setColor(Color.rgb(105, 105, 105)); canvas.drawText(mOffText, 0, 20, mPaint); canvas.restoreToCount(count); // 畫出那個圓點按鈕 count = canvas.save(); canvas.translate(mDstX - mThumbWidth / 2, 0); drawBitmap(canvas, null, null, mSwitch_thumb); canvas.restoreToCount(count); } } /** * 畫出Bitmap * * @param canvas * @param src * @param dst * @param bitmap */ private void drawBitmap(Canvas canvas, Rect src, Rect dst, Bitmap bitmap) { dst = (dst == null ? new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()) : dst); Paint paint = new Paint(); canvas.drawBitmap(bitmap, src, dst, paint); } public interface OnSwitchChangedListener { /** * 狀態改變 回撥函式 * * @param status * SWITCH_ON表示開啟 SWITCH_OFF表示關閉 */ public abstract void onSwitchChanged(SwitchButton switchButton, int status); } /** * 為開關控制元件設定狀態改變監聽函式 * * @param listener */ public void setOnSwitchChangedListener(OnSwitchChangedListener listener) { if (listener != null) { this.listener = listener; } } /** * 改變控制元件狀態的動畫執行緒 * * @author loonggg * */ private class TranslateAnimationRunnable implements Runnable { private int srcX, dstX; private int duration; /** * 滑動動畫 * * @param srcX * 滑動起始點 * @param dstX * 滑動終止點 * @param duration * 是否採用動畫,1採用,0不採用 */ public TranslateAnimationRunnable(float srcX, float dstX, int duration) { this.srcX = (int) srcX; this.dstX = (int) dstX; this.duration = duration; } @Override public void run() { final int patch = (dstX > srcX ? 5 : -5); if (duration == 0) { SwitchButton.this.mSwitchStatus = SWITCH_SCROLING; SwitchButton.this.postInvalidate(); } else { int x = srcX + patch; while (Math.abs(x - dstX) > 5) { mDstX = x; SwitchButton.this.mSwitchStatus = SWITCH_SCROLING; SwitchButton.this.postInvalidate(); x += patch; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } mDstX = dstX; SwitchButton.this.mSwitchStatus = mDstX > 35 ? SWITCH_ON : SWITCH_OFF; SwitchButton.this.postInvalidate(); } } } }
<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="80dp" android:gravity="center_vertical" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:text="是否開啟藍芽" /> <net.loonggg.switchbutton.SwitchButton android:id="@+id/sb" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" /> </RelativeLayout>
到這裡控制元件的自定義生成就已經完了,當然咱的這篇文章還要學習一下onDraw()方法,對於onDraw()方法的使用,裡面的程式碼的解釋如下:
你看明白了嗎?其實對於這些東西解釋再明白也不如自己親手寫寫程式碼,一步步測試來的更明白!
我感覺還得進一步解釋一下int count = canvas.save()和canvas.restoreToCount(count),這兩個的作用就是和save和restore方法一樣。那麼它們到底有什麼用呢?請看下面的解釋:
在onDraw方法裡,我們經常會看到呼叫save和restore方法,它們到底是幹什麼用的呢?
save:用來儲存Canvas的狀態。save之後,可以呼叫Canvas的平移、放縮、旋轉、錯切、裁剪等操作。
restore:用來恢復Canvas之前儲存的狀態。防止save後對Canvas執行的操作對後續的繪製有影響。
save和restore要配對使用(restore可以比save少,但不能多),如果restore呼叫次數比save多,會引發Error。