Android View 仿iOS SwitchButton
2015-07-09 15:30
661 查看
自学android差不多有一年了,从最初的小白菜鸟,摸爬滚打,看大神们的博客,android官网的api,某网站的视频教学,github开源项目。奋斗这么久隐隐感觉自己可以脱离新手的身份了,交出这篇文章权当作andriod小学水准的毕业典礼。
iOS SwitchButton。 说实话功能也不过就个开关功能而已。但是为什么让人感觉不错,因为效果看起来赏心悦目呀:
~~~~~~~~~~~~~~~~~
好了,为了实现它,首先要分析它。
这个按钮被我玩来玩去最后静止的时候都会停留在下面的样子:
那么怎么把这个实现出来呢?观察一番会发现上图是极其规律的,只是一些基础几何图形的组合。所以具备纯代码实现可能性,同时如果用图片实现这个效果需要对应的png文件辅助,相信大家一定觉得麻烦。
那么就把它画出来!如何画出来的分析路线:
1. 位置固定不变的背景,像田径场一样的形状。
2. 圆圆的按钮,压在“田径场”上面。 【之后背景全称作"田径场",比较形象,不服solo 囧】
3. 淡淡的按钮阴影,夹在他们之间。
ps:哎,我还是分析的那么透彻,赞一个。
开始动手!新建
[html]
view plaincopyprint?
public class SwitchView extends View { public SwitchView(Context context) { this(context, null); } public SwitchView(Context context, AttributeSet attrs) { super(context, attrs); setLayerType(LAYER_TYPE_SOFTWARE, null); } }
之后为自己确定大小~ 截图量了一下 算上阴影宽高比例是 149:92 。即 height = width * 0.65 左右
[java]
view plaincopyprint?
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = (int) (widthSize * 0.65f); setMeasuredDimension(widthSize, heightSize); }
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(0xffcccccc); }
ps: 如果我说,剩下的大家自己思考,你们不会打我吧。
第二步,画田径场!
[java]
view plaincopyprint?
private final Paint paint = new Paint();
private final Path sPath = new Path();
private int mWidth, mHeight;
private float sWidth, sHeight;
private float sLeft, sTop, sRight, sBottom;
private float sCenterX, sCenterY;
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w; // 视图自身宽度
mHeight = h; // 视图自身高度
sLeft = sTop = 0; // 田径场 左和上的坐标
sRight = mWidth; // 田径场 右占自身的全部
sBottom = mHeight * 0.8f; // 田径场底部 占全身的百分之八十, 下面预留百分之二十的空间画按钮阴影。
sWidth = sRight - sLeft; // 田径场的宽度
sHeight = sBottom - sTop; // 田径场的高度
sCenterX = (sRight + sLeft) / 2; // 田径场的X轴中心坐标
sCenterY = (sBottom + sTop) / 2; // 田径场的Y轴中心坐标
RectF sRectF = new RectF(sLeft, sTop, sBottom, sBottom);
sPath.arcTo(sRectF, 90, 180);
sRectF.left = sRight - sBottom;
sRectF.right = sRight;
sPath.arcTo(sRectF, 270, 180);
sPath.close(); // path准备田径场的路径
}
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setAntiAlias(true);
paint.setStyle(Style.FILL);
paint.setColor(0xffcccccc);
canvas.drawPath(sPath, paint); // 画出田径场
paint.reset();
}
由于田径场不是基础规则的几何图形,只好交给万能的path对象啦。 path 左右加两段圆弧中间一连接,感谢api朋友 [arcTo]
值得注意的是预留出按钮阴影的位置。
既然都做到这个程度了,那就把背景的效果做完嘛。有什么效果呢。
额,这么搓逼,不过确实是这个效果,也许整体完成后就是另一番风景吧。orz
这个效果用大腿想象就知道是点击触发的,既然如此:
[java]
view plaincopyprint?
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: sAnim = 1; // 动画标示 isOn = !isOn; // 状态标示 , 开关 invalidate(); break; } return super.onTouchEvent(event); }
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setAntiAlias(true); paint.setStyle(Style.FILL); paint.setColor(0xffcccccc); canvas.drawPath(sPath, paint); // 画出田径场 sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 // draw logic - 动态改变绘制参数,达到动画效果 paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 }
分析:不用分析了,一张图大家都懂的~
看不出这是两个田径场的说明你只是地球人而已。对 就是一个白色的田径场压在了灰色的上面而已,然后大小随某个东西【anim标示】的变化而变化。
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // ... sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 final float scale = 0.98f * (isOn ? sAnim : 1 - sAnim); //缩放大小参数随sAnim变化而变化 canvas.save(); canvas.scale(scale, scale, sCenterX, sCenterY); paint.setColor(0xffffffff); canvas.drawPath(sPath, paint); canvas.restore(); paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 // ... }
为了完美适配各种分辨率,这个0.98应该被一个变量替换。
对比完成的效果图会使用感觉有点别扭,别扭的原因就是缩放的中心位置。
应该在4个箭头的起点处。就是那里:田径场的宽度 减去 按钮的一个半径,在减去按钮距离右边的间隔什么的。
那么我就要把按钮的一些参数给算好咯~
[java]
view plaincopyprint?
private float bRadius, bStrokWidth; private float bWidth; private float bLeft, bTop, bRight, bBottom; private float sScaleCenterX; private float sScale; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // ... bLeft = bTop = 0; bRight = bBottom = sBottom; // 和田径场同高,同宽的节奏, 没错包裹圆形的肯定是个正方形是小孩子都知道的。 bWidth = bRight - bLeft; final float halfHeightOfS = (sBottom - sTop) / 2; bRadius = halfHeightOfS * 0.9f; // 按钮的半径 bStrokWidth = 2 * (halfHeightOfS - bRadius); // 按钮的边框 sScale = 1 - bStrokWidth / sHeight; //替换之前的0.98< sScaleCenterX = sWidth - halfHeightOfS; // ... } @Override protected void onDraw(Canvas canvas) { // ... canvas.scale(scale, scale, sScaleCenterX, sCenterY); }
大家估计看出我的命名规则啦: s开头的表示田径场,b开头的表示按钮。
那么效果理所当然的变成下图所示。
继续补上按钮。
[java]
view plaincopyprint?
canvas.save(); paint.setStyle(Style.FILL); paint.setColor(0xffffffff); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮白底 paint.setStyle(Style.STROKE); paint.setColor(0xffdddddd); paint.setStrokeWidth(bStrokWidth); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮灰边 canvas.restore();
之后动起来~这里依旧玩canvas。
相对于scale缩放方法, 他还有translate平移和retate旋转方法,是不是很牛逼~ 。这里需要的是平移方法
[java]
view plaincopyprint?
bTranslateX = sWidth - bWidth; final float translate = bTranslateX * (isOn ? 1 - sAnim : sAnim); // 平移距离参数随sAnim变化而变化 canvas.translate(translate, 0);
好了 接下来进入我们的正题内容:
对于按钮的开关不见得是我们想开就开,想关就关的。举生活中常见的例子:小明家跳闸了,然后小明去把闸往上推,然后发现推不上去自动弹回来,再推,再弹 *n。
之后小明终于受不了打电话给闪电侠,通知它过来修。 电话拨出去的时候,并不是马上被接听,同时也有可能没人接。。。编不下去了。
特么生活的现实就是比理想残酷,小明我不是故意黑你的。
为了能够让大家明确的感受到按钮与之前的不同,所以换了一个明显的颜色~
那么转换为程序语言这个按钮开关应该是4种状态:
已经关闭。 已经打开。准备关闭。准备打开。
所以我们为switchView 添加4种状态, 之前的 变量isOn可以退休了。
[java]
view plaincopyprint?
private final int STATE_SWITCH_ON = 4; // 已经打开 private final int STATE_SWITCH_ON2 = 3; // 准备关闭 private final int STATE_SWITCH_OFF2 = 2; // 准备打开 private final int STATE_SWITCH_OFF = 1; // 已经关闭 private int state = STATE_SWITCH_OFF; private int lastState = state;
细心的朋友会发现,田径场还是那个田径场,而按钮不再是那个按钮。它可以变瘪 orz
so canvas.drawCircle就不行啦, 好了我们再来看一张图。
看不出这是两个田径场的说明你仍然只是地球人而已。对 就是一个紫色的田径场压在了灰色的上面而已,然后形状随某个东西【anim标示】的变化而在圆形和田径场之间变化而已。
[java]
view plaincopyprint?
private final Path bPath = new Path();
private final RectF bRectF = new RectF();
private float bOffset;
private float bOnLeftX, bOn2LeftX, bOff2LeftX, bOffLeftX;
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// ...
final float halfHeightOfS = (sBottom - sTop) / 2;
bRadius = halfHeightOfS * 0.9f;
bOffset = bRadius * 0.3f; // 有多瘪~ 半径的三分之一左右
bStrokWidth = 2 * (halfHeightOfS - bRadius);
bOnLeftX = sWidth - bWidth; // 在已经开启状态下,按钮距离自身左端的距离
bOn2LeftX = bOnLeftX - bOffset;// 在准备关闭状态下,按钮距离自身左端的距离
bOffLeftX = 0;// 在已经关闭状态下,按钮距离自身左端的距离
bOff2LeftX = 0;// 在准备开启状态下,按钮距离自身左端的距离
bRectF.left = bLeft; // 替代 circle性质的按钮,改为path性质的按钮,以提供“变瘪”的功能 。囧
bRectF.right = bRight;
bRectF.top = bTop + bStrokWidth / 2;
bRectF.bottom = bBottom - bStrokWidth / 2;
// ...
}
那么同时再次观察刚才的gif图片。会发现在按钮变瘪的时候(已经开启到准备关闭,已经关闭到准备开启),田径场是无动作的。
所以anim标示,存在2个~ 之前这个参数叫sAnim而不是 anim,估计大家也猜出来啦。这里在添加一个 bAnim。
[java]
view plaincopyprint?
private float sAnim, bAnim;
重新设定动画的触发时机!!!
[java]
view plaincopyprint?
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: lastState = state; if (state == STATE_SWITCH_OFF) { bAnim = 1; state = STATE_SWITCH_OFF2; } else if (state == STATE_SWITCH_OFF2) { bAnim = 1; sAnim = 1; // 只有在准备打开,并且结果成功的时候 state = STATE_SWITCH_ON; } else if (state == STATE_SWITCH_ON) { bAnim = 1; state = STATE_SWITCH_ON2; } else if (state == STATE_SWITCH_ON2) { bAnim = 1; sAnim = 1; // 和在准备关闭,并且结果成功的时候才会 触发背景的变化。 state = STATE_SWITCH_OFF; } invalidate(); break; } return super.onTouchEvent(event); }
这个事件处理的逻辑依旧容易,invalidate嘛。抬起手指重绘嘛,那关键问题就再一次的抛给了onDraw同志。
onDraw同志需要根据我们给的anim标示来处理按钮的动画效果咯
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setAntiAlias(true);
paint.setStyle(Style.FILL);
paint.setColor(0xffcccccc);
canvas.drawPath(sPath, paint); // 最底下的田径场
sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0;
bAnim = bAnim - 0.1f > 0 ? bAnim - 0.1f : 0;
final boolean isOn = (state == STATE_SWITCH_ON || state == STATE_SWITCH_ON2);
final float scale = sScale * (isOn ? sAnim : 1 - sAnim);
final float scaleOffset = (bOnLeftX + bRadius - sCenterX) * (isOn ? 1 - sAnim : sAnim);
canvas.save();
canvas.scale(scale, scale, sCenterX + scaleOffset, sCenterY); // 田径场动画的缩放中心
paint.setColor(0xffffffff);
canvas.drawPath(sPath, paint); // 这个之前讲过了
canvas.restore();
canvas.save();
final boolean isState2 = (state == STATE_SWITCH_ON2 || state == STATE_SWITCH_OFF2);
final float percent = (isState2 ? 1 - bAnim : bAnim);
calcBPath(percent); // 根据anim标示计算变瘪的按钮路径
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(bStrokWidth);
paint.setColor(0xffff00cc);
canvas.translate(calcBTranslate(bAnim), 0); // 根据anim标示计算按钮开关平移的坐标
canvas.drawPath(bPath, paint);
canvas.restore();
paint.reset();
if (sAnim > 0 || bAnim > 0) invalidate(); // 重绘的标示由1个变为了2个。
}
so,白眼狼都看出来啊。啊不,口误,Oh shi-t 笔误,明眼人都看的出来。核心逻辑丢在 calcBPath() 和 calcBTranslate()里面咯。 如果问我为什么要创建2个方法丢在里面,我只能告诉你,这是kami的启示~
[java]
view plaincopyprint?
private void calcBPath(float percent) {
bPath.reset();
bRectF.left = bLeft + bStrokWidth / 2;
bRectF.right = bRight - bStrokWidth / 2;
bPath.arcTo(bRectF, 90, 180);
bRectF.left = bLeft + percent * bOffset + bStrokWidth / 2;
bRectF.right = bRight + percent * bOffset - bStrokWidth / 2;
bPath.arcTo(bRectF, 270, 180);
bPath.close();
}
看方法内容,原来percent 通过直接影响rect来间接影响了path啊。 left和right值似乎随percent的增大而增大。这样 两个圆弧慢慢被拉开,反之缩小。
这特么谁写的,真是太精辟了。 --- 匿名
[java]
view plaincopyprint?
private float calcBTranslate(float percent) {
float result = 0;
int wich = state - lastState;
switch (wich) {
case 1: // off - off2
result = bOff2LeftX - (bOff2LeftX - bOffLeftX) * percent;
break;
case 2: // off2 - on
result = bOnLeftX - (bOnLeftX - bOff2LeftX) * percent;
break;
case -1:// on - on2
result = bOn2LeftX + (bOnLeftX - bOn2LeftX) * percent;
break;
case -2:// on2 - off
result = bOffLeftX + (bOn2LeftX - bOffLeftX) * percent;
break;
}
return result - bOffLeftX;
}
看方法内容,原来平移的结果result 是根据不同的情况进行了分类啊,真是层次分明。
这特么谁写的,66666。 ---匿名
好了,打死我也不会告诉你们,评论是我自己留下的。
做到这里几乎完成了大半,不过仍有不合理的地方。 首先从刚才的TouchEvent的代码逻辑可以看出,点击一下动一下,点击四下一个周天~
对的。 到了准备开启后,或者准备关闭后的结果不用该由控件自身决定,应该交给业务逻辑。 那么其结果是成功还是失败就不管我们的事了,只需要提供一个
执行结果动画的公开方法就OK了。 同时,calcBTranslate()的内部逻辑便需要扩充,添加对失败结果【开启不成功,关闭不成功】的处理等。
那么 定义并且创建接口~
[java]
view plaincopyprint?
public interface OnSwitchStateChangedListener {
void onStateChanged(int state);
}
private OnSwitchStateChangedListener listener = new OnSwitchStateChangedListener() {
@Override public void onStateChanged(int state) {
if (state == STATE_SWITCH_OFF2) {
toggleSwitch(STATE_SWITCH_ON);
}
else if (state == STATE_SWITCH_ON2) {
toggleSwitch(STATE_SWITCH_OFF);
}
}
};
public void setOnSwitchStateChangedListener(OnSwitchStateChangedListener listener) {
if (listener == null) throw new IllegalArgumentException("empty listener");
this.listener = listener;
}
仔细的朋友又会发现。我这里已经有个接口的默认实现啦。 实现的内容:当状态变换为准备关闭的时候 ,准备打开的时候做了一件不可告人的事情。
[java]
view plaincopyprint?
private void refreshState(int newState) {
lastState = state;
state = newState;
postInvalidate(); // 为什么postInvalidate() 而不用 invalidate()。 你猜~
}
private synchronized void toggleSwitch(int wich) {
if (wich == STATE_SWITCH_ON || wich == STATE_SWITCH_OFF) {
if ((wich == STATE_SWITCH_ON && (lastState == STATE_SWITCH_OFF || lastState == STATE_SWITCH_OFF2))
|| (wich == STATE_SWITCH_OFF && (lastState == STATE_SWITCH_ON || lastState == STATE_SWITCH_ON2))) {
sAnim = 1;
}
bAnim = 1;
refreshState(wich);
}
else {
Log.e("SwitchView_step2", "do not support state : " + wich);
}
}
toggleSwitch()。 根据当前状态和上次状态判断 在成功打开,和成功关闭的时候 开启sAnim田径场背景动画标示~ ,同时必然开始按钮的动画标示~之后重绘。
所以当这个控件没有被重新设置SwitchStateChangedListener的时候一切就是期望的那么美好。 想开就开,想关就关~
接着提供对外方法
[java]
view plaincopyprint?
public int getState() { // 获得状态 return state; } public void setState(boolean isOn) { // 设置状态 只能设置 [已经关闭] 和 [已经开启] final int wich = isOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; refreshState(wich); } public void toggleSwitch(boolean letItOn) { // 切换状态 只支持 [已经关闭] 和 [已经开启] 的切换 final int wich = letItOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; postDelayed(new Runnable() { @Override public void run() { toggleSwitch(wich); } }, 300); }
那么300行左右的代码 完成了我们的仿iOS SwitchButton 的控件 SwitchView (就不和它一个名字,不服 solo)
至于阴影效果,如何使用的相关内容:
查看SwitchView 源码
转载于:http://blog.csdn.net/bfbx5173/article/details/45191147#comments
自学android差不多有一年了,从最初的小白菜鸟,摸爬滚打,看大神们的博客,android官网的api,某网站的视频教学,github开源项目。奋斗这么久隐隐感觉自己可以脱离新手的身份了,交出这篇文章权当作andriod小学水准的毕业典礼。
iOS SwitchButton。 说实话功能也不过就个开关功能而已。但是为什么让人感觉不错,因为效果看起来赏心悦目呀:
~~~~~~~~~~~~~~~~~
好了,为了实现它,首先要分析它。
这个按钮被我玩来玩去最后静止的时候都会停留在下面的样子:
那么怎么把这个实现出来呢?观察一番会发现上图是极其规律的,只是一些基础几何图形的组合。所以具备纯代码实现可能性,同时如果用图片实现这个效果需要对应的png文件辅助,相信大家一定觉得麻烦。
那么就把它画出来!如何画出来的分析路线:
1. 位置固定不变的背景,像田径场一样的形状。
2. 圆圆的按钮,压在“田径场”上面。 【之后背景全称作"田径场",比较形象,不服solo 囧】
3. 淡淡的按钮阴影,夹在他们之间。
ps:哎,我还是分析的那么透彻,赞一个。
开始动手!新建
[html]
view plaincopyprint?
public class SwitchView extends View { public SwitchView(Context context) { this(context, null); } public SwitchView(Context context, AttributeSet attrs) { super(context, attrs); setLayerType(LAYER_TYPE_SOFTWARE, null); } }
public class SwitchView extends View { public SwitchView(Context context) { this(context, null); } public SwitchView(Context context, AttributeSet attrs) { super(context, attrs); setLayerType(LAYER_TYPE_SOFTWARE, null); } }
之后为自己确定大小~ 截图量了一下 算上阴影宽高比例是 149:92 。即 height = width * 0.65 左右
[java]
view plaincopyprint?
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = (int) (widthSize * 0.65f); setMeasuredDimension(widthSize, heightSize); }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = (int) (widthSize * 0.65f); setMeasuredDimension(widthSize, heightSize); }绘制~
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(0xffcccccc); }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(0xffcccccc); }好哒,通向胜利的第一步已经完成了。
ps: 如果我说,剩下的大家自己思考,你们不会打我吧。
第二步,画田径场!
[java]
view plaincopyprint?
private final Paint paint = new Paint();
private final Path sPath = new Path();
private int mWidth, mHeight;
private float sWidth, sHeight;
private float sLeft, sTop, sRight, sBottom;
private float sCenterX, sCenterY;
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w; // 视图自身宽度
mHeight = h; // 视图自身高度
sLeft = sTop = 0; // 田径场 左和上的坐标
sRight = mWidth; // 田径场 右占自身的全部
sBottom = mHeight * 0.8f; // 田径场底部 占全身的百分之八十, 下面预留百分之二十的空间画按钮阴影。
sWidth = sRight - sLeft; // 田径场的宽度
sHeight = sBottom - sTop; // 田径场的高度
sCenterX = (sRight + sLeft) / 2; // 田径场的X轴中心坐标
sCenterY = (sBottom + sTop) / 2; // 田径场的Y轴中心坐标
RectF sRectF = new RectF(sLeft, sTop, sBottom, sBottom);
sPath.arcTo(sRectF, 90, 180);
sRectF.left = sRight - sBottom;
sRectF.right = sRight;
sPath.arcTo(sRectF, 270, 180);
sPath.close(); // path准备田径场的路径
}
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setAntiAlias(true);
paint.setStyle(Style.FILL);
paint.setColor(0xffcccccc);
canvas.drawPath(sPath, paint); // 画出田径场
paint.reset();
}
private final Paint paint = new Paint(); private final Path sPath = new Path(); private int mWidth, mHeight; private float sWidth, sHeight; private float sLeft, sTop, sRight, sBottom; private float sCenterX, sCenterY; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; // 视图自身宽度 mHeight = h; // 视图自身高度 sLeft = sTop = 0; // 田径场 左和上的坐标 sRight = mWidth; // 田径场 右占自身的全部 sBottom = mHeight * 0.8f; // 田径场底部 占全身的百分之八十, 下面预留百分之二十的空间画按钮阴影。 sWidth = sRight - sLeft; // 田径场的宽度 sHeight = sBottom - sTop; // 田径场的高度 sCenterX = (sRight + sLeft) / 2; // 田径场的X轴中心坐标 sCenterY = (sBottom + sTop) / 2; // 田径场的Y轴中心坐标 RectF sRectF = new RectF(sLeft, sTop, sBottom, sBottom); sPath.arcTo(sRectF, 90, 180); sRectF.left = sRight - sBottom; sRectF.right = sRight; sPath.arcTo(sRectF, 270, 180); sPath.close(); // path准备田径场的路径 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setAntiAlias(true); paint.setStyle(Style.FILL); paint.setColor(0xffcccccc); canvas.drawPath(sPath, paint); // 画出田径场 paint.reset(); }
由于田径场不是基础规则的几何图形,只好交给万能的path对象啦。 path 左右加两段圆弧中间一连接,感谢api朋友 [arcTo]
值得注意的是预留出按钮阴影的位置。
既然都做到这个程度了,那就把背景的效果做完嘛。有什么效果呢。
额,这么搓逼,不过确实是这个效果,也许整体完成后就是另一番风景吧。orz
这个效果用大腿想象就知道是点击触发的,既然如此:
[java]
view plaincopyprint?
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: sAnim = 1; // 动画标示 isOn = !isOn; // 状态标示 , 开关 invalidate(); break; } return super.onTouchEvent(event); }
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: sAnim = 1; // 动画标示 isOn = !isOn; // 状态标示 , 开关 invalidate(); break; } return super.onTouchEvent(event); }这个容易,invalidate嘛。抬起手指重绘嘛,那关键问题就抛给了onDraw同志。
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setAntiAlias(true); paint.setStyle(Style.FILL); paint.setColor(0xffcccccc); canvas.drawPath(sPath, paint); // 画出田径场 sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 // draw logic - 动态改变绘制参数,达到动画效果 paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setAntiAlias(true); paint.setStyle(Style.FILL); paint.setColor(0xffcccccc); canvas.drawPath(sPath, paint); // 画出田径场 sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 // draw logic - 动态改变绘制参数,达到动画效果 paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 }so,配菜都准备好了,上主菜~
分析:不用分析了,一张图大家都懂的~
看不出这是两个田径场的说明你只是地球人而已。对 就是一个白色的田径场压在了灰色的上面而已,然后大小随某个东西【anim标示】的变化而变化。
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // ... sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 final float scale = 0.98f * (isOn ? sAnim : 1 - sAnim); //缩放大小参数随sAnim变化而变化 canvas.save(); canvas.scale(scale, scale, sCenterX, sCenterY); paint.setColor(0xffffffff); canvas.drawPath(sPath, paint); canvas.restore(); paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 // ... }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // ... sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; // 动画标示 ,重绘10次 final float scale = 0.98f * (isOn ? sAnim : 1 - sAnim); //缩放大小参数随sAnim变化而变化 canvas.save(); canvas.scale(scale, scale, sCenterX, sCenterY); paint.setColor(0xffffffff); canvas.drawPath(sPath, paint); canvas.restore(); paint.reset(); if (sAnim > 0) invalidate(); // 继续重绘 // ... }没错,还是用的sPath只不过换了个颜色而已。 我们玩的是canvas, canvas的api scale 可以缩放画布。之前申明并且计算好的sCenterX,和sCenterY的作用就是确定画布缩放中心啦。 而0.98的作用便为我们免费的留下了一条边缘。如果设置1的话,白色的田径场将完全覆盖灰色田径场,界面上就是一片白了。
为了完美适配各种分辨率,这个0.98应该被一个变量替换。
对比完成的效果图会使用感觉有点别扭,别扭的原因就是缩放的中心位置。
应该在4个箭头的起点处。就是那里:田径场的宽度 减去 按钮的一个半径,在减去按钮距离右边的间隔什么的。
那么我就要把按钮的一些参数给算好咯~
[java]
view plaincopyprint?
private float bRadius, bStrokWidth; private float bWidth; private float bLeft, bTop, bRight, bBottom; private float sScaleCenterX; private float sScale; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // ... bLeft = bTop = 0; bRight = bBottom = sBottom; // 和田径场同高,同宽的节奏, 没错包裹圆形的肯定是个正方形是小孩子都知道的。 bWidth = bRight - bLeft; final float halfHeightOfS = (sBottom - sTop) / 2; bRadius = halfHeightOfS * 0.9f; // 按钮的半径 bStrokWidth = 2 * (halfHeightOfS - bRadius); // 按钮的边框 sScale = 1 - bStrokWidth / sHeight; //替换之前的0.98< sScaleCenterX = sWidth - halfHeightOfS; // ... } @Override protected void onDraw(Canvas canvas) { // ... canvas.scale(scale, scale, sScaleCenterX, sCenterY); }
private float bRadius, bStrokWidth; private float bWidth; private float bLeft, bTop, bRight, bBottom; private float sScaleCenterX; private float sScale; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // ... bLeft = bTop = 0; bRight = bBottom = sBottom; // 和田径场同高,同宽的节奏, 没错包裹圆形的肯定是个正方形是小孩子都知道的。 bWidth = bRight - bLeft; final float halfHeightOfS = (sBottom - sTop) / 2; bRadius = halfHeightOfS * 0.9f; // 按钮的半径 bStrokWidth = 2 * (halfHeightOfS - bRadius); // 按钮的边框 sScale = 1 - bStrokWidth / sHeight; //替换之前的0.98< sScaleCenterX = sWidth - halfHeightOfS; // ... } @Override protected void onDraw(Canvas canvas) { // ... canvas.scale(scale, scale, sScaleCenterX, sCenterY); }
大家估计看出我的命名规则啦: s开头的表示田径场,b开头的表示按钮。
那么效果理所当然的变成下图所示。
继续补上按钮。
[java]
view plaincopyprint?
canvas.save(); paint.setStyle(Style.FILL); paint.setColor(0xffffffff); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮白底 paint.setStyle(Style.STROKE); paint.setColor(0xffdddddd); paint.setStrokeWidth(bStrokWidth); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮灰边 canvas.restore();
canvas.save(); paint.setStyle(Style.FILL); paint.setColor(0xffffffff); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮白底 paint.setStyle(Style.STROKE); paint.setColor(0xffdddddd); paint.setStrokeWidth(bStrokWidth); canvas.drawCircle(bWidth / 2, bWidth / 2, bRadius, paint); // 按钮灰边 canvas.restore();上面通过canvas.drawCircle的方法画了俩圆,看起来就是我们滴按钮咯。
之后动起来~这里依旧玩canvas。
相对于scale缩放方法, 他还有translate平移和retate旋转方法,是不是很牛逼~ 。这里需要的是平移方法
[java]
view plaincopyprint?
bTranslateX = sWidth - bWidth; final float translate = bTranslateX * (isOn ? 1 - sAnim : sAnim); // 平移距离参数随sAnim变化而变化 canvas.translate(translate, 0);
bTranslateX = sWidth - bWidth; final float translate = bTranslateX * (isOn ? 1 - sAnim : sAnim); // 平移距离参数随sAnim变化而变化 canvas.translate(translate, 0);很清晰的可以看出,按钮平移的距离 是 田径场的宽度 减去按钮所占区域的宽度 , 所以即时再复杂的逻辑一点点抽丝剥茧 那么- 真相只有一个。
好了 接下来进入我们的正题内容:
对于按钮的开关不见得是我们想开就开,想关就关的。举生活中常见的例子:小明家跳闸了,然后小明去把闸往上推,然后发现推不上去自动弹回来,再推,再弹 *n。
之后小明终于受不了打电话给闪电侠,通知它过来修。 电话拨出去的时候,并不是马上被接听,同时也有可能没人接。。。编不下去了。
特么生活的现实就是比理想残酷,小明我不是故意黑你的。
为了能够让大家明确的感受到按钮与之前的不同,所以换了一个明显的颜色~
那么转换为程序语言这个按钮开关应该是4种状态:
已经关闭。 已经打开。准备关闭。准备打开。
所以我们为switchView 添加4种状态, 之前的 变量isOn可以退休了。
[java]
view plaincopyprint?
private final int STATE_SWITCH_ON = 4; // 已经打开 private final int STATE_SWITCH_ON2 = 3; // 准备关闭 private final int STATE_SWITCH_OFF2 = 2; // 准备打开 private final int STATE_SWITCH_OFF = 1; // 已经关闭 private int state = STATE_SWITCH_OFF; private int lastState = state;
private final int STATE_SWITCH_ON = 4; // 已经打开 private final int STATE_SWITCH_ON2 = 3; // 准备关闭 private final int STATE_SWITCH_OFF2 = 2; // 准备打开 private final int STATE_SWITCH_OFF = 1; // 已经关闭 private int state = STATE_SWITCH_OFF; private int lastState = state;
细心的朋友会发现,田径场还是那个田径场,而按钮不再是那个按钮。它可以变瘪 orz
so canvas.drawCircle就不行啦, 好了我们再来看一张图。
看不出这是两个田径场的说明你仍然只是地球人而已。对 就是一个紫色的田径场压在了灰色的上面而已,然后形状随某个东西【anim标示】的变化而在圆形和田径场之间变化而已。
[java]
view plaincopyprint?
private final Path bPath = new Path();
private final RectF bRectF = new RectF();
private float bOffset;
private float bOnLeftX, bOn2LeftX, bOff2LeftX, bOffLeftX;
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// ...
final float halfHeightOfS = (sBottom - sTop) / 2;
bRadius = halfHeightOfS * 0.9f;
bOffset = bRadius * 0.3f; // 有多瘪~ 半径的三分之一左右
bStrokWidth = 2 * (halfHeightOfS - bRadius);
bOnLeftX = sWidth - bWidth; // 在已经开启状态下,按钮距离自身左端的距离
bOn2LeftX = bOnLeftX - bOffset;// 在准备关闭状态下,按钮距离自身左端的距离
bOffLeftX = 0;// 在已经关闭状态下,按钮距离自身左端的距离
bOff2LeftX = 0;// 在准备开启状态下,按钮距离自身左端的距离
bRectF.left = bLeft; // 替代 circle性质的按钮,改为path性质的按钮,以提供“变瘪”的功能 。囧
bRectF.right = bRight;
bRectF.top = bTop + bStrokWidth / 2;
bRectF.bottom = bBottom - bStrokWidth / 2;
// ...
}
private final Path bPath = new Path(); private final RectF bRectF = new RectF(); private float bOffset; private float bOnLeftX, bOn2LeftX, bOff2LeftX, bOffLeftX; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // ... final float halfHeightOfS = (sBottom - sTop) / 2; bRadius = halfHeightOfS * 0.9f; bOffset = bRadius * 0.3f; // 有多瘪~ 半径的三分之一左右 bStrokWidth = 2 * (halfHeightOfS - bRadius); bOnLeftX = sWidth - bWidth; // 在已经开启状态下,按钮距离自身左端的距离 bOn2LeftX = bOnLeftX - bOffset;// 在准备关闭状态下,按钮距离自身左端的距离 bOffLeftX = 0;// 在已经关闭状态下,按钮距离自身左端的距离 bOff2LeftX = 0;// 在准备开启状态下,按钮距离自身左端的距离 bRectF.left = bLeft; // 替代 circle性质的按钮,改为path性质的按钮,以提供“变瘪”的功能 。囧 bRectF.right = bRight; bRectF.top = bTop + bStrokWidth / 2; bRectF.bottom = bBottom - bStrokWidth / 2; // ... }
那么同时再次观察刚才的gif图片。会发现在按钮变瘪的时候(已经开启到准备关闭,已经关闭到准备开启),田径场是无动作的。
所以anim标示,存在2个~ 之前这个参数叫sAnim而不是 anim,估计大家也猜出来啦。这里在添加一个 bAnim。
[java]
view plaincopyprint?
private float sAnim, bAnim;
private float sAnim, bAnim;
重新设定动画的触发时机!!!
[java]
view plaincopyprint?
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: lastState = state; if (state == STATE_SWITCH_OFF) { bAnim = 1; state = STATE_SWITCH_OFF2; } else if (state == STATE_SWITCH_OFF2) { bAnim = 1; sAnim = 1; // 只有在准备打开,并且结果成功的时候 state = STATE_SWITCH_ON; } else if (state == STATE_SWITCH_ON) { bAnim = 1; state = STATE_SWITCH_ON2; } else if (state == STATE_SWITCH_ON2) { bAnim = 1; sAnim = 1; // 和在准备关闭,并且结果成功的时候才会 触发背景的变化。 state = STATE_SWITCH_OFF; } invalidate(); break; } return super.onTouchEvent(event); }
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: lastState = state; if (state == STATE_SWITCH_OFF) { bAnim = 1; state = STATE_SWITCH_OFF2; } else if (state == STATE_SWITCH_OFF2) { bAnim = 1; sAnim = 1; // 只有在准备打开,并且结果成功的时候 state = STATE_SWITCH_ON; } else if (state == STATE_SWITCH_ON) { bAnim = 1; state = STATE_SWITCH_ON2; } else if (state == STATE_SWITCH_ON2) { bAnim = 1; sAnim = 1; // 和在准备关闭,并且结果成功的时候才会 触发背景的变化。 state = STATE_SWITCH_OFF; } invalidate(); break; } return super.onTouchEvent(event); }
这个事件处理的逻辑依旧容易,invalidate嘛。抬起手指重绘嘛,那关键问题就再一次的抛给了onDraw同志。
onDraw同志需要根据我们给的anim标示来处理按钮的动画效果咯
[java]
view plaincopyprint?
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setAntiAlias(true);
paint.setStyle(Style.FILL);
paint.setColor(0xffcccccc);
canvas.drawPath(sPath, paint); // 最底下的田径场
sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0;
bAnim = bAnim - 0.1f > 0 ? bAnim - 0.1f : 0;
final boolean isOn = (state == STATE_SWITCH_ON || state == STATE_SWITCH_ON2);
final float scale = sScale * (isOn ? sAnim : 1 - sAnim);
final float scaleOffset = (bOnLeftX + bRadius - sCenterX) * (isOn ? 1 - sAnim : sAnim);
canvas.save();
canvas.scale(scale, scale, sCenterX + scaleOffset, sCenterY); // 田径场动画的缩放中心
paint.setColor(0xffffffff);
canvas.drawPath(sPath, paint); // 这个之前讲过了
canvas.restore();
canvas.save();
final boolean isState2 = (state == STATE_SWITCH_ON2 || state == STATE_SWITCH_OFF2);
final float percent = (isState2 ? 1 - bAnim : bAnim);
calcBPath(percent); // 根据anim标示计算变瘪的按钮路径
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(bStrokWidth);
paint.setColor(0xffff00cc);
canvas.translate(calcBTranslate(bAnim), 0); // 根据anim标示计算按钮开关平移的坐标
canvas.drawPath(bPath, paint);
canvas.restore();
paint.reset();
if (sAnim > 0 || bAnim > 0) invalidate(); // 重绘的标示由1个变为了2个。
}
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setAntiAlias(true); paint.setStyle(Style.FILL); paint.setColor(0xffcccccc); canvas.drawPath(sPath, paint); // 最底下的田径场 sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; bAnim = bAnim - 0.1f > 0 ? bAnim - 0.1f : 0; final boolean isOn = (state == STATE_SWITCH_ON || state == STATE_SWITCH_ON2); final float scale = sScale * (isOn ? sAnim : 1 - sAnim); final float scaleOffset = (bOnLeftX + bRadius - sCenterX) * (isOn ? 1 - sAnim : sAnim); canvas.save(); canvas.scale(scale, scale, sCenterX + scaleOffset, sCenterY); // 田径场动画的缩放中心 paint.setColor(0xffffffff); canvas.drawPath(sPath, paint); // 这个之前讲过了 canvas.restore(); canvas.save(); final boolean isState2 = (state == STATE_SWITCH_ON2 || state == STATE_SWITCH_OFF2); final float percent = (isState2 ? 1 - bAnim : bAnim); calcBPath(percent); // 根据anim标示计算变瘪的按钮路径 paint.setStyle(Style.STROKE); paint.setStrokeWidth(bStrokWidth); paint.setColor(0xffff00cc); canvas.translate(calcBTranslate(bAnim), 0); // 根据anim标示计算按钮开关平移的坐标 canvas.drawPath(bPath, paint); canvas.restore(); paint.reset(); if (sAnim > 0 || bAnim > 0) invalidate(); // 重绘的标示由1个变为了2个。 }
so,白眼狼都看出来啊。啊不,口误,Oh shi-t 笔误,明眼人都看的出来。核心逻辑丢在 calcBPath() 和 calcBTranslate()里面咯。 如果问我为什么要创建2个方法丢在里面,我只能告诉你,这是kami的启示~
[java]
view plaincopyprint?
private void calcBPath(float percent) {
bPath.reset();
bRectF.left = bLeft + bStrokWidth / 2;
bRectF.right = bRight - bStrokWidth / 2;
bPath.arcTo(bRectF, 90, 180);
bRectF.left = bLeft + percent * bOffset + bStrokWidth / 2;
bRectF.right = bRight + percent * bOffset - bStrokWidth / 2;
bPath.arcTo(bRectF, 270, 180);
bPath.close();
}
private void calcBPath(float percent) { bPath.reset(); bRectF.left = bLeft + bStrokWidth / 2; bRectF.right = bRight - bStrokWidth / 2; bPath.arcTo(bRectF, 90, 180); bRectF.left = bLeft + percent * bOffset + bStrokWidth / 2; bRectF.right = bRight + percent * bOffset - bStrokWidth / 2; bPath.arcTo(bRectF, 270, 180); bPath.close(); }
看方法内容,原来percent 通过直接影响rect来间接影响了path啊。 left和right值似乎随percent的增大而增大。这样 两个圆弧慢慢被拉开,反之缩小。
这特么谁写的,真是太精辟了。 --- 匿名
[java]
view plaincopyprint?
private float calcBTranslate(float percent) {
float result = 0;
int wich = state - lastState;
switch (wich) {
case 1: // off - off2
result = bOff2LeftX - (bOff2LeftX - bOffLeftX) * percent;
break;
case 2: // off2 - on
result = bOnLeftX - (bOnLeftX - bOff2LeftX) * percent;
break;
case -1:// on - on2
result = bOn2LeftX + (bOnLeftX - bOn2LeftX) * percent;
break;
case -2:// on2 - off
result = bOffLeftX + (bOn2LeftX - bOffLeftX) * percent;
break;
}
return result - bOffLeftX;
}
private float calcBTranslate(float percent) { float result = 0; int wich = state - lastState; switch (wich) { case 1: // off - off2 result = bOff2LeftX - (bOff2LeftX - bOffLeftX) * percent; break; case 2: // off2 - on result = bOnLeftX - (bOnLeftX - bOff2LeftX) * percent; break; case -1:// on - on2 result = bOn2LeftX + (bOnLeftX - bOn2LeftX) * percent; break; case -2:// on2 - off result = bOffLeftX + (bOn2LeftX - bOffLeftX) * percent; break; } return result - bOffLeftX; }
看方法内容,原来平移的结果result 是根据不同的情况进行了分类啊,真是层次分明。
这特么谁写的,66666。 ---匿名
好了,打死我也不会告诉你们,评论是我自己留下的。
做到这里几乎完成了大半,不过仍有不合理的地方。 首先从刚才的TouchEvent的代码逻辑可以看出,点击一下动一下,点击四下一个周天~
对的。 到了准备开启后,或者准备关闭后的结果不用该由控件自身决定,应该交给业务逻辑。 那么其结果是成功还是失败就不管我们的事了,只需要提供一个
执行结果动画的公开方法就OK了。 同时,calcBTranslate()的内部逻辑便需要扩充,添加对失败结果【开启不成功,关闭不成功】的处理等。
那么 定义并且创建接口~
[java]
view plaincopyprint?
public interface OnSwitchStateChangedListener {
void onStateChanged(int state);
}
private OnSwitchStateChangedListener listener = new OnSwitchStateChangedListener() {
@Override public void onStateChanged(int state) {
if (state == STATE_SWITCH_OFF2) {
toggleSwitch(STATE_SWITCH_ON);
}
else if (state == STATE_SWITCH_ON2) {
toggleSwitch(STATE_SWITCH_OFF);
}
}
};
public void setOnSwitchStateChangedListener(OnSwitchStateChangedListener listener) {
if (listener == null) throw new IllegalArgumentException("empty listener");
this.listener = listener;
}
public interface OnSwitchStateChangedListener { void onStateChanged(int state); } private OnSwitchStateChangedListener listener = new OnSwitchStateChangedListener() { @Override public void onStateChanged(int state) { if (state == STATE_SWITCH_OFF2) { toggleSwitch(STATE_SWITCH_ON); } else if (state == STATE_SWITCH_ON2) { toggleSwitch(STATE_SWITCH_OFF); } } }; public void setOnSwitchStateChangedListener(OnSwitchStateChangedListener listener) { if (listener == null) throw new IllegalArgumentException("empty listener"); this.listener = listener; }
仔细的朋友又会发现。我这里已经有个接口的默认实现啦。 实现的内容:当状态变换为准备关闭的时候 ,准备打开的时候做了一件不可告人的事情。
[java]
view plaincopyprint?
private void refreshState(int newState) {
lastState = state;
state = newState;
postInvalidate(); // 为什么postInvalidate() 而不用 invalidate()。 你猜~
}
private synchronized void toggleSwitch(int wich) {
if (wich == STATE_SWITCH_ON || wich == STATE_SWITCH_OFF) {
if ((wich == STATE_SWITCH_ON && (lastState == STATE_SWITCH_OFF || lastState == STATE_SWITCH_OFF2))
|| (wich == STATE_SWITCH_OFF && (lastState == STATE_SWITCH_ON || lastState == STATE_SWITCH_ON2))) {
sAnim = 1;
}
bAnim = 1;
refreshState(wich);
}
else {
Log.e("SwitchView_step2", "do not support state : " + wich);
}
}
private void refreshState(int newState) { lastState = state; state = newState; postInvalidate(); // 为什么postInvalidate() 而不用 invalidate()。 你猜~ } private synchronized void toggleSwitch(int wich) { if (wich == STATE_SWITCH_ON || wich == STATE_SWITCH_OFF) { if ((wich == STATE_SWITCH_ON && (lastState == STATE_SWITCH_OFF || lastState == STATE_SWITCH_OFF2)) || (wich == STATE_SWITCH_OFF && (lastState == STATE_SWITCH_ON || lastState == STATE_SWITCH_ON2))) { sAnim = 1; } bAnim = 1; refreshState(wich); } else { Log.e("SwitchView_step2", "do not support state : " + wich); } }
toggleSwitch()。 根据当前状态和上次状态判断 在成功打开,和成功关闭的时候 开启sAnim田径场背景动画标示~ ,同时必然开始按钮的动画标示~之后重绘。
所以当这个控件没有被重新设置SwitchStateChangedListener的时候一切就是期望的那么美好。 想开就开,想关就关~
接着提供对外方法
[java]
view plaincopyprint?
public int getState() { // 获得状态 return state; } public void setState(boolean isOn) { // 设置状态 只能设置 [已经关闭] 和 [已经开启] final int wich = isOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; refreshState(wich); } public void toggleSwitch(boolean letItOn) { // 切换状态 只支持 [已经关闭] 和 [已经开启] 的切换 final int wich = letItOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; postDelayed(new Runnable() { @Override public void run() { toggleSwitch(wich); } }, 300); }
public int getState() { // 获得状态 return state; } public void setState(boolean isOn) { // 设置状态 只能设置 [已经关闭] 和 [已经开启] final int wich = isOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; refreshState(wich); } public void toggleSwitch(boolean letItOn) { // 切换状态 只支持 [已经关闭] 和 [已经开启] 的切换 final int wich = letItOn ? STATE_SWITCH_ON : STATE_SWITCH_OFF; postDelayed(new Runnable() { @Override public void run() { toggleSwitch(wich); } }, 300); }
那么300行左右的代码 完成了我们的仿iOS SwitchButton 的控件 SwitchView (就不和它一个名字,不服 solo)
至于阴影效果,如何使用的相关内容:
查看SwitchView 源码
转载于:http://blog.csdn.net/bfbx5173/article/details/45191147#comments
相关文章推荐
- Android Sdk 国内镜像下载地址
- android:textSize="20dp" 中20dp是什么意思?
- android:ToolBar详解(手把手教程)
- android系统相机的使用、及解决拍照闪退的问题
- 【第三篇】学习 android 事件总线androidEventbus之list数据事件的传递,发送list数据事件到另外一个Activity
- Android 一种截图方法,稍微修改.保存
- Android Studio 工程.GitIgnore应该忽略的文件
- Android 当打开“开发者模式”中的“不保留活动”后,程序应当怎么保持正常运行
- Android AOSP输入法(LatinIME)大写判断分析
- android 动画总结笔记 一
- 获取Android界面信息
- Android设置拍照或者上传本地图片
- android获取mp4视频文件总时长和视频宽高<转>
- Android Parcelable vs Serializable
- AndroidStudio使用本地aar包
- EventBus使用详解
- Android Maven pom.xml
- AndroidStudio生成aar包
- android软件更新
- android4.4从系统图库无法加载图片的问题