Android中实现iPhone开关

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

<?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>
运行结果:







posted on 2014-02-12 10:38  岚之山  阅读(161)  评论(0编辑  收藏  举报

导航