自定义View,自定义Switch样式
最近在项目中要用到一个开关功能,控制是否要打开app的手势密码功能,想到android自带的两个原生控件Switch与ToggleButton,根据ui设计图最后选择了使用Switch。
由于不同的系统有自己定义的不同的Switch样式,所以导致一个问题,在不同的设备上显示出来的switch样子并不是一样子的,并且如果你的ui设计师很注重你的还原度的话,使用默认的Switch基本上是不可能实现的。
刚开始想实现这个功能是想通过自定义样式去实现一个统一的展示ui,自定义样式的步骤:
1.Switch控件支持设置Switch中的thumb,也就是里面那个可以滑动的部分,属性为android:thumb="";然后还有Switch的背景,不同选中状态有不同的背景颜色,属性为android:track=""。Switch本身也有checked状态,这就不免让我们想到了radiobutton控件,通过代码设置选择器按钮来实现不同的状态下的不同展示效果。下面上代码:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="15dp" android:height="15dp" android:gravity="center_vertical|right" android:right="3dp" android:left="3dp">
<shape android:shape="rectangle">
<corners android:radius="100dp"/>
<solid android:color="@color/dab96b"/>
</shape>
</item>
</layer-list>
通过width和height设置中心滑动圆的大小,left和right设置圆到边框的距离(ps:经过试验,当radius设置大小大于长款中的最小值时,默认就会让小的那一边变成半圆,如果长款相等的话那就成了一个圆形,大家可以自己试试)。
这个是选中的情况下滑动圆的样式,不选中下的样式就不贴出来了。然后是背景:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:bottomLeftRadius="12dp" android:bottomRightRadius="12dp"
android:topLeftRadius="12dp" android:topRightRadius="12dp"/>
<solid android:color="@color/eddcb5"/>
<size android:height="24dp" android:width="38dp"/>
</shape>
样式也差不多,通过<size>标签设置背景的大小,记得与上面滑动圆进行匹配。
样式写完了,最后只需要用到Switch样式里面就好了,效果也成功出来了。然而你以为这样就完了吗,不可能的。。
android最经常遇到的问题就是是配上的问题了,在现在一般的手机上这里的样式展示完全是没有问题的。然而如果版本过低的话,很多情况下size所设置的宽高是不起作用的,这种情况在一些机型上很明显,所以你的样式在这种设备上面展示下来的话根本不行,完全没有效果,还不如用原生的。。
所以再经历这么个问题以后,我选择了另外一种方案,通过自定义view来实现一个switch开关。
首先考虑自定义一个view需要哪些东西:
1.需要画外边框
2.需要画里面滑动的圆点
3.需要画里面的填充色
看上去其实挺简单的,实际上也确实比较简单,不过在自定义的过程中需要记得把动画加上去,因为switch切换状态的过程是有一个动画效果的。其他话不多说,上代码:
public class Switch extends View {
private Context mContext;
private int mHeight,mWidth;
private Path mPath;
private Paint mStrokePaint; //边框画笔
private Paint mSolidPaint; //填充色画笔
private Paint mCirclePaint; //小圆球画笔
private float mMarginLeft; //小圆球到左边距离
private boolean mIsCheck; //是否被选中
private final static int CIRCLEPADDING = DensityUtils.dp2px(TApplication.getInstance(),2);
private int mCircleWidth;
private int mDefaultSolidColor,mTargetSolidColor;
private int mDefaultStrokeColor,mTargetStrokeColor;
private int mDefaultCircleColor,mTargetCircleColor;
private int mSolidColor_;
private int mCircleColor_;
private int mStrokeColor_;
private ObjectAnimator mTranslateAnim, mSolidColorAnim,mCircleColorAnim,mStrokeColorAnim;
private int mStrokeWidth;
private int mRealWidth;
private int mRealHeight;
public interface OnCheckedChange{
void onCheckChange(boolean isChecked);
}
public Switch(Context context) {
this(context,null);
}
public Switch(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public Switch(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
TypedArray a = mContext.obtainStyledAttributes(attrs,R.styleable.Switch);
mDefaultStrokeColor = a.getColor(R.styleable.Switch_default_stroke_color, Color.BLACK);
mTargetStrokeColor = a.getColor(R.styleable.Switch_target_stroke_color, Color.BLACK);
mDefaultCircleColor = a.getColor(R.styleable.Switch_default_circle_color, Color.RED);
mDefaultSolidColor = a.getColor(R.styleable.Switch_default_solid_color, Color.WHITE);
mTargetCircleColor = a.getColor(R.styleable.Switch_target_circle_color, Color.RED);
mTargetSolidColor = a.getColor(R.styleable.Switch_target_solid_color, Color.WHITE);
mIsCheck = a.getBoolean(R.styleable.Switch_checked,false);
mSolidColor_ = mIsCheck ? mTargetSolidColor : mDefaultSolidColor;
mCircleColor_ = mIsCheck ? mTargetCircleColor : mDefaultCircleColor;
mStrokeColor_ = mIsCheck ? mTargetStrokeColor : mDefaultStrokeColor;
mTranslateAnim = initAnim("mMarginLeft",new FloatEvaluator());
mSolidColorAnim = initAnim("mSolidColor_",new ArgbEvaluator());
mCircleColorAnim = initAnim("mCircleColor_",new ArgbEvaluator());
mStrokeColorAnim = initAnim("mStrokeColor_",new ArgbEvaluator());
mStrokeWidth = DensityUtils.dp2px(mContext, 2f);
mMarginLeft = 0;
mPath = new Path();
mCirclePaint = new Paint();
mStrokePaint = new Paint();
mSolidPaint = new Paint();
initPaint(mCirclePaint,mSolidPaint,mStrokePaint);
mStrokePaint.setStyle(Paint.Style.STROKE);
mTranslateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMarginLeft = (float)(animation.getAnimatedValue());
postInvalidate();
}
});
mCircleColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCircleColor_ = (int) animation.getAnimatedValue();
postInvalidate();
}
});
mSolidColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mSolidColor_ = (int) animation.getAnimatedValue();
postInvalidate();
}
});
mStrokeColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mStrokeColor_ = (int) animation.getAnimatedValue();
postInvalidate();
}
});
}
private ObjectAnimator initAnim(String propertyName, TypeEvaluator evaluator){
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(this); //这里千万记住别写错,楼主因为写成了setObjectValue被坑了好几个小时
anim.setPropertyName(propertyName);
anim.setEvaluator(evaluator);
return anim;
}
public void initPaint(Paint... paints){
for (int i = 0; i < paints.length; i++) {
paints[i].setAntiAlias(true);
paints[i].setStrokeWidth(mStrokeWidth);
}
}
public void startAnim(boolean isCheck){
if (isCheck){
mCircleColorAnim.setIntValues(mDefaultCircleColor,mTargetCircleColor);
mTranslateAnim.setFloatValues(0,mWidth - mHeight);
mSolidColorAnim.setIntValues(mDefaultSolidColor,mTargetSolidColor);
mStrokeColorAnim.setIntValues(mDefaultStrokeColor,mTargetStrokeColor);
}else {
mCircleColorAnim.setIntValues(mTargetCircleColor,mDefaultCircleColor);
mTranslateAnim.setFloatValues(mWidth - mHeight,0);
mSolidColorAnim.setIntValues(mTargetSolidColor,mDefaultSolidColor);
mStrokeColorAnim.setIntValues(mTargetStrokeColor, mDefaultStrokeColor);
}
AnimatorSet set = new AnimatorSet();
set.setDuration(200);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.playTogether(mTranslateAnim,mCircleColorAnim,mSolidColorAnim,mStrokeColorAnim);
set.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mWidth == 0){
mWidth = getWidth();
mHeight = getHeight();
mRealWidth = mWidth - mStrokeWidth * 2;
mRealHeight = mHeight - mStrokeWidth * 2;
mMarginLeft = mIsCheck ? mWidth - mHeight : 0;
mCircleWidth = mHeight - CIRCLEPADDING * 2 - mStrokeWidth * 2;
}
if (mWidth <= 0 || mHeight <= 0)return;
mPath.reset();
mSolidPaint.setColor(mSolidColor_);
mStrokePaint.setColor(mStrokeColor_);
mCirclePaint.setColor(mCircleColor_);
mPath.addRoundRect(mStrokeWidth,mStrokeWidth,mRealWidth + mStrokeWidth,mRealHeight + mStrokeWidth,mRealHeight / 2,mRealHeight / 2, Path.Direction.CW);
canvas.drawPath(mPath,mStrokePaint);
canvas.drawPath(mPath,mSolidPaint);
canvas.drawCircle(mHeight / 2.0f + mMarginLeft,mHeight / 2f,mCircleWidth / 2,mCirclePaint);
}
public void setCheck(boolean checked){
this.setCheck(checked,true);
}
private void setCheck(boolean checked,boolean fromOut){
boolean flag = mIsCheck == checked;
mIsCheck = checked;
if (mListener != null && !flag){
mListener.onCheckChange(mIsCheck);
}
if (!fromOut){
startAnim(mIsCheck);
}else {
mSolidColor_ = mIsCheck ? mTargetSolidColor : mDefaultSolidColor;
mCircleColor_ = mIsCheck ? mTargetCircleColor : mDefaultCircleColor;
mStrokeColor_ = mIsCheck ? mTargetStrokeColor : mDefaultStrokeColor;
mMarginLeft = mIsCheck ? mWidth - mHeight : 0;
postInvalidate();
}
}
public float getMarginLeft() {
return mMarginLeft;
}
private OnCheckedChange mListener;
public void setOnCheckedChangeListener(OnCheckedChange listener){
mListener = listener;
}
public void setMarginLeft(float marginLeft) {
mMarginLeft = marginLeft;
}
public int getSolidColor_() {
return mSolidColor_;
}
public void setSolidColor_(int solidColor_) {
mSolidColor_ = solidColor_;
}
public int getCircleColor_() {
return mCircleColor_;
}
public void setCircleColor_(int circleColor_) {
mCircleColor_ = circleColor_;
}
public int getStrokeColor_() {
return mStrokeColor_;
}
public void setStrokeColor_(int strokeColor_) {
mStrokeColor_ = strokeColor_;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (mTranslateAnim.isRunning()){
return false;
}
return true;
case MotionEvent.ACTION_CANCEL:
return false;
case MotionEvent.ACTION_UP:
setCheck(!mIsCheck,false);
return true;
}
return true;
}
}
这里用到了四个动画,一个是滑块滑动的动画,一个是滑块颜色的渐变动画,还有事边框颜色的渐变动画以及填充色的颜色渐变动画,这些颜色都可以通过xml设置自定义属性设置进来。这里有个地方需要注意,楼主标注的地方有两个问题,
1:如果你使用的是setObjectValue()方法的话在现在主流的机型上是不会有问题的,可以很好的展示出来,但是如果你使用的是低版本的手机,那么就会提示你NullPointerException。这很容易误导是因为版本兼容性问题,其实并不是,属性动画确实存在一些版本兼容性的问题,不过是使用颜色动画的时候使用了ofArgb这个方法去初始化一个颜色动画,那么就会有这个问题。因为ofArgb是api21的方法,其他的那些兼容性问题则是要深究到android3.*版本,现在市场上这些版本基本没有了,如果实在要适配的话可以选择添加一个三方包NineOldAndroids这样就可以兼容android3.*以及以下版本。
2:path中有很多方法也是属于api21的方法比如上方的path.addRoundRect()方法,楼主这里后面通过使用path.lineTo()配合path.arcTo()方法画出来了一个类似操场跑道的图案,path.addRoundRect()也是添加一个跑道图案这种效果的方法。使用者请注意path.arcTo(float left,float top,float right,float bottom,float startAngle,float sweepAngle,boolean forceMoveTo)这个方法才是api11的方法,其他两个重载都是属于api21的,也会存在兼容性问题,使用的时候还请注意,如果大家对一些api方法不清楚可以自行去查阅。
附上效果图: