Android中实现iPhone开关
阅读原文时间:2023年07月09日阅读:3

前一段时间在做项目的时候遇到了一个问题,美工在设计的时候设计的是一个iPhone中的开关,但是都知道Android中的Switch开关和IOS中的不同,这样就需要通过动画来实现一个iPhone开关了。

通常我们设置界面采用的是PreferenceActivity

package me.imid.movablecheckbox;

import android.os.Bundle;
import android.preference.PreferenceActivity;

public class MovableCheckboxActivity extends PreferenceActivity {

@Override  
public void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    addPreferencesFromResource(R.xml.testpreference);  
}  

}

有关PreferenceActivity请看:http://blog.csdn.net/dawanganban/article/details/19082949

我们的基本思路是将CheckBox自定义成我们想要的样子,然后再重写CheckBoxPreference将自定义的CheckBox载入。

1、重写CheckBox

package me.imid.view;

import me.imid.movablecheckbox.R;

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.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.CheckBox;

public class SwitchButton extends CheckBox {
private Paint mPaint;

private ViewParent mParent;

private Bitmap mBottom;

private Bitmap mCurBtnPic;

private Bitmap mBtnPressed;

private Bitmap mBtnNormal;

private Bitmap mFrame;

private Bitmap mMask;

private RectF mSaveLayerRectF;

private PorterDuffXfermode mXfermode;

private float mFirstDownY; // 首次按下的Y

private float mFirstDownX; // 首次按下的X

private float mRealPos; // 图片的绘制位置

private float mBtnPos; // 按钮的位置

private float mBtnOnPos; // 开关打开的位置

private float mBtnOffPos; // 开关关闭的位置

private float mMaskWidth;

private float mMaskHeight;

private float mBtnWidth;

private float mBtnInitPos;

private int mClickTimeout;

private int mTouchSlop;

private final int MAX\_ALPHA = 255;

private int mAlpha = MAX\_ALPHA;

private boolean mChecked = false;

private boolean mBroadcasting;

private boolean mTurningOn;

private PerformClick mPerformClick;

private OnCheckedChangeListener mOnCheckedChangeListener;

private OnCheckedChangeListener mOnCheckedChangeWidgetListener;

private boolean mAnimating;

private final float VELOCITY = 350;

private float mVelocity;

private final float EXTENDED\_OFFSET\_Y = 15;

private float mExtendOffsetY; // Y轴方向扩大的区域,增大点击区域

private float mAnimationPosition;

private float mAnimatedVelocity;

public SwitchButton(Context context, AttributeSet attrs) {  
    this(context, attrs, android.R.attr.checkboxStyle);  
}

public SwitchButton(Context context) {  
    this(context, null);  
}

public SwitchButton(Context context, AttributeSet attrs, int defStyle) {  
    super(context, attrs, defStyle);  
    initView(context);  
}

private void initView(Context context) {  
    mPaint = new Paint();  
    mPaint.setColor(Color.WHITE);  
    Resources resources = context.getResources();

    // get viewConfiguration  
    mClickTimeout = ViewConfiguration.getPressedStateDuration()  
            + ViewConfiguration.getTapTimeout();  
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    // get Bitmap  
    mBottom = BitmapFactory.decodeResource(resources, R.drawable.bottom);  
    mBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.btn\_pressed);  
    mBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.btn\_unpressed);  
    mFrame = BitmapFactory.decodeResource(resources, R.drawable.frame);  
    mMask = BitmapFactory.decodeResource(resources, R.drawable.mask);  
    mCurBtnPic = mBtnNormal;

    mBtnWidth = mBtnPressed.getWidth();  
    mMaskWidth = mMask.getWidth();  
    mMaskHeight = mMask.getHeight();

    mBtnOffPos = mBtnWidth / 2;  
    mBtnOnPos = mMaskWidth - mBtnWidth / 2;

    mBtnPos = mChecked ? mBtnOnPos : mBtnOffPos;  
    mRealPos = getRealPos(mBtnPos);

    final float density = getResources().getDisplayMetrics().density;  
    mVelocity = (int) (VELOCITY \* density + 0.5f);  
    mExtendOffsetY = (int) (EXTENDED\_OFFSET\_Y \* density + 0.5f);

    mSaveLayerRectF = new RectF(0, mExtendOffsetY, mMask.getWidth(), mMask.getHeight()  
            + mExtendOffsetY);  
    mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC\_IN);  
}

@Override  
public void setEnabled(boolean enabled) {  
    mAlpha = enabled ? MAX\_ALPHA : MAX\_ALPHA / 2;  
    super.setEnabled(enabled);  
}

public boolean isChecked() {  
    return mChecked;  
}

public void toggle() {  
    setChecked(!mChecked);  
}

