下載地址:https://github.com/ChenSiLiang/Android.git
最近在做界面,發現Android自帶的開關控件有點醜,好看的一個還需要比較高的SDK版本。
所以,打算寫了一個開關控件。
發現微信的開關比較簡潔大方,與Android的扁平風格相宜得章。
拋磚引玉,與君共勉。
思路:
1.使用一張圖片作爲小滑塊,然後以小滑塊的雙倍長度作爲描繪的底部(添加偏移量作爲填補縫隙),開和關的背景顏色可以自定義。如果用圖片作爲底部感覺更加損耗性能,感覺不需要這樣做。
2.大家注意到滑動的時候背景的顏色是會根據滑動的距離變化的。這個細節我覺得應該可以只描繪一層底部就可以了。但是因爲灰色和綠色的值的轉化之間與滑動距離的關係比較難找,鄙人數學基礎可差可差了,可恥大一的時候高數沒學好。
耍了個小聰明:),描繪了兩層,底層是灰色的,第二層是綠色的(最上面是小滑塊)。滑動的距離與第二層的透明度成正比關係。這樣滑動的時候就可以看見灰色和綠色的轉換了。如果有大神請告訴LZ如何用一層實現這個效果。
註釋寫得比較清楚。
有很多地方當時寫的時候沒注意容易吃大虧。
public class CsSwitch extends CheckBox {
/**
* 是否打開
*/
private boolean mChecked = false;
/**
* 是否正在滑動
*/
private boolean mIsMoving = false;
/**
* 是否正在顯示動畫效果
*/
private boolean mAnimating = false;
/**
* 確保開關只激活一次
*/
private boolean mBroadcasting = false;
/**
* 底部圖片高
*/
private int mThumbHeight = 0;
/**
* 畫布高度
*/
private int mMeasuredHeight = 0;
/**
* 底部圖片寬
*/
private int mSwtichWidth = 0;
/**
* 小滑塊的寬度
*/
private int mThumbWidth = 0;
/**
* 點擊範圍
*/
private int mTouchSlop = 0;
private static final int DEFAULT_HEIGHT = 46;
/**
* 點擊事件過時的時間
*/
private long mClickTimeout = 0;
/**
* 首次按下的x軸座標
*/
private float mFristDownX = 0;
/**
* 首次按下的Y軸座標
*/
private float mFirstDownY = 0;
/**
* 小滑塊的初始位置
*/
private float mInitPos = 0;
/**
* 小滑塊當前x軸位置
*/
private float mCurXPos = 0;
/**
* 小滑塊爲開時的位置
*/
private float mOnPos = 0;
/**
* 小滑塊爲關時候的位置
*/
private float mOffPos = 0;
/**
* 動畫的矢量速度
*/
private float mAnimatorVelocity = 0;
/**
* 動畫位置
*/
private float mAnimationPosition = 0;
/**
* 矢量速度
*/
private float mVelocity = 10;
/**
* 小滑塊圖片
*/
private Bitmap mThumbBitmap = null;
/**
* 小滑塊資源id
*/
private int mThumbResource = 0;
/**
* 父控件
*/
private ViewParent mParent = null;
/**
* 畫筆
*/
private Paint mPaint = null;
/**
* 點擊事件
*/
private OnClickListener mOnClickListener;
/**
* 狀態改變監聽器
*/
private OnCheckedChangeListener mOnCheckedChangeListener = null;
/**
* 底部矩形
*/
private RectF mSwitchFRect = null;
/**
* 默認綠色
*/
private static int sDefaultColorGreen = 0;
/**
* 自定義綠色
*/
private int mCheckedColor = 0;
/**
* 默認灰色
*/
private static int sDefaultColorGray = 0;
/**
* 自定義灰色
*/
private int mUncheckedColor = 0;
/**
* X軸增量
*/
private static final int X_OFFSET = 6;
/**
* 一半X軸增量
*/
private static final int HALF_X_OFFSET = X_OFFSET / 4;
/**
* Y軸增量
*/
private static final int Y_OFFSET = 4;
/**
* 一半Y軸增量
*/
private static final int HALT_Y_OFFSET = Y_OFFSET / 2;
/**
* 透明度的最大值
*/
private static final int MAX_ALPHA = 255;
/**
* 圓角矩形的弧度
*/
private static final float RECF_RADIUS = 13;
/**
* 開關狀態改變的延遲時間
*/
private static final long CHECKED_DELAY_TIME = 10;
/**
* 調用點擊時間的Runnable對象
*/
private PerformClick mPerformClick = null;
/**
* 記錄滑動是否打開
*/
private boolean mTurningOn = false;
public boolean isChecked() {
return mChecked;
}
public OnCheckedChangeListener getOnCheckedChangeListener() {
return mOnCheckedChangeListener;
}
public void setOnCheckedChangeListener(OnCheckedChangeListener Listener) {
this.mOnCheckedChangeListener = Listener;
}
/**
* 初始化時調用
*/
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
Log.e("sysout", "setChecked:" + checked);
mPaint.setColor(checked ? mCheckedColor : mUncheckedColor);
if (mBroadcasting) {
return;
}
// 設置調用onCheckedChanged時的狀態
super.setChecked(checked);
// 初始化時會給mBroadcasting附真值
mBroadcasting = true;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(CsSwitch.this,
checked);
}
mBroadcasting = false;
}
}
public OnClickListener getOnClickListener() {
return mOnClickListener;
}
public void setOnClickListener(OnClickListener onClickListener) {
this.mOnClickListener = onClickListener;
}
public CsSwitch(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
/**
* 初始化
*
* @param context
*/
private void init(Context context, AttributeSet attrs, int defStyle) {
Resources resources = context.getResources();
// 顏色
sDefaultColorGreen = resources.getColor(R.color.open_green);
sDefaultColorGray = resources.getColor(R.color.closed_gray);
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.CsSwitch, defStyle, 0);
mCheckedColor = ta.getColor(R.styleable.CsSwitch_checked_color,
sDefaultColorGreen);
mUncheckedColor = ta.getColor(R.styleable.CsSwitch_unchecked_color,
sDefaultColorGray);
mThumbResource = ta.getResourceId(R.styleable.CsSwitch_thumb,
R.drawable.ic_switch_thumb);
// 小滑塊圖片
mThumbBitmap = BitmapFactory.decodeResource(resources, mThumbResource);
mMeasuredHeight = mThumbBitmap.getHeight();
// 得到小滑塊圖片的寬高
mThumbHeight = DEFAULT_HEIGHT;
mThumbWidth = mThumbBitmap.getWidth();
mSwtichWidth = mThumbWidth * 2 + X_OFFSET;
// 記錄打開位置
mOnPos = mSwtichWidth / 2;
// 點擊範圍
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 點擊事件
mClickTimeout = ViewConfiguration.getPressedStateDuration()
+ ViewConfiguration.getTapTimeout();
// 開關底部矩形
mSwitchFRect = new RectF(0, 0, mSwtichWidth, mThumbHeight + Y_OFFSET);
// 設置畫筆
mPaint = new Paint();
mPaint.setColor(mChecked ? mCheckedColor : mUncheckedColor);
mPaint.setStyle(Style.FILL_AND_STROKE);
mPaint.setDither(true);
// 使用抗鋸齒
mPaint.setAntiAlias(true);
mPaint.setFilterBitmap(true);
ta.recycle();
}
public CsSwitch(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.checkboxStyle);
}
public CsSwitch(Context context) {
this(context, null);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
}
@Override
protected void onDraw(Canvas canvas) {
// 在畫布上繪畫圖形
if (mIsMoving) {
mPaint.setColor(mUncheckedColor);
canvas.drawRoundRect(mSwitchFRect, RECF_RADIUS, RECF_RADIUS, mPaint);
mPaint.setColor(mCheckedColor);
double absPos = Math.min(Math.abs(mCurXPos), mThumbWidth);
int alpha = (int) (absPos / mThumbWidth * MAX_ALPHA);
mPaint.setAlpha(alpha);
canvas.drawRoundRect(mSwitchFRect, RECF_RADIUS, RECF_RADIUS, mPaint);
mPaint.setAlpha(MAX_ALPHA);
canvas.drawBitmap(mThumbBitmap, mCurXPos + HALF_X_OFFSET,
HALT_Y_OFFSET, mPaint);
} else {
canvas.drawRoundRect(mSwitchFRect, RECF_RADIUS, RECF_RADIUS, mPaint);
canvas.drawBitmap(mThumbBitmap, mCurXPos + HALF_X_OFFSET,
HALT_Y_OFFSET, mPaint);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 測量控件的最大寬高,裁定畫布的最大寬高
setMeasuredDimension(mSwtichWidth + X_OFFSET,
Math.max(mMeasuredHeight + Y_OFFSET, mThumbHeight + Y_OFFSET));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
// x,y距離
int disX = (int) (x - mFristDownX);
int disY = (int) (y - mFirstDownY);
// x,y絕對值
int absDisX = Math.abs(disX);
int absDisY = Math.abs(disY);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsMoving = false;
mFristDownX = x;
mFirstDownY = y;
mParent = getParent();
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(true);
}
mInitPos = mChecked ? mOnPos : mOffPos;
break;
case MotionEvent.ACTION_MOVE:
mIsMoving = true;
mCurXPos = mInitPos + event.getX() - mFristDownX;
// 超出按鈕位置時小滑塊的位置
if (mCurXPos >= mOnPos) {
// 開
mCurXPos = mOnPos;
mPaint.setColor(mCheckedColor);
} else if (mCurXPos <= 0) {
// 關
mCurXPos = mOffPos;
mPaint.setColor(mUncheckedColor);
}
mTurningOn = mCurXPos > ((mOnPos - mOffPos) / 3);
break;
case MotionEvent.ACTION_UP:
mIsMoving = false;
long time = event.getEventTime() - event.getDownTime();
// 響應點擊事件
if (absDisX < mTouchSlop && absDisY < mTouchSlop
&& time < mClickTimeout) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
} else {
startAnimator(!mTurningOn);
}
break;
default:
break;
}
// 重繪
invalidate();
return isEnabled();
}
private class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
@Override
public boolean performClick() {
startAnimator(mChecked);
if (mOnClickListener != null) {
mOnClickListener.onClick(CsSwitch.this);
}
return true;
}
/**
* 延遲設定開關值,保證動畫流暢
*
* @param checked
*/
private void setCheckedDelay(final boolean checked) {
postDelayed(new Runnable() {
@Override
public void run() {
setChecked(checked);
}
}, CHECKED_DELAY_TIME);
}
private void stopAnimator() {
mAnimating = false;
}
/**
* 動畫+設置開關值.
*
* @param checked
* 開關值
*/
private void startAnimator(final boolean checked) {
// 用更平滑的動畫過渡
mAnimating = true;
mAnimationPosition = mCurXPos;
mAnimatorVelocity = checked ? -mVelocity : mVelocity;
new SwitchAnimator().run();
}
/**
* 點擊開關動畫
*/
private class SwitchAnimator implements Runnable {
@Override
public void run() {
if (!mAnimating) {
return;
}
mAnimationPosition += mAnimatorVelocity;
if (mAnimatorVelocity > 0) {
// 打開
mPaint.setColor(mCheckedColor);
} else {
// 關閉
mPaint.setColor(mUncheckedColor);
}
if (mAnimationPosition >= mOnPos) {
stopAnimator();
mAnimationPosition = mOnPos;
setCheckedDelay(true);
} else if (mAnimationPosition <= 0) {
stopAnimator();
setCheckedDelay(false);
mAnimationPosition = mOffPos;
}
mCurXPos = mAnimationPosition;
invalidate();
// 循環播放動畫
SwitchAnimatorController.sendMsg(this);
}
}
下面是動畫實現:
/**
* 動畫控制循環播放
*/
public class SwitchAnimatorController {
private static AnimatorHandler mHandler = new AnimatorHandler();
private static final long DELAY_TIME = 10;
public static void sendMsg(Runnable runnable) {
Message msg = new Message();
msg.obj = runnable;
mHandler.sendMessageDelayed(msg, DELAY_TIME);
}
private static class AnimatorHandler extends Handler {
@Override
public void handleMessage(Message msg) {
if (msg.obj != null) {
((Runnable) msg.obj).run();
}
}
}
}
因爲是做成庫文件的形式,可以在佈局文件裏這樣使用:
<com.csl.myswitch.CsSwitch
xmlns:yourname="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
yourname:checked_color
yourname:unchecked_color
yourname:thumb
這樣可以聲明開關爲打開/關閉時背景顏色,
小滑塊的圖片等。
因爲是用小滑塊的寬度*2作爲底部的,用奇奇怪怪的圖片概不負責~