最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

Android 自定义SwitchButton开关控件

开发进阶 AndroidChina 13496浏览

SwitchButton开关控件早已经非常流行。有各种各样的样式,SwitchButton开关控件一般用于app软件设置那里,控制缓存、声音、提示、下载等等。是具有很好的UI体验以及用户的习惯性。那么再下面介绍一个SwitchButton开关控件。并附上源码。

源码下载:点击

一、看实现的效果图

二、自定义SwitchButton

这是一个继承CheckBox的SwitchButton类。来实现做这些动画效果的,首先准备好这些图片,然后canvas绘制控件 的边框、背景、以及按钮。绘制时候加上相应的图片。然后用onTouchEvent这个函数,接受当按下,滑动,松开后的效果。那么大概就出来了。接下来看具体的代码。

package com.org.switchbtn;

import com.switchbutton.activity.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();
     }
}


二、控制辅助类FrameAnimationController

package com.org.switchbtn;

import android.os.Handler;
import android.os.Message;

public class FrameAnimationController {
     private static final int MSG_ANIMATE = 1000;

     public static final int ANIMATION_FRAME_DURATION = 1000 / 60;

     private static final Handler mHandler = new AnimationHandler();

     private FrameAnimationController() {
           throw new UnsupportedOperationException();
     }

     public static void requestAnimationFrame(Runnable runnable) {
          Message message = new Message();
          message.what = MSG_ANIMATE;
          message.obj = runnable;
          mHandler.sendMessageDelayed(message, ANIMATION_FRAME_DURATION);
     }

     public static void requestFrameDelay(Runnable runnable, long delay) {
          Message message = new Message();
          message.what = MSG_ANIMATE;
          message.obj = runnable;
          mHandler.sendMessageDelayed(message, delay);
    }

    private static class AnimationHandler extends Handler {
         public void handleMessage(Message m) {
              switch (m.what) {
                   case MSG_ANIMATE:
                         if (m.obj != null) {
                              ((Runnable) m.obj).run();
                         }
                   break;
             }
        }
    }
}

三、看最后监听,调用,实现MainActivity

因为自定义SwitchButton这个类是继承checkbox。所以还是用这个接口OnCheckedChangeListener。用这个接口实现监听。

package com.switchbutton.activity;

import com.org.switchbtn.SwitchButton;

import android.os.Bundle;
import android.app.Activity;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Toast;

public class MainActivity extends Activity implements OnCheckedChangeListener{
      private SwitchButton mMsgNotifySwitch;
      private SwitchButton mMsgSoundSwitch;
      @Override
      protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);

             initUI();

      }

      private void initUI() {
            mMsgNotifySwitch = (SwitchButton)findViewById(R.id.message_notify_switch);
            mMsgSoundSwitch = (SwitchButton)findViewById(R.id.message_sound_switch);
            mMsgNotifySwitch.setOnCheckedChangeListener(this);
            mMsgSoundSwitch.setOnCheckedChangeListener(this);

     }

     @Override
     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
           switch (buttonView.getId()) {
                 case R.id.message_notify_switch:
                       if (isChecked) {
                              Toast.makeText(this, "新消息提醒打开", Toast.LENGTH_SHORT).show();
                       } else {
                              Toast.makeText(this, "新消息提醒关闭", Toast.LENGTH_SHORT).show();
                       }
                       break;
                 case R.id.message_sound_switch:
                       if (isChecked) {
                             Toast.makeText(this, "声音提醒打开", Toast.LENGTH_SHORT).show();
                       } else {
                              Toast.makeText(this, "声音提醒关闭", Toast.LENGTH_SHORT).show();
                       }
                       break;
                 default:
                       break;
           }

     }

}

四、附上最后的xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <RelativeLayout
            android:id="@+id/sound_and_vibrate"
            android:layout_width="fill_parent"
            android:layout_height="44.0dip"
            android:background="@drawable/common_strip_setting_bottom"
            android:clickable="false"
            android:focusable="false" >

      <TextView
            style="@style/B4_Font_white"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="12.0dip"
            android:duplicateParentState="true"
            android:gravity="center_vertical"
            android:text="@string/set_message_sound" />

      <com.org.switchbtn.SwitchButton
            android:id="@+id/message_sound_switch"
            android:layout_width="80dip"
            android:layout_height="28dip"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="8.0dip" />
     </RelativeLayout>

     <RelativeLayout
            android:id="@+id/pushSetting"
            android:layout_width="fill_parent"
            android:layout_height="44.0dip"
            android:layout_alignParentLeft="true"
            android:layout_below="@+id/sound_and_vibrate"
            android:background="@drawable/common_strip_setting_top"
            android:clickable="false"
            android:focusable="false" >

            <TextView
                   android:id="@+id/encoding_style"
                   style="@style/B4_Font_white"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:layout_centerVertical="true"
                   android:layout_marginLeft="12.0dip"
                   android:duplicateParentState="true"
                   android:gravity="center_vertical"
                   android:text="@string/set_message_notify" />

            <com.org.switchbtn.SwitchButton
                   android:id="@+id/message_notify_switch"
                   android:layout_width="80dip"
                   android:layout_height="28dip"
                   android:layout_alignParentRight="true"
                   android:layout_centerVertical="true"
                   android:layout_marginRight="8.0dip" />
       </RelativeLayout>

</RelativeLayout>

到这里结束了,欢迎交流学习自定义控件。

源码下载:点击

转载请注明:Android开发中文站 » Android 自定义SwitchButton开关控件