/\*\*  
 \* 内部调用此方法设置checked状态,此方法会延迟执行各种回调函数,保证动画的流畅度  
 \*  
 \* @param checked  
 \*/  
private void setCheckedDelayed(final boolean checked) {  
    this.postDelayed(new Runnable() {

        @Override  
        public void run() {  
            setChecked(checked);  
        }  
    }, 10);  
}

/\*\*  
 \* <p>  
 \* Changes the checked state of this button.  
 \* </p>  
 \*  
 \* @param checked true to check the button, false to uncheck it  
 \*/  
public void setChecked(boolean checked) {

    if (mChecked != checked) {  
        mChecked = checked;

        mBtnPos = checked ? mBtnOnPos : mBtnOffPos;  
        mRealPos = getRealPos(mBtnPos);  
        invalidate();

        // Avoid infinite recursions if setChecked() is called from a  
        // listener  
        if (mBroadcasting) {  
            return;  
        }

        mBroadcasting = true;  
        if (mOnCheckedChangeListener != null) {  
            mOnCheckedChangeListener.onCheckedChanged(SwitchButton.this, mChecked);  
        }  
        if (mOnCheckedChangeWidgetListener != null) {  
            mOnCheckedChangeWidgetListener.onCheckedChanged(SwitchButton.this, mChecked);  
        }

        mBroadcasting = false;  
    }  
}

/\*\*  
 \* Register a callback to be invoked when the checked state of this button  
 \* changes.  
 \*  
 \* @param listener the callback to call on checked state change  
 \*/  
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {  
    mOnCheckedChangeListener = listener;  
}

/\*\*  
 \* Register a callback to be invoked when the checked state of this button  
 \* changes. This callback is used for internal purpose only.  
 \*  
 \* @param listener the callback to call on checked state change  
 \* @hide  
 \*/  
void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {  
    mOnCheckedChangeWidgetListener = listener;  
}

@Override  
public boolean onTouchEvent(MotionEvent event) {  
    int action = event.getAction();  
    float x = event.getX();  
    float y = event.getY();  
    float deltaX = Math.abs(x - mFirstDownX);  
    float deltaY = Math.abs(y - mFirstDownY);  
    switch (action) {  
        case MotionEvent.ACTION\_DOWN:  
            attemptClaimDrag();  
            mFirstDownX = x;  
            mFirstDownY = y;  
            mCurBtnPic = mBtnPressed;  
            mBtnInitPos = mChecked ? mBtnOnPos : mBtnOffPos;  
            break;  
        case MotionEvent.ACTION\_MOVE:  
            float time = event.getEventTime() - event.getDownTime();  
            mBtnPos = mBtnInitPos + event.getX() - mFirstDownX;  
            if (mBtnPos >= mBtnOffPos) {  
                mBtnPos = mBtnOffPos;  
            }  
            if (mBtnPos <= mBtnOnPos) {  
                mBtnPos = mBtnOnPos;  
            }  
            mTurningOn = mBtnPos > (mBtnOffPos - mBtnOnPos) / 2 + mBtnOnPos;

            mRealPos = getRealPos(mBtnPos);  
            break;  
        case MotionEvent.ACTION\_UP:  
            mCurBtnPic = mBtnNormal;  
            time = event.getEventTime() - event.getDownTime();  
            if (deltaY < mTouchSlop && deltaX < mTouchSlop && time < mClickTimeout) {  
                if (mPerformClick == null) {  
                    mPerformClick = new PerformClick();  
                }  
                if (!post(mPerformClick)) {  
                    performClick();  
                }  
            } else {  
                startAnimation(!mTurningOn);  
            }  
            break;  
    }

    invalidate();  
    return isEnabled();  
}

private final class PerformClick implements Runnable {  
    public void run() {  
        performClick();  
    }  
}

@Override  
public boolean performClick() {  
    startAnimation(!mChecked);  
    return true;  
}

/\*\*  
 \* Tries to claim the user's drag motion, and requests disallowing any  
 \* ancestors from stealing events in the drag.  
 \*/  
private void attemptClaimDrag() {  
    mParent = getParent();  
    if (mParent != null) {  
        mParent.requestDisallowInterceptTouchEvent(true);  
    }  
}

/\*\*  
 \* 将btnPos转换成RealPos  
 \*  
 \* @param btnPos  
 \* @return  
 \*/  
private float getRealPos(float btnPos) {  
    return btnPos - mBtnWidth / 2;  
}

