很
多时候,我们在很多无论是Android还是IOS的APP中都会遇到这样的一种效果,有一个按钮,我们点击一下,便会滑动一下,一会显示“开”,一会显示“关”,这便是开关按钮了,比如:很多Android手机的设置功能里,就有很多功能是用开关按钮实现的,那么这些开关按钮时如何实现的呢?下面,就让我们一起来实现这个功能吧。
一、原理
我们在界面的某一个区域里放置一个背景图A,这个图片一边为“开”,一边为“关”,在这个图片上放置一个图片B,图B大约为图A的一半,恰好可以覆盖掉图A上的“开”或者“关”,当我们手指点击图片的时候,图B在图A上滑动,相应的覆盖“开”或者“关”,这样就实现了开关按钮的效果。
二、实现
1、自定义View类MyToggle
这个类继承自View类同时实现了OnTouchListener接口,这个类实现的功能比较多,我们分解来看这个类。
1)属性字段
这个类中定义了不少的属性字段,每个属性字段的具体含义详见代码注释
具体实现代码如下:
- //开关开启的背景图片
- private Bitmap bkgSwitchOn;
- //开关关闭的背景图片
- private Bitmap bkgSwitchOff;
- //开关的滚动图片
- private Bitmap btnSlip;
- //当前开关是否为开启状态
- private boolean toggleStateOn;
- //开关状态的监听事件
- private OnToggleStateListener toggleStateListener;
- //记录开关·当前的状态
- private boolean isToggleStateListenerOn;
- //手指按下屏幕时的x坐标
- private float proX;
- //手指滑动过程中当前x坐标
- private float currentX;
- //是否处于滑动状态
- private boolean isSlipping;
- //记录上一次开关的状态
- private boolean proToggleState = true;
- //开关开启时的矩形
- private Rect rect_on;
- //开关关闭时的矩形
- private Rect rect_off;
2)覆写View类的构造方法
我们在构造方法里完成的操作就是调用我们自己创建的init()方法
具体实现代码如下:
- public MyToggle(Context context) {
- super(context);
- init(context);
- }
- public MyToggle(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
3)创建init方法
这个方法中实现的操作就是设置触摸事件。
具体实现代码如下:
- //初始化方法
- private void init(Context context) {
- setOnTouchListener(this);
- }
4)手指触摸事件回调方法onTouch
这个方法是当手指操作手机屏幕时,Android自动回调的方法,我们在这个方法中,监听手指的按下、移动和抬起事件,记录手指当前的X坐标来判断图片B的移动方向,从而实现图片B在图片A上的移动来达到按钮开和关的效果。
具体实现代码如下:
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- //记录手指按下时的x坐标
- proX = event.getX();
- currentX = proX;
- //将滑动标识设置为true
- isSlipping = true;
- break;
- case MotionEvent.ACTION_MOVE:
- //记录手指滑动过程中当前x坐标
- currentX = event.getX();
- break;
- case MotionEvent.ACTION_UP:
- //手指抬起时将是否滑动的标识设置为false
- isSlipping = false;
- //处于关闭状态
- if(currentX < bkgSwitchOn.getWidth() / 2 ){
- toggleStateOn = false;
- } else { // 处于开启状态
- toggleStateOn = true;
- }
- // 如果使用了开关监听器,同时开关的状态发生了改变,这时使用该代码
- if(isToggleStateListenerOn && toggleStateOn != proToggleState){
- proToggleState = toggleStateOn;
- toggleStateListener.onToggleState(toggleStateOn);
- }
- break;
- }
- invalidate();//重绘
- return true;
- }
5)界面重绘方法onDraw
这个方法主要实现的是界面的重绘操作。
只要的思路是:
画背景图A:
当前手指滑动X坐标currentX大于图A宽度的一般时,按钮背景为开启状态;
当前手指滑动X坐标currentX小于图A宽度的一般时,按钮背景为关闭状态;
记录滑块B的X坐标:
B滑动时:
当前手指滑动X坐标currentX大于背景图A的宽度,则B坐标为图A宽度减去图B宽度
当前手指滑动X坐标currentX小于背景图A的宽度,则B坐标为当前X坐标currentX减去滑块宽度的一半
B静止:
当按钮处于“开”状态,则B坐标为“开”状态的最左边X坐标
当按钮处于“关”状态,则B坐标为“关”状态的最左边X坐标
具体实现代码如下:
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //用来记录我们滑动块的位置
- int left_slip = 0;
- Matrix matrix = new Matrix();
- Paint paint = new Paint();
- if(currentX < bkgSwitchOn.getWidth() / 2){
- //在画布上绘制出开关状态为关闭时的 背景图片
- canvas.drawBitmap(bkgSwitchOff, matrix, paint);
- }else{
- //在画布上绘制出开关状态为开启时的 背景图片
- canvas.drawBitmap(bkgSwitchOn, matrix, paint);
- }
- if(isSlipping){//开关是否处于滑动状态
- // 滑动块 是否超过了整个滑动按钮的宽度
- if(currentX > bkgSwitchOn.getWidth()){
- //指定滑动块的位置
- left_slip = bkgSwitchOn.getWidth() - btnSlip.getWidth();
- } else {
- //设置当前滑动块的位置
- left_slip = (int) (currentX - btnSlip.getWidth() /2);
- }
- } else {//开关是否处于 不滑动状态
- if(toggleStateOn){
- left_slip = rect_on.left;
- } else {
- left_slip = rect_off.left;
- }
- }
- if(left_slip < 0){
- left_slip = 0;
- } else if( left_slip > bkgSwitchOn.getWidth() - btnSlip.getWidth()){
- left_slip = bkgSwitchOn.getWidth() - btnSlip.getWidth();
- }
- //绘制图像
- canvas.drawBitmap(btnSlip, left_slip, 0, paint);
- }
6)计算开关的宽高
这里我通过覆写onMeasure来计算开关的宽度和高度
具体实现代码如下:
- //计算开关的宽高
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(bkgSwitchOn.getWidth(), bkgSwitchOn.getHeight());
- }
7)设置图片资源信息
这个方法主要是供外界调用,向本类提供图片资源。
具体代码实现如下:
- /**
- * 设置图片资源信息
- * @param bkgSwitch_on
- * @param bkgSwitch_off
- * @param btn_Slip
- */
- public void setImageRes(int bkgSwitch_on, int bkgSwitch_off, int btn_Slip) {
- bkgSwitchOn = BitmapFactory.decodeResource(getResources(), bkgSwitch_on);
- bkgSwitchOff = BitmapFactory.decodeResource(getResources(),bkgSwitch_off);
- btnSlip = BitmapFactory.decodeResource(getResources(), btn_Slip);
- rect_on = new Rect(bkgSwitchOn.getWidth() - btnSlip.getWidth(), 0,bkgSwitchOn.getWidth(), btnSlip.getHeight());
- rect_off = new Rect(0, 0, btnSlip.getWidth(), btnSlip.getHeight());
- }
8)设置开关按钮的状态
通过传递一个boolean类型的状态,我们在这个方法中将这个状态标识记录下来。
具体实现代码如下:
- /**
- * 设置开关按钮的状态
- * @param state
- */
- public void setToggleState(boolean state) {
- toggleStateOn = state;
- }
9)自定义开关状态监听器
我在这个类中定义了一个开关状态监听器接口OnToggleStateListener,里面有一个onToggleState方法来执行按钮的状态变化监听操作。
具体代码实现如下:
- /**
- * 自定义开关状态监听器
- * @author liuyazhuang
- *
- */
- interface OnToggleStateListener {
- abstract void onToggleState(boolean state);
- }
10)设置开关监听器
创建setOnToggleStateListener方法,传递一个OnToggleStateListener监听器对象,通过外界创建OnToggleStateListener对象,并将OnToggleStateListener对象传递进来,我们只需要将外界传递过来的OnToggleStateListener对象记录下来,同时当我们调用OnToggleStateListener接口中的onToggleState方法时,便实现了回调外界OnToggleStateListener实现类中的onToggleState方法。
具体代码实现如下:
- //设置开关监听器并将是否设置了开关监听器设置为true
- public void setOnToggleStateListener(OnToggleStateListener listener) {
- toggleStateListener = listener;
- isToggleStateListenerOn = true;
- }
11)MyToggle完整代码如下:
- package com.lyz.slip.toggle;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Matrix;
- import android.graphics.Paint;
- import android.graphics.Rect;
- import android.util.AttributeSet;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.View.OnTouchListener;
- /**
- * 自定义开关类
- * @author liuyazhuang
- *
- */
- public class MyToggle extends View implements OnTouchListener {
- //开关开启的背景图片
- private Bitmap bkgSwitchOn;
- //开关关闭的背景图片
- private Bitmap bkgSwitchOff;
- //开关的滚动图片
- private Bitmap btnSlip;
- //当前开关是否为开启状态
- private boolean toggleStateOn;
- //开关状态的监听事件
- private OnToggleStateListener toggleStateListener;
- //记录开关·当前的状态
- private boolean isToggleStateListenerOn;
- //手指按下屏幕时的x坐标
- private float proX;
- //手指滑动过程中当前x坐标
- private float currentX;
- //是否处于滑动状态
- private boolean isSlipping;
- //记录上一次开关的状态
- private boolean proToggleState = true;
- //开关开启时的矩形
- private Rect rect_on;
- //开关关闭时的矩形
- private Rect rect_off;
- public MyToggle(Context context) {
- super(context);
- init(context);
- }
- public MyToggle(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
- //初始化方法
- private void init(Context context) {
- setOnTouchListener(this);
- }
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- //记录手指按下时的x坐标
- proX = event.getX();
- currentX = proX;
- //将滑动标识设置为true
- isSlipping = true;
- break;
- case MotionEvent.ACTION_MOVE:
- //记录手指滑动过程中当前x坐标
- currentX = event.getX();
- break;
- case MotionEvent.ACTION_UP:
- //手指抬起时将是否滑动的标识设置为false
- isSlipping = false;
- //处于关闭状态
- if(currentX < bkgSwitchOn.getWidth() / 2 ){
- toggleStateOn = false;
- } else { // 处于开启状态
- toggleStateOn = true;
- }
- // 如果使用了开关监听器,同时开关的状态发生了改变,这时使用该代码
- if(isToggleStateListenerOn && toggleStateOn != proToggleState){
- proToggleState = toggleStateOn;
- toggleStateListener.onToggleState(toggleStateOn);
- }
- break;
- }
- invalidate();//重绘
- return true;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //用来记录我们滑动块的位置
- int left_slip = 0;
- Matrix matrix = new Matrix();
- Paint paint = new Paint();
- if(currentX < bkgSwitchOn.getWidth() / 2){
- //在画布上绘制出开关状态为关闭时的 背景图片
- canvas.drawBitmap(bkgSwitchOff, matrix, paint);
- }else{
- //在画布上绘制出开关状态为开启时的 背景图片
- canvas.drawBitmap(bkgSwitchOn, matrix, paint);
- }
- if(isSlipping){//开关是否处于滑动状态
- // 滑动块 是否超过了整个滑动按钮的宽度
- if(currentX > bkgSwitchOn.getWidth()){
- //指定滑动块的位置
- left_slip = bkgSwitchOn.getWidth() - btnSlip.getWidth();
- } else {
- //设置当前滑动块的位置
- left_slip = (int) (currentX - btnSlip.getWidth() /2);
- }
- } else {//开关是否处于 不滑动状态
- if(toggleStateOn){
- left_slip = rect_on.left;
- } else {
- left_slip = rect_off.left;
- }
- }
- if(left_slip < 0){
- left_slip = 0;
- } else if( left_slip > bkgSwitchOn.getWidth() - btnSlip.getWidth()){
- left_slip = bkgSwitchOn.getWidth() - btnSlip.getWidth();
- }
- //绘制图像
- canvas.drawBitmap(btnSlip, left_slip, 0, paint);
- }
- //计算开关的宽高
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(bkgSwitchOn.getWidth(), bkgSwitchOn.getHeight());
- }
- /**
- * 设置图片资源信息
- * @param bkgSwitch_on
- * @param bkgSwitch_off
- * @param btn_Slip
- */
- public void setImageRes(int bkgSwitch_on, int bkgSwitch_off, int btn_Slip) {
- bkgSwitchOn = BitmapFactory.decodeResource(getResources(), bkgSwitch_on);
- bkgSwitchOff = BitmapFactory.decodeResource(getResources(),bkgSwitch_off);
- btnSlip = BitmapFactory.decodeResource(getResources(), btn_Slip);
- rect_on = new Rect(bkgSwitchOn.getWidth() - btnSlip.getWidth(), 0,bkgSwitchOn.getWidth(), btnSlip.getHeight());
- rect_off = new Rect(0, 0, btnSlip.getWidth(), btnSlip.getHeight());
- }
- /**
- * 设置开关按钮的状态
- * @param state
- */
- public void setToggleState(boolean state) {
- toggleStateOn = state;
- }
- /**
- * 自定义开关状态监听器
- * @author liuyazhuang
- *
- */
- interface OnToggleStateListener {
- abstract void onToggleState(boolean state);
- }
- //设置开关监听器并将是否设置了开关监听器设置为true
- public void setOnToggleStateListener(OnToggleStateListener listener) {
- toggleStateListener = listener;
- isToggleStateListenerOn = true;
- }
- }
2、MainActivity
这个类实现很简单,主要的功能就是加载界面布局,初始化界面控件,调用MyToggle类中的方法实现按钮的开关效果
具体代码实现如下:
- package com.lyz.slip.toggle;
- import android.app.Activity;
- import android.os.Bundle;
- import android.widget.Toast;
- import com.lyz.slip.toggle.MyToggle.OnToggleStateListener;
- /**
- * 程序主入口
- * @author liuyazhuang
- *
- */
- public class MainActivity extends Activity {
- //自定义开关对象
- private MyToggle toggle;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- toggle = (MyToggle) findViewById(R.id.toggle);
- //设置开关显示所用的图片
- toggle.setImageRes(R.drawable.bkg_switch, R.drawable.bkg_switch, R.drawable.btn_slip);
- //设置开关的默认状态 true开启状态
- toggle.setToggleState(true);
- //设置开关的监听
- toggle.setOnToggleStateListener(new OnToggleStateListener() {
- @Override
- public void onToggleState(boolean state) {
- // TODO Auto-generated method stub
- if(state){
- Toast.makeText(getApplicationContext(), "开关开启", 0).show();
- } else {
- Toast.makeText(getApplicationContext(), "开关关闭", 0).show();
- }
- }
- });
- }
- }
3、布局文件activity_main.xml
这里我引用了自己定义的View类MyToggle。
具体代码实现如下:
- <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" >
- <com.lyz.slip.toggle.MyToggle
- android:id="@+id/toggle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"/>
- </RelativeLayout>
4、AndroidManifest.xml
具体代码如下:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.lyz.slip.toggle"
- android:versionCode="1"
- android:versionName="1.0" >
- <uses-sdk
- android:minSdkVersion="10"
- android:targetSdkVersion="18" />
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@style/AppTheme" >
- <activity
- android:name="com.lyz.slip.toggle.MainActivity"
- android:label="@string/app_name" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- </manifest>
三、运行效果