自定义view-滑动开关
2017-11-17 17:00
423 查看
介绍
前段时间,我看到了一篇关于可滑动开关Switch组件的文章,效果图如下:思路也挺简单的:这个控件主要由田径场式背景和滑块组成。他将田径场式背景分为3部分,最左边的半圆,中间的两条直线部分和最右边的半圆。假设线的宽度为lx,半圆的半径则为lx的一半,通过监听touch事件,不停的绘制两个半圆和两条线段、滑块,从而达到滑块跟着手指滑动的显示效果。
虽然效果是实现了,但是田径场式背景被拆分绘制,我感觉还是有点繁琐,不统一,我就想有没有什么办法可以一次性将这个背景画出来?答案是有的(你这不是废话~)。
两种方法的差异
我们都知道在Android中有提供用来绘制各种图案的类:Path。Path主要用于绘制复杂的图形轮廓,比如折线,圆弧以及各种复杂图案。现在再来看这个田径场式背景,说白了就是一个圆角矩形形状,这个圆角设置足够大就可以了。我们可以使用Path中的addRoundRect(RectF rect, float[] radii, Direction dir)绘制出圆角矩形。其中第二个参数是8个值(矩形的4个角)的数组,4对[ x,y ]半径。实现
这个控件支持自定义属性:<?xml version="1.0" encoding="utf-8"?> <resources> <!-- JYSwitchButton的自定义属性 openColor:开启状态的颜色 closeColor:关闭状态的颜色 circleColor:滑动圆形图标的颜色 openText:开启状态的文本 closeText:关闭状态的文本 openTextColor:开启状态的文本颜色 closeTextColor:关闭状态的文本颜色 textSize:字体大小 --> <declare-styleable name="switchbutton"> <attr name="openColor" format="integer"></attr> <attr name="closeColor" format="integer"></attr> <attr name="circleColor" format="integer"></attr> <attr name="openText" format="string"></attr> <attr name="closeText" format="string"></attr> <attr name="openTextColor" format="integer"></attr> <attr name="closeTextColor" format="integer"></attr> <attr name="textSize" format="dimension"></attr> </declare-styleable> </resources>
也提供了一些设置的方法:
* 支持自定义颜色值setOpenColor/setCloseColor/setCircleColor; * 支持设置偏移量setOffset; * 支持设置初始状态changeState; * 支持获取默认状态getDefaultState; * 支持开启/关闭状态的监听setListener(OnSwitchStateChangeListener); * 支持设置滑动圆形图标的边距setCirclePadding; * 支持设置开启/关闭文本和颜色setOpenText、setCloseText、setOpenTextColor、setCloseTextColor * 支持设置文本字体大小setTextSize
话不多说了,撸代码去:
package com.ha.cjy.jyswitchbutton; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * 滑动开关按钮 * * Created by cjy on 17/11/14. */ public class JYSwitchButton extends View { private Context mContext; //画笔 private Paint mPaint; //移动距离 private int mCurrentX; //宽度 private int mViewWidth; //高度 private int mViewHeight; //Y中心 private int mCenterY; //左边圆的X中心点 private int mStartX; //右边圆的X中心点 private int mEndX; //滑动圆形图标的半径 private int mRadius; //滑动圆形图标的边距 private int mCirclePadding = 2; //是否已经初始化好宽高了 private boolean mIsInit = false; //是否开启,默认是关闭状态 private boolean mIsOpen = false; //状态监听器 private OnSwitchStateChangeListener mListener; //矩形4个角的半径坐标,左上,右上,右下,左下(顺时针) private float[] mRadiusArr = new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f}; //偏移量,用来控制view的显示大小 private int mOffset = 20; //开启状态的背景色 private int mOpenColor = Color.BLUE; //关闭状态的背景色 private int mCloseColor = Color.GRAY; //滑动圆形图标的颜色 private int mCircleColor = Color.LTGRAY; //字体最大值、最小值 private float mTextMaxSize = 32; private float mTextMinSize = 10; //开启/关闭文本 private String mOpenText=""; private String mCloseText=""; //开启/关闭文本的颜色 private int mOpenTextColor = Color.WHITE; private int mCloseTextColor = Color.WHITE; public JYSwitchButton(Context context) { this(context, null); } public JYSwitchButton(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public JYSwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; getProperty(context,attrs); init(); defaultRoundRadius(); } /** * 初始化操作 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(Color.BLACK); } /** * 获取自定义属性 * @param context * @param attrs */ private void getProperty(Context context, AttributeSet attrs){ TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.switchbutton); mOpenColor = typedArray.getInteger(R.styleable.switchbutton_openColor,mOpenColor); mCloseColor = typedArray.getInteger(R.styleable.switchbutton_closeColor,mCloseColor); mCircleColor = typedArray.getInteger(R.styleable.switchbutton_circleColor,mCircleColor); mOpenText = typedArray.getString(R.styleable.switchbutton_openText); mCloseText = typedArray.getString(R.styleable.switchbutton_closeText); if (mOpenText == null) mOpenText = ""; if (mCloseText == null) mCloseText = ""; mOpenTextColor = typedArray.getInteger(R.styleable.switchbutton_openTextColor,mOpenTextColor); mCloseTextColor = typedArray.getInteger(R.styleable.switchbutton_closeTextColor,mCloseTextColor); mTextMaxSize = typedArray.getDimension(R.styleable.switchbutton_textSize,mTextMaxSize); //取完属性,记得释放 typedArray.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mViewWidth = getMeasuredWidth(); mViewHeight = getMeasuredWidth() / 2 + mOffset; setMeasuredDimension(mViewWidth, mViewHeight); mCenterY = mViewHeight / 2 - mOffset; mRadius = mViewHeight / 2 - mOffset; mCurrentX = mRadius; mStartX = mRadius; mEndX = mViewWidth - mRadius; mIsInit = true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mIsInit) { //控制滑动区域 mCurrentX = mCurrentX > mStartX ? mCurrentX : mStartX; mCurrentX = mCurrentX < mEndX ? mCurrentX : mEndX; Path path = new Path(); RectF rectF = new RectF(); rectF.top = 0; rectF.left = 0; rectF.right = mViewWidth; rectF.bottom = mRadius * 2; //画圆角矩形背景图 path.addRoundRect(rectF, mRadiusArr, Path.Direction.CW); if (mIsOpen) { //左边色块 mPaint.setColor(mOpenColor); canvas.drawPath(path, mPaint); //绘制文本 drawText(canvas,mOpenText,mOpenTextColor); } else { //右边色块 mPaint.setColor(mCloseColor); canvas.drawPath(path, mPaint); //绘制文本 drawText(canvas,mCloseText,mCloseTextColor); } //滑动圆形 mPaint.setColor(mCircleColor); int realRadius = mRadius-mCirclePadding; if (realRadius < mRadius/2 ){ realRadius = mRadius/2; }else if(realRadius > mRadius){ realRadius = mRadius; } canvas.drawCircle(mCurrentX, mCenterY,realRadius, mPaint); } } /** * 绘制文本 * @param canvas * @param text 文本 * @param color 文本颜色 */ private void drawText(Canvas canvas,String text,int color){ if(text.isEmpty()) return; mPaint.setColor(color); mPaint.setTextSize(mTextMaxSize); //文本宽度 int textWidth = mViewWidth-mRadius*2; float trySize = mTextMaxSize; //根据文本宽度,字体大小适配 while (mPaint.measureText(text)<textWidth){ trySize += 1; mPaint.setTextSize(trySize); } while (mPaint.measureText(text)>textWidth){ trySize -= 1; if (trySize < mTextMinSize){ trySize = mTextMinSize; break; } mPaint.setTextSize(trySize); } mPaint.setTextSize(px2sp(mContext,trySize)); int x = mIsOpen?mStartX+10:mEndX-mRadius-10; Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); int textHeight = (int)(fontMetrics.descent-fontMetrics.ascent); int baseline = (int) (mCenterY+(mCenterY*1.0/3.0)); canvas.drawText(text,x,baseline,mPaint); } /** * 默认的圆角数据 */ private void defaultRoundRadius() { mRadiusArr[0] = 120; mRadiusArr[1] = 120; mRadiusArr[2] = 120; mRadiusArr[3] = 120; mRadiusArr[4] = 120; mRadiusArr[5] = 120; mRadiusArr[6] = 120; mRadiusArr[7] = 120; } @Override public boolean onTouchEvent(MotionEvent event) { int lastX = 0; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { lastX = (int) event.getX(); break; } case MotionEvent.ACTION_MOVE: { //滑动的偏移量 mCurrentX = (int) (event.getX() - lastX); break; } case MotionEvent.ACTION_UP: { mCurrentX = (int) (event.getX() - lastX); if (mCurrentX > mViewWidth / 2) {//从左到右滑动 mCurrentX = mEndX; if (mIsOpen == false) { mIsOpen = true; if (mListener != null) { mListener.onSwitchStateChange(mIsOpen); } } } else {//从右向左滑动 mCurrentX = mStartX; if (mIsOpen == true) { mIsOpen = false; if (mListener != null) { mListener.onSwitchStateChange(mIsOpen); } } } break; } } postInvalidate(); return true; } /** * 获取默认状态 * @return */ public boolean getDefaultState(){ return this.mIsOpen; } /** * 设置状态:开启/关闭 * @param isOpen 是否开启 true-开启 false-关闭 */ public void changeState(boolean isOpen){ this.mIsOpen = isOpen; postDelayed(new Runnable() {//延迟100毫秒,等计算好宽高再进行重新绘制 @Override public void run() { if (mIsInit) { if (mIsOpen) { mCurrentX = mEndX; } else { mCurrentX = mStartX; } } invalidate(); } },100); } /** * 设置滑动圆形图标的边距 * @param padding */ public void setCirclePadding(int padding){ this.mCirclePadding = padding; } /** * 设置开启状态的颜色 * @param color */ public void setOpenColor(int color){ this.mOpenColor = color; } /** * 设置关闭状态的颜色 * @param color */ public void setCloseColor(int color){ this.mCloseColor = color; } /** * 设置滑动圆形的颜色 * @param color */ public void setCircleColor(int color){ this.mCircleColor = color; } /** * 设置开启状态的文本 * @param value */ public void setOpenText(String value){ this.mOpenText = value; } /** * 设置关闭状态的文本 * @param value */ public void setCloseText(String value){ this.mCloseText = value; } /** * 设置开启状态的文本 * @param color */ public void setOpenTextColor(int color){ this.mOpenTextColor = color; } /** * 设置关闭状态的文本颜色 * @param color */ public void setCloseTextColor(int color){ this.mCloseTextColor = color; } /** * 设置字体大小 * @param textSize */ public void setTextSize(int textSize){ this.mTextMaxSize = textSize; } /** * 设置偏移量 * @param offset 偏移量 */ public void setOffset(int offset){ this.mOffset = offset; } /** * 设置监听器 * @param listener */ public void setListener(OnSwitchStateChangeListener listener) { this.mListener = listener; } /** * 将px值转换为sp值,保证文字大小不变 * @param context * @param pxValue * @return */ private float px2sp(Context context, float pxValue) { float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (pxValue / fontScale); } /** * 开启/关闭状态的监听 */ interface OnSwitchStateChangeListener { /** * 状态变化的事件 * @param isOpen true-开启 false-关闭 */ void onSwitchStateChange(boolean isOpen); } }
以上注释写的很详细了。主要的有几点:
重写了onMeasure方法,使控件的高度依赖于控件宽度,保证控件的宽高比;
控制好滑块的滑动范围;
根据文本宽度进行字体大小的适配;
设置状态监听器OnSwitchStateChangeListener,在控件内部定义其对象和开放方法setListener,以便外部进行状态的监听和调用;
设置状态(changeState)的时候会去重新绘制,但是这时绘制如果控件的宽高还没有计算出来,就会导致数据不正确,滑块的位置就会显示错误,所以需要延迟一段时间去执行绘制工作。
使用
布局文件:在该布局中添加该控件即可,因为有自定义的属性,需要声明其命名空间:xmlns:jy="http://schemas.android.com/apk/res/com.ha.cjy.jyswitchbutton"<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:jy="http://schemas.android.com/apk/res/com.ha.cjy.jyswitchbutton" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.ha.cjy.jyswitchbutton.JYSwitchButton android:id="@+id/btnSwitch" android:layout_marginTop="40dp" android:layout_marginLeft="40dp" android:layout_width="60dp" android:layout_height="wrap_content" jy:textSize="40sp" jy:openColor="@color/colorAccent" jy:closeColor="@color/colorPrimary" jy:circleColor="@color/colorWhite" jy:openText="开" jy:closeText="关" jy:openTextColor="@color/colorWhite" jy:closeTextColor="@color/colorWhite"/> </LinearLayout>
Activity代码如下:
package com.ha.cjy.jyswitchbutton; import android.graphics.Color; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.Toast; public class MainActivity extends AppCompatActivity implements JYSwitchButton.OnSwitchStateChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //滑动开关控件 JYSwitchButton btnSwitch = (JYSwitchButton) findViewById(R.id.btnSwitch); // btnSwitch.changeState(true); // Log.i("state",btnSwitch.getDefaultState()+""); // btnSwitch.setOpenColor(Color.RED); // btnSwitch.setCloseColor(Color.GRAY); // btnSwitch.setCircleColor(Color.BLUE); btnSwitch.setListener(this); } @Override public void onSwitchStateChange(boolean isOpen) { Toast.makeText(this,"状态:"+(isOpen?"开启":"关闭"),Toast.LENGTH_SHORT).show(); } }
总结
其实还有一些细节问题我没有在这篇文章上讲出,比如文本绘制的baseline实现还有些问题,希望感兴趣的同学可以自行研究代码并完善它。项目地址传送门摸我的github。相关文章推荐
- Android自定义View-------IOS风格的滑动开关
- android自定义滑动开关控件,自定义view
- 自定义View滑动开关
- Android自定义View示例(二)—滑动开关
- Android自定义View示例(二)—滑动开关
- Android自定义view 滑动开关 支持左右滑动 适用于listview
- 自定义view实现开关按钮并监听(有滑动效果)
- Android自定义View-------IOS风格的滑动开关
- android自定义View之滑动开关SlideButton
- Android 自定义View之随手指滑动的ToggleButton
- Android自定义View实现随手势滑动控件
- Android简易实战教程--第二十七话《自定义View入门案例之开关按钮详细分析》
- android 自定义ImageView实现图片手势滑动,多点触摸放大缩小效果
- android 滑动删除的listview(自定义view)
- Android 自定义 HorizontalScrollView 打造再多图片(控件)也不怕 OOM 的横向滑动效果
- Android 自定义ViewPager设置屏蔽左右滑动事件
- 自定义类似于ViewPager的可上下滑动切换效果的视图
- 使用自定义RadioButton和ViewPager实现TabHost效果和带滑动的页卡效果
- Android 自定义View学习(3)--仿IOS风格滑动按钮
- 自定义滑动开关