Android自定义开关按钮ToggleButton

简介

由于项目开发需要,所以做了一个自定义的开关按钮,样式类似于Android5.0中开关按钮,也有开启关闭的切换效果,在这里和大家分享一下,先看一下效果图如下:

togglebutton.gif

看过效果图以后我们废话不多说,直接来看一下实现代码。

实现

这里主要是写了一个类去继承View,然后不断的对它进行重绘以显示出这种效果,该类ToggleButton.java的实现代码如下:

public class ToggleButton extends View implements View.OnClickListener{   
  private OnToggleChanged listener;      
  private int onColor = Color.parseColor("#4ebb7f");   
  private int offColor = Color.parseColor("#dadbda");   
  private boolean toggleOn = true;   
  private int BTN_WIDTH = 40;// 宽度   
  private int BTN_HEIGHT = 30;// 高度   
  private int CIRCLE_RADIUS = 8;// 圆的半径   
  private int LINE_WIDTH = 2;// 直线高度   
  private int circleX;// 圆心X轴坐标      
  private boolean changeCompelete = true;   
  private Resources r;   
  private TimerTask task;   
  private Timer timer;      
  private Handler handler = new Handler() {      
    public void handleMessage(android.os.Message msg) {         
      if (msg.what == 1111) {            
        invalidate();         
      }      
    };   
  };      

  public ToggleButton(Context context) {      
    this(context, null, 0);      
  }   

  public ToggleButton(Context context, AttributeSet attrs) {      
    this(context, attrs, 0);      
  }      

  public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {      
    super(context, attrs, defStyleAttr);      
    doInit();   
  }   

  public int getOnColor() {      
    return onColor;   
  }   
  public void setOnColor(int onColor) {      
    this.onColor = onColor;      
    invalidate();   
  }   
  public int getOffColor() {      
    return offColor;   
  }   
  public void setOffColor(int offColor) {      
    this.offColor = offColor;      
    invalidate();   
  }   
  public boolean isToggleOn() {      
    return toggleOn;   
  }   
  public void setToggleOn(boolean toggleOn) {      
    this.toggleOn = toggleOn;      
    if (toggleOn) {         
      circleX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH - CIRCLE_RADIUS, r.getDisplayMetrics());      
    }else {         
      circleX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CIRCLE_RADIUS, r.getDisplayMetrics());      
    }      
    invalidate();   
  }      
  private void doInit() {      
    r = Resources.getSystem();      
    setOnClickListener(this);      
    circleX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH - CIRCLE_RADIUS, r.getDisplayMetrics());   
  }   

  @Override   
  protected void onDraw(Canvas canvas) {      
    if (toggleOn) {         
      Paint linePaint = new Paint();         
      linePaint.setColor(onColor);         
      linePaint.setAntiAlias(true);         
      linePaint.setStyle(Style.FILL);
      linePaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, LINE_WIDTH, r.getDisplayMetrics()));         
      canvas.drawRect(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (BTN_HEIGHT - LINE_WIDTH) / 2, r.getDisplayMetrics()), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH, r.getDisplayMetrics()), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (BTN_HEIGHT + LINE_WIDTH) / 2, r.getDisplayMetrics()), linePaint);
      Paint circlePaint = new Paint();         
      circlePaint.setColor(onColor);         
      circlePaint.setAntiAlias(true);         
      canvas.drawCircle(circleX, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_HEIGHT / 2, r.getDisplayMetrics()),TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CIRCLE_RADIUS, r.getDisplayMetrics()), circlePaint);      
    } else {         
      Paint circlePaint = new Paint();         
      circlePaint.setColor(offColor);         
      circlePaint.setAntiAlias(true);         
      canvas.drawCircle(circleX,TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_HEIGHT / 2, r.getDisplayMetrics()),TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CIRCLE_RADIUS, r.getDisplayMetrics()), circlePaint);         
      Paint linePaint = new Paint();         
      linePaint.setColor(offColor);         
      linePaint.setAntiAlias(true);         
      linePaint.setStyle(Style.FILL);
      linePaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, LINE_WIDTH, r.getDisplayMetrics()));         
      canvas.drawRect(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (BTN_HEIGHT - LINE_WIDTH) / 2, r.getDisplayMetrics()),TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH, r.getDisplayMetrics()), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (BTN_HEIGHT + LINE_WIDTH) / 2, r.getDisplayMetrics()), linePaint);      
    }   
  }   

  @Override   
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);      
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);      
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);      
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);      
    if (widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST{
      widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH, r.getDisplayMetrics());         
      widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);      
    }      
    if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) {heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_HEIGHT,r.getDisplayMetrics());         
      heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);      
    }      
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);   
  }   

  @Override   
  public void onClick(View v) {      
    if (changeCompelete) {         
      if (toggleOn) {            
        toggleOn = false;         
      }else {            
        toggleOn = true;         
      }         
      changeCompelete = false;         
      task = new TimerTask() {            
        @Override            
        public void run() {               
          if (toggleOn) {                  
            circleX++;               
          }else {                  
            circleX--;               
          }               
          handler.sendEmptyMessage(1111);               
          if (circleX == TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH - CIRCLE_RADIUS, r.getDisplayMetrics()) || circleX ==TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CIRCLE_RADIUS, r.getDisplayMetrics())) {                  
            changeCompelete = true;                  
            timer.cancel();               
            }            
          }         
      };         
      timer = new Timer();         
      timer.schedule(task, 0, 2);   
      if (listener != null)      
         listener.onToggle(toggleOn);      
    }   
  }   

  public interface OnToggleChanged {      
    public void onToggle(boolean on);   
  }   

  public void setOnToggleChanged(OnToggleChanged onToggleChanged) {      
    listener = onToggleChanged;   
  }
}

这是整个自定义开关按钮的全部代码,其实非常简单,我来给大家解释一下具体思路,首先我们规定好按钮的宽度和高度,也就是代码中BTN_WIDTH、BTN_HEIGHT这两个值,当然这两个值你可以设置成任意值,并不一定是和我一样,然后是圆的半径CIRCLE_RADIUS。整个按钮其实是通过画一个直线和圆组合而成,而圆心的X轴坐标在最左边就是CIRCLE_RADIUS,在最右边是BTN_WIDTH-CIRCLE_RADIUS。然后我们想实现一个开关变换的动态效果时就不断改变圆心的X轴坐标的位置并不断对圆进行重绘,从而造成这么一种假象,好像圆在不断的移动。在开关切换时我们变换画笔的颜色,这些颜色当然也可以任意设置,整个思路大致就是这样。下面我们来继续来对它进行使用,就和使用Android自带的控件一样进行使用。

布局文件activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"    
  android:layout_height="match_parent">    
  <me.my.togglebutton.ToggleButton        
    android:layout_width="wrap_content"        
    android:layout_height="wrap_content"        
    android:layout_centerInParent="true"        
    android:id="@+id/toggle_button"/>
</RelativeLayout>

主界面MainActivity.java:

public class MainActivity extends Activity{    
  private ToggleButton toggleButton;    
  @Override    
  protected void onCreate(Bundle savedInstanceState) {        
    super.onCreate(savedInstanceState);        
    setContentView(R.layout.activity_main);        
    toggleButton = (ToggleButton) findViewById(R.id.toggle_button);
    toggleButton.setOnToggleChanged(new ToggleButton.OnToggleChanged() {            
      @Override            
      public void onToggle(boolean on) {                
        Toast.makeText(getApplicationContext(), "是否开启" + on, Toast.LENGTH_SHORT).show(); 
      }        
    });    
  }
}

运行之后效果如上图所示,到这里自定义开关按钮就介绍完了。下面为了使这个控件有更多效果设置的空间,我再来和大家谈谈如何为自定义控件设置自定义属性,就以本文中的这个控件为例,我们为其加上一些自定义属性。

