仿Ios滑動開關按鈕
效果圖
實現思路
外層是一個圓角矩形,內層則是一個小圓形,如果細細觀看,會發現外層還是有一層灰色的邊框的,至少滑動的動畫,我們可以考慮Scroller或者其他的動畫效果。
具體程式碼
這裡我們使用了自定義屬性,所以要在values目錄下建立一個attrs.xml檔案,
<attr name="onBackground" format="reference"></attr> 開啟時背景的顏色 <attr name="offBackground" format="reference"></attr> 關閉時背景的顏色 <attr name="circleColor" format="reference"></attr> 圓形的顏色 <attr name="time" format="integer"></attr> 動畫執行的時間 <attr name="layout_width" format="dimension"></attr> 預設寬度 <attr name="layout_height" format="dimension"></attr> 預設高度
自定義一個控制元件這裡我們就叫ButtonView,可以繼承於Button也可以繼承於View,這裡我們就繼承自View,正好複習一個看看Android內部如何實現的單擊事件,看一下其原理。
重寫第建構函式,這裡重寫建構函式我習慣重寫第三個建構函式,但是遇到一些問題,記錄一下,避免大家犯同樣的錯誤。
例如
public ButtonView(Context context) { this(context,null); } public ButtonView(Context context, AttributeSet attrs) { this(context, attrs,-1); //初始化的一些程式碼 } public ButtonView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
這麼寫本身是沒問題,執行起來效果也是沒問題的,但是當我們使用findViewById()的話獲取到的則是null,其實這裡只需要把-1改成0即可,至於為什麼,大家可以參考一下Android的原始碼。這裡我就直接重寫第二個構造函數了。
public ButtonView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.buttonView);//載入自定義的屬性 offBackground=a.getColor(R.styleable.buttonView_offBackground, Color.parseColor("#F6F6F6"));//獲取關閉的顏色並設定預設值 onBackground=a.getColor(R.styleable.buttonView_onBackground, Color.parseColor("#38DA4E"));//獲取開啟的顏色並設定預設值 time=a.getInteger(R.styleable.buttonView_time, 600);//時間這裡代表的是毫秒數 circleColor=a.getColor(R.styleable.buttonView_circleColor, Color.parseColor("#D7D7D7"));//圓形顏色 layout_height=a.getDimension(R.styleable.buttonView_layout_height, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics()));//預設高度 layout_width=a.getDimension(R.styleable.buttonView_layout_width, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, context.getResources().getDisplayMetrics()));//預設寬度 mPaint=new Paint();//對畫筆進行一些初始化 mConfig=ViewConfiguration.get(context);//ViewConfiguration內有一個Android內部的初始值,比如多長時間是單擊時間,多長時間是長按時間。 }
我們現在需要重寫onMeasure方法,測量一下其寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode= MeasureSpec.getMode(heightMeasureSpec);
int heightSize= MeasureSpec.getSize(heightMeasureSpec);
int widthMode= MeasureSpec.getMode(widthMeasureSpec);
int widthSize= MeasureSpec.getSize(widthMeasureSpec);
int measureView_width = measureView(widthMode, widthSize, layout_width);
int measureView_height= measureView(heightMode, heightSize, layout_height);
//計算最大移動位置
max_move=(int) (layout_width-RADIO);
//計算最小移動位置
min_move=(int)(circleX+layout_height/2-4);
setMeasuredDimension(measureView_width, measureView_height);
}
private int measureView(int mode,int size,float defaultSize){
switch (mode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
if(size>defaultSize){
size=(int) Math.ceil(defaultSize);
}
return MeasureSpec.makeMeasureSpec(size,MeasureSpec.EXACTLY);
case MeasureSpec.EXACTLY:
return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
return -1;
}
int min_move=0;最小移動的位置
int max_move=0;最大移動的位置
int current_move=min_move;當前移動的位置
int currentColor=offBackground;當前漸變的顏色
static final int RADIO=30;預設矩形圓角大小
int circleX=0;圓形X軸
這裡要說一下MeasureSpec內部有三個值
- UNSPECIFIED 子控制元件想多大就多大
- AT_MOST 在最大範圍內,或者設定了warp_content
- EXACTLY 設定了固定值,或者Match_Parent
而makeMeasureSpec()根據提供的大小值和模式建立一個測量值
setMeasuredDimension需要的就是兩個測量值作為引數。
重寫onDraw方法進行繪製內容
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
mPaint.reset();//重置畫筆
mPaint.setAntiAlias(true);//設定抗鋸齒
mPaint.setColor(currentColor);//設定畫筆顏色
mPaint.setDither(true);//設定防抖動
canvas.drawRoundRect(new RectF(0, 0, layout_width, layout_height), RADIO, RADIO, mPaint);//繪製圓角矩形
mPaint.setStyle(Style.STROKE);//繪製邊框
mPaint.setColor(Color.parseColor("#DADADA"));//設定邊框的顏色
canvas.drawRoundRect(new RectF(0, 0, layout_width, layout_height), RADIO, RADIO, mPaint);//繪製矩形
mPaint.setStyle(Style.FILL);//設定填充效果
mPaint.setColor(circleColor);//設定圓形顏色
canvas.drawCircle(current_move, layout_height/2, layout_height/2-4, mPaint);//繪製圓
}
單擊事件
這樣基本上就已經成型了,下面我們品位一下Android的單擊事件如何形成的。
float x=0;
float y=0;
boolean isClick=false;
Handler mHandler=new Handler();
ClickRunAble clickRunable=new ClickRunAble();
class ClickRunAble implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
isClick=true;
}
}
private void isMove(float x,float y,int touchSlop){
if(Math.abs(this.x-x)>touchSlop||Math.abs(this.y-y)>touchSlop){
isClick=false;
}
}
上面程式碼執行進行準備工作,手按下然後鬆開即單擊事件,我們重寫onTouchEvent(MotionEvent event)
按下時的動作
x=event.getX();//獲取按下的X軸
y=event.getY();//獲取按下的Y軸
isClick=false;//預設不是單擊
mHandler.postDelayed(clickRunable, mConfig.getScaledTouchSlop());//指定多長時間後 執行ClickRunAble內部程式碼,其實很簡單就把把isClick設定成true
移動時的動作
float x=event.getX();
float y=event.getY();
isMove(x, y, mConfig.getScaledTouchSlop());如果移動距離大於ScaledTouchSlop則不是單擊事件。
鬆開時
mHandler.removeCallbacks(clickRunable);
if(isClick){
//單擊事件所要執行的程式碼。。。
}
即可。
動畫效果
這裡動畫效果採用Scoller明顯不是很好,我們需要不變變化兩個值:顏色和位置
這裡我們採用屬性動畫
//定義我們要操作的兩個屬性
private Property<ButtonView, Integer> mColor=new Property<ButtonView, Integer>(Integer.class,"currentColor") {
@Override
public Integer get(ButtonView object) {
return object.currentColor;
}
@Override
public void set(ButtonView object, Integer value) {
// TODO Auto-generated method stub
object.currentColor=value;
invalidate();
}
};
private Property<ButtonView, Integer> mMove=new Property<ButtonView, Integer>(Integer.class,"current_move") {
@Override
public Integer get(ButtonView object) {
// TODO Auto-generated method stub
return object.current_move;
}
@Override
public void set(ButtonView object, Integer value) {
// TODO Auto-generated method stub
object.current_move=value;
}
};
//開啟動畫
public void startAnimator(int start,int end,int startColor,int endColor,int duration){
set=new AnimatorSet();
set.playTogether(ObjectAnimator.ofInt(this,mMove, start,end),ObjectAnimator.ofObject(this, mColor, new ArgbEvaluator(),startColor,endColor));
set.setDuration(duration);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// TODO Auto-generated method stub
isAnimatorStart=true;
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimatorStart=false;
if(changeListener!=null){
changeListener.onChange(isOn);//當動畫完畢後,我們希望開啟關閉都能做一些時間,此函式進行回撥
}
}
});
set.setInterpolator(new DecelerateInterpolator());
set.start();
}
上述對關鍵程式碼進行了梳理,具體請看原始碼。
另外這裡使用了屬性動畫,在Android3.0之前是沒有效果的,這裡可以使用nineoldandroids.jar相容3.0之前的版本。
此Dome還有一些地方可以優化。