最近重構(gòu)代碼,發(fā)現(xiàn)了之前偷懶遺留的一個問題。有一個控制設(shè)備開關(guān)機的控件,由于之前趕項目交期,匆匆忙忙直接在Activity中重寫onTouch事件,效果雖然也實現(xiàn)了,但是肯定不是很好的,今天重新將這個小玩意重新封裝成一個自定義控件,話不多說,先看看實現(xiàn)的效果。
這里寫圖片描述
其實看樣子都知道,是一個蠻簡單的自定義控件,至于為什么要寫這篇博客呢,因為也有段時間沒有搞自定義控件了,一時手癢,哈哈 = =,溫故而知新,總結(jié)一下總歸是有好處的。
既然是自定義控件,那么自然少不了自定義屬性,先貼上attrs.xml代碼。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SlideSwitchView">
<attr name="slide_button" format="reference"/>
<attr name="android:text"/>
<attr name="android:textSize"/>
<attr name="android:textColor"/>
</declare-styleable>
</resources>
這里定義了四個屬性,分別是文本內(nèi)容,文本字號,文本顏色以及滑動button的背景圖片,如果有拓展需求的話,也可以很簡單的拓展。
然后再看布局代碼, 簡單粗暴,就只有一個控件。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:swipe="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.zyw.horrarndoo.swipebutton.MainActivity">
<com.zyw.horrarndoo.swipebutton.SlideSwitchView
android:layout_centerInParent="true"
android:id="@+id/slide_switch_view"
swipe:slide_button="@mipmap/slide"
android:text = "Slide to power on"
android:textSize = "22sp"
android:textColor = "@android:color/holo_green_dark"
android:layout_width="wrap_content"
android:layout_height="60dp"/>
</RelativeLayout>
下面開始寫我們的自定義控件,寫代碼之前,不妨先擼一擼思路。
- 我們這個自定義控件跟現(xiàn)有的控件實際上是沒有什么聯(lián)系的,所以我們這里自定義控件繼承自View;
- 由于button是要隨手勢拖動的,所以我們肯定是要重寫onTouchEvent方法的,在onTouchEvent中動態(tài)的更新button的X坐標(biāo)值;
- 既然界面會涉及到重繪,所以我們肯定需要重寫onDraw方法,為了實現(xiàn)button拖動,這里我們通過drawBitmap的方法動態(tài)繪制button;
- 由于我們這個控件會放到各個界面中去用,我們肯定就需要重寫onMeasure方法,動態(tài)的確定button的寬高以及text的長度等;
- 外界需要知道控件滑動的結(jié)果,我們這里定義一個接口將狀態(tài)回調(diào)出去;
思路擼完,上代碼。
/**
* 自定義滑動開關(guān)
* <p>
* Created by Horrarndoo on 2017/6/1.
*/
public class SlideSwitchView extends View {
private Bitmap slideButtonBitmap; // 滑塊圖片
private Paint mPaint; // 畫筆
private float currentX; //當(dāng)前滑動的x坐標(biāo)
private int mBaseLineY; // text基準(zhǔn)線
private String mTextContent; //text內(nèi)容
public SlideSwitchView(Context context) {
this(context, null);
}
public SlideSwitchView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideSwitchView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPaint();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideSwitchView);
setSlideButtonResource(ta.getResourceId(R.styleable.SlideSwitchView_slide_button, -1));
setText(ta.getString(R.styleable.SlideSwitchView_android_text));
setTextSize(ta.getDimension(R.styleable.SlideSwitchView_android_textSize,30));
setTextColor(ta.getColor(R.styleable.SlideSwitchView_android_textColor, Color.BLACK));
ta.recycle();
}
/**
* 初始化畫筆
*/
private void initPaint() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setAntiAlias(true);
}
/**
* 初始化text居中基準(zhǔn)線
*/
private void initTextBaseLine() {
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float top = fontMetrics.top;//為基線到字體上邊框的距離,即上圖中的top
float bottom = fontMetrics.bottom;//為基線到字體下邊框的距離,即上圖中的bottom
mBaseLineY = (int) (getMeasuredHeight() / 2 - top / 2 - bottom / 2);//基線中間點的y軸計算公式
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
int newWidth = (int) (slideButtonBitmap.getWidth() * 2 + getTextWidth());
if (width >= newWidth)
width = newWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
if (height < slideButtonBitmap.getHeight()) {
// 獲得圖片的寬高
int widthSlide = slideButtonBitmap.getWidth();
int heightSlide = slideButtonBitmap.getHeight();
float scaleHeight = height * 1.0f / slideButtonBitmap.getHeight();
Matrix matrix = new Matrix();
matrix.postScale(scaleHeight, scaleHeight);
slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
heightSlide, matrix, true);
invalidate();
}
}
if (slideButtonBitmap.getWidth() > (width - getTextWidth()) / 2) {
// 獲得圖片的寬高
int widthSlide = slideButtonBitmap.getWidth();
int heightSlide = slideButtonBitmap.getHeight();
float scaleWidth = (width - getTextWidth()) / 2 / slideButtonBitmap.getWidth();
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleWidth);
slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
heightSlide, matrix, true);
invalidate();
}
setMeasuredDimension(width, slideButtonBitmap.getHeight());
initTextBaseLine();
}
// Canvas 畫布, 畫板. 在上邊繪制的內(nèi)容都會顯示到界面上.
@Override
protected void onDraw(Canvas canvas) {
// 1. 繪制text
canvas.drawText(mTextContent, slideButtonBitmap.getWidth(), mBaseLineY, mPaint);
// 2. 繪制滑塊
if (isTouchMode) {
// 根據(jù)當(dāng)前用戶觸摸到的位置畫滑塊
// 讓滑塊向左移動自身一半大小的位置
float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;
int maxLeft = getMeasuredWidth() - slideButtonBitmap.getWidth();
// 限定滑塊范圍
if (newLeft < 0) {
newLeft = 0; // 左邊范圍
} else if (newLeft > maxLeft) {
newLeft = maxLeft; // 右邊范圍
}
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, mPaint);
} else {
//還原button位置
canvas.drawBitmap(slideButtonBitmap, 0, 0, mPaint);
}
}
boolean isTouchMode = false;
private OnSwitchStateUpdateListener onSwitchStateUpdateListener;
// 重寫觸摸事件, 響應(yīng)用戶的觸摸.
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouchMode = true;
currentX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
currentX = event.getX();
break;
case MotionEvent.ACTION_UP:
isTouchMode = false;
currentX = event.getX();
float center = getMeasuredWidth() / 2.0f;
// 根據(jù)當(dāng)前按下的位置, 和控件中心的位置進行比較.
boolean isStateChanged = currentX > center;
// 如果開關(guān)狀態(tài)變化了, 通知界面
if (isStateChanged && onSwitchStateUpdateListener != null) {
onSwitchStateUpdateListener.onStateUpdate();
}
break;
default:
break;
}
// 重繪界面
invalidate(); // 會引發(fā)onDraw()被調(diào)用, 里邊的變量會重新生效.界面會更新
return true; // 消費了用戶的觸摸事件, 才可以收到其他的事件.
}
/**
* 設(shè)置滑塊圖片資源
*
* @param slideButton 滑塊圖片資源
*/
public void setSlideButtonResource(int slideButton) {
slideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
}
/**
* 設(shè)置text字號大小
*
* @param textSize text字號大小
*/
public void setTextSize(float textSize) {
mPaint.setTextSize(textSize);
mPaint.setStrokeWidth(textSize / 15.f);
}
/**
* 設(shè)置text內(nèi)容
*
* @param text text內(nèi)容
*/
public void setText(String text) {
mTextContent = text;
}
/**
* 設(shè)置text顏色
*
* @param color text顏色資源
*/
public void setTextColor(int color) {
mPaint.setColor(color);
}
/**
* 獲取text文字寬度
*
* @return text文字寬度
*/
private float getTextWidth() {
return mPaint.measureText(mTextContent);
}
/**
* 獲取text文字高度
*
* @return text文字高度
*/
private float getTextHeight() {
return mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top;
}
public interface OnSwitchStateUpdateListener {
// 狀態(tài)回調(diào)
void onStateUpdate();
}
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
}
這里我們主要看看onTouchEvent方法和onMeasure方法。
在onTouchEvent中,我們在ACTION_MOVE中不斷的刷新button的X坐標(biāo),然后刷新button的位置,達到拖動button滑動的效果 ,最后在ACTION_UP中判斷滑動位置是否超過控件一半位置來確定是否傳遞滑動狀態(tài)變化給回調(diào)。
在onMeasure中,我們計算了幾種情況。
- 首先要保證寬度為wrap_content的時候,控件要有滑動的空間,所以在wrap_content的時候,設(shè)置控件寬度值為button.width*2+text.width,保證控件最低的寬度值。
- 控件高度確定的情況下,button的寬高也根據(jù)控件寬高做相應(yīng)比例的縮放,避免圖片超出控件范圍。
- 最后要計算圖片寬度,保證view.width = button.width*2+text.width。
最后一步,在Activity中調(diào)用。
public class MainActivity extends AppCompatActivity {
private SlideSwitchView mSlideSwitchView;
private boolean mIsPowerOn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSlideSwitchView = (SlideSwitchView) findViewById(R.id.slide_switch_view);
mSlideSwitchView.setOnSwitchStateUpdateListener(new SlideSwitchView.OnSwitchStateUpdateListener() {
@Override
public void onStateUpdate() {
mIsPowerOn = !mIsPowerOn;
String content = mIsPowerOn ? "Slide to power off" : "Slide to power on";
mSlideSwitchView.setText(content);
}
});
}
}
附上完整demo地址:https://github.com/Horrarndoo/SwipeButton