自定义控件的自定义属性

添加自定义属性文件attrs.xml(该文件放在项目的values目录下):

<?xml version="1.0" encoding="utf-8"?>
<resources>    
  <declare-styleable name="ToggleButton">        
    <attr name="onColor" format="color" />        
    <attr name="offColor" format="color" />        
    <attr name="btnWidth" format="integer" />        
    <attr name="btnHeight" format="integer" />        
    <attr name="circleRadius" format="integer" />        
    <attr name="lineHeight" format="integer" />    
  </declare-styleable>
</resources>

上述文件定义的自定义属性对应MainActivity.java中的如下几个变量:

private int onColor = Color.parseColor("#4ebb7f");// 开启颜色
private int offColor = Color.parseColor("#dadbda");// 关闭颜色
private int BTN_WIDTH = 40;// 宽度  
private int BTN_HEIGHT = 30;// 高度  
private int CIRCLE_RADIUS = 8;// 圆的半径  
private int LINE_WIDTH = 2;// 直线高度

现在我们可以在布局文件中直接为自定义的开关控件设置这些属性,我就随便设置了一些值,更改后的布局文件activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto" 
  android:layout_width="match_parent"    
  android:layout_height="match_parent">    
  <me.my.togglebutton.ToggleButton        
    android:layout_width="wrap_content"        
    android:layout_height="wrap_content"        
    android:layout_centerInParent="true"        
    app:offColor="#ff0000"        
    app:onColor="#00ff00"  
    app:btnWidth="80"
    app:btnHeight="60"
    app:circleRadius="15"
    app:lineHeight="4"      
    android:id="@+id/toggle_button"/>
</RelativeLayout>

这里要注意在布局文件中加上:

xmlns:app="http://schemas.android.com/apk/res-auto"

加上了这一句代码才能使用自定义属性,并且为其命名为app,当然你也可以命名为其他的,然后就可以为自定义控件添加自定义属性了,你也可以为属性设置其他值。注意,虽然在布局文件中为控件设置了自定义属性,但这还不够,我门还需要在自定义控件的初始化中去获取这些值,所以我们更改上面的ToggleButton.java文件中的部分代码如下:

private void doInit(Context context, AttributeSet attrs, int defStyleAttr) {   
  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToggleButton, defStyleAttr, 0);
  onColor = a.getColor(R.styleable.ToggleButton_onColor, Color.parseColor("#4ebb7f"));   
  offColor = a.getColor(R.styleable.ToggleButton_offColor, Color.parseColor("#dadbda"));
  BTN_WIDTH = a.getInteger(R.styleable.ToggleButton_btnWidth, 40);   
  BTN_HEIGHT = a.getInteger(R.styleable.ToggleButton_btnHeight, 30);   
  CIRCLE_RADIUS = a.getInteger(R.styleable.ToggleButton_circleRadius, 8);   
  LINE_WIDTH = a.getInteger(R.styleable.ToggleButton_lineHeight, 2);   
  r = Resources.getSystem();   
  setOnClickListener(this);   
  circleX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BTN_WIDTH - CIRCLE_RADIUS, r.getDisplayMetrics());
}

这里只是在doInit()方法中加入了几句代码,然后为该方法加入相应参数就可以了,至此自定义属性的工作就完成了,然后运行一下,来看一下运行效果吧:

togglebutton2.gif

ok,到这里所以介绍就结束了,代码中可能有不足之处,欢迎大家批评指正,共同学习!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容

  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,689评论 22 664
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,297评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,985评论 4 60
  • 如果这个世界真有奇迹,那也只是努力的另外一个名字。遇不到真爱,是因为你活得还不够漂亮,只有当你的脸能够闪烁出迷人的...
    王莉Lydia阅读 266评论 0 1
  • 时光如梭,一转眼你长大成了一个男子汉,伴随着你的成长,我们有过辛苦,烦恼,但更多是感受了一家人在一起快乐和...
    勿忘心安whh阅读 420评论 0 2