@Override  
protected void onDraw(Canvas canvas) {  
    canvas.saveLayerAlpha(mSaveLayerRectF, mAlpha, Canvas.MATRIX\_SAVE\_FLAG  
            | Canvas.CLIP\_SAVE\_FLAG | Canvas.HAS\_ALPHA\_LAYER\_SAVE\_FLAG  
            | Canvas.FULL\_COLOR\_LAYER\_SAVE\_FLAG | Canvas.CLIP\_TO\_LAYER\_SAVE\_FLAG);  
    // 绘制蒙板  
    canvas.drawBitmap(mMask, 0, mExtendOffsetY, mPaint);  
    mPaint.setXfermode(mXfermode);

    // 绘制底部图片  
    canvas.drawBitmap(mBottom, mRealPos, mExtendOffsetY, mPaint);  
    mPaint.setXfermode(null);  
    // 绘制边框  
    canvas.drawBitmap(mFrame, 0, mExtendOffsetY, mPaint);

    // 绘制按钮  
    canvas.drawBitmap(mCurBtnPic, mRealPos, mExtendOffsetY, mPaint);  
    canvas.restore();  
}

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    setMeasuredDimension((int) mMaskWidth, (int) (mMaskHeight + 2 \* mExtendOffsetY));  
}

private void startAnimation(boolean turnOn) {  
    mAnimating = true;  
    mAnimatedVelocity = turnOn ? -mVelocity : mVelocity;  
    mAnimationPosition = mBtnPos;

    new SwitchAnimation().run();  
}

private void stopAnimation() {  
    mAnimating = false;  
}

private final class SwitchAnimation implements Runnable {

    @Override  
    public void run() {  
        if (!mAnimating) {  
            return;  
        }  
        doAnimation();  
        FrameAnimationController.requestAnimationFrame(this);  
    }  
}

private void doAnimation() {  
    mAnimationPosition += mAnimatedVelocity \* FrameAnimationController.ANIMATION\_FRAME\_DURATION  
            / 1000;  
    if (mAnimationPosition <= mBtnOnPos) {  
        stopAnimation();  
        mAnimationPosition = mBtnOnPos;  
        setCheckedDelayed(true);  
    } else if (mAnimationPosition >= mBtnOffPos) {  
        stopAnimation();  
        mAnimationPosition = mBtnOffPos;  
        setCheckedDelayed(false);  
    }  
    moveView(mAnimationPosition);  
}

private void moveView(float position) {  
    mBtnPos = position;  
    mRealPos = getRealPos(mBtnPos);  
    invalidate();  
}  

}

2、新建一个布局文件preference_widget_checkbox.xml


3、重写CheckBoxPreference并通过Inflater加载布局文件,同时屏蔽原有点击事件

package me.imid.preference;

import me.imid.movablecheckbox.R;
import me.imid.view.SwitchButton;

import android.app.Service;
import android.content.Context;
import android.preference.PreferenceActivity;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.Checkable;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.TextView;

