Android之——史上最简单自定义开关按钮的实现

·  阅读 662

前言

很多时候,我们在很多无论是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\>
复制代码

三、运行效果

image.png

image.png

四、温馨提示

大家可以到链接下载Android自定义开关按钮实现示例完整源代码

本实例中,为了方面,我把一些文字直接写在了布局文件中和相关的类中,大家在真实的项目中要把这些文字写在string.xml文件中,在外部引用这些资源,切记,这是作为一个Android程序员最基本的开发常识和规范,我在这里只是为了方便直接写在了类和布局文件中。

分类:
Android
收藏成功!
已添加到「」, 点击更改