1. 程式人生 > >做出一個SwitchButton的效果,並詳細學習一下onDraw(Canvas canvas)方法的使用

做出一個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。