public class CheckBoxPreference extends android.preference.CheckBoxPreference {
private Context mContext;
private int mLayoutResId = R.layout.preference;
private int mWidgetLayoutResId = R.layout.preference_widget_checkbox;

private boolean mShouldDisableView = true;

private CharSequence mSummaryOn;  
private CharSequence mSummaryOff;

private boolean mSendAccessibilityEventViewClickedType;

private AccessibilityManager mAccessibilityManager;

public CheckBoxPreference(Context context, AttributeSet attrset,  
        int defStyle) {  
    super(context, attrset);  
    mContext = context;  
    mSummaryOn = getSummaryOn();  
    mSummaryOff = getSummaryOff();  
    mAccessibilityManager = (AccessibilityManager) mContext  
            .getSystemService(Service.ACCESSIBILITY\_SERVICE);  
}

public CheckBoxPreference(Context context, AttributeSet attrs) {  
    this(context, attrs, android.R.attr.checkBoxPreferenceStyle);  
}

public CheckBoxPreference(Context context) {  
    this(context, null);  
}

/\*\*  
 \* Creates the View to be shown for this Preference in the  
 \* {@link PreferenceActivity}. The default behavior is to inflate the main  
 \* layout of this Preference (see {@link #setLayoutResource(int)}. If  
 \* changing this behavior, please specify a {@link ViewGroup} with ID  
 \* {@link android.R.id#widget\_frame}.  
 \* <p>  
 \* Make sure to call through to the superclass's implementation.  
 \*  
 \* @param parent  
 \*            The parent that this View will eventually be attached to.  
 \* @return The View that displays this Preference.  
 \* @see #onBindView(View)  
 \*/  
protected View onCreateView(ViewGroup parent) {  
    final LayoutInflater layoutInflater = (LayoutInflater) mContext  
            .getSystemService(Context.LAYOUT\_INFLATER\_SERVICE);

    final View layout = layoutInflater.inflate(mLayoutResId, parent, false);

    if (mWidgetLayoutResId != 0) {  
        final ViewGroup widgetFrame = (ViewGroup) layout  
                .findViewById(R.id.widget\_frame);  
        layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);  
    }  
    return layout;  
}

@Override  
protected void onBindView(View view) {  
    // 屏蔽item点击事件  
    view.setClickable(false);

    TextView textView = (TextView) view.findViewById(R.id.title);  
    if (textView != null) {  
        textView.setText(getTitle());  
    }

    textView = (TextView) view.findViewById(R.id.summary);  
    if (textView != null) {  
        final CharSequence summary = getSummary();  
        if (!TextUtils.isEmpty(summary)) {  
            if (textView.getVisibility() != View.VISIBLE) {  
                textView.setVisibility(View.VISIBLE);  
            }

            textView.setText(getSummary());  
        } else {  
            if (textView.getVisibility() != View.GONE) {  
                textView.setVisibility(View.GONE);  
            }  
        }  
    }

    if (mShouldDisableView) {  
        setEnabledStateOnViews(view, isEnabled());  
    }

    View checkboxView = view.findViewById(R.id.checkbox);  
    if (checkboxView != null && checkboxView instanceof Checkable) {  
        ((Checkable) checkboxView).setChecked(isChecked());  
        SwitchButton switchButton = (SwitchButton) checkboxView;  
        switchButton  
                .setOnCheckedChangeListener(new OnCheckedChangeListener() {

                    public void onCheckedChanged(CompoundButton buttonView,  
                            boolean isChecked) {  
                        // TODO Auto-generated method stub  
                        mSendAccessibilityEventViewClickedType = true;  
                        if (!callChangeListener(isChecked)) {  
                            return;  
                        }  
                        setChecked(isChecked);  
                    }  
                });  
        // send an event to announce the value change of the CheckBox and is  
        // done here  
        // because clicking a preference does not immediately change the  
        // checked state  
        // for example when enabling the WiFi  
        if (mSendAccessibilityEventViewClickedType  
                && mAccessibilityManager.isEnabled()  
                && checkboxView.isEnabled()) {  
            mSendAccessibilityEventViewClickedType = false;

            int eventType = AccessibilityEvent.TYPE\_VIEW\_CLICKED;  
            checkboxView.sendAccessibilityEventUnchecked(AccessibilityEvent  
                    .obtain(eventType));  
        }  
    }

    // Sync the summary view  
    TextView summaryView = (TextView) view.findViewById(R.id.summary);  
    if (summaryView != null) {  
        boolean useDefaultSummary = true;  
        if (isChecked() && mSummaryOn != null) {  
            summaryView.setText(mSummaryOn);  
            useDefaultSummary = false;  
        } else if (!isChecked() && mSummaryOff != null) {  
            summaryView.setText(mSummaryOff);  
            useDefaultSummary = false;  
        }

        if (useDefaultSummary) {  
            final CharSequence summary = getSummary();  
            if (summary != null) {  
                summaryView.setText(summary);  
                useDefaultSummary = false;  
            }  
        }

        int newVisibility = View.GONE;  
        if (!useDefaultSummary) {  
            // Someone has written to it  
            newVisibility = View.VISIBLE;  
        }  
        if (newVisibility != summaryView.getVisibility()) {  
            summaryView.setVisibility(newVisibility);  
        }  
    }  
}

/\*\*  
 \* Makes sure the view (and any children) get the enabled state changed.  
 \*/  
private void setEnabledStateOnViews(View v, boolean enabled) {  
    v.setEnabled(enabled);

    if (v instanceof ViewGroup) {  
        final ViewGroup vg = (ViewGroup) v;  
        for (int i = vg.getChildCount() - 1; i >= 0; i--) {  
            setEnabledStateOnViews(vg.getChildAt(i), enabled);  
        }  
    }  
}

}

4、在res/xml下新建选项设置布局文件


<me.imid.preference.CheckBoxPreference  
    android:defaultValue="true"  
    android:enabled="false"  
    android:summary="summary"  
    android:title="MyCheckbox(disabled)" />  
<me.imid.preference.CheckBoxPreference  
    android:defaultValue="true"  
    android:dependency="checkbox"  
    android:summaryOff="off"  
    android:summaryOn="on"  
    android:title="MyCheckbox(enabled)" />  
<me.imid.preference.CheckBoxPreference  
    android:defaultValue="false"  
    android:key="checkbox"  
    android:summaryOff="off"  
    android:summaryOn="on"  
    android:title="MyCheckbox(enabled)" />

<CheckBoxPreference  
    android:defaultValue="true"  
    android:enabled="false"  
    android:summaryOff="off"  
    android:summaryOn="on"  
    android:title="defalt checkbox(disabled)" />  
<CheckBoxPreference  
    android:defaultValue="true"  
    android:dependency="checkbox1"  
    android:summaryOff="off"  
    android:summaryOn="on"  
    android:title="defalt checkbox(enabled)" />  
<CheckBoxPreference  
    android:defaultValue="false"  
    android:key="checkbox1"  
    android:summaryOff="off"  
    android:summaryOn="on"  
    android:title="defalt checkbox(enabled)" />

运行结果: