前一段时间在做项目的时候遇到了一个问题,美工在设计的时候设计的是一个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);
}
}


我们的基本思路是将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

<?xml version="1.0" encoding="utf-8"?>
<me.imid.view.SwitchButton xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center" />

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下新建选项设置布局文件

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

<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)" />

</PreferenceScreen>

运行结果:

Android中实现iPhone开关_sed