1. 程式人生 > >仿Ios滑動開關按鈕

仿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內部有三個值

  1. UNSPECIFIED 子控制元件想多大就多大
  2. AT_MOST 在最大範圍內,或者設定了warp_content
  3. 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還有一些地方可以優化。