UI图的效果如下
实现的gif效果图如下,后续做些细节上的优化即可
实现思路,通过自定义继承View,通过Canvas,Paint等api绘制出来
分析思路:
以UI 3倍图作为标准,宽为156px,高为90px,则宽高比为 156 / 90
高度在dimens已经定义好,3倍图的UI,则对应于30dp
则宽度width = height * 156 / 90;
这里的circleThumb 的移动范围,我们发现是这个circleThumb的圆形的X坐标一直在变话,其变化范围为RangeLeft = 这个circleThumb的半径,小于这个,圆圈就出了View区域左边的边界了,同理,RangeRight = View区域宽度 - 半径,超过RangeRight则出了边界;
ok,主要的分析完毕后,就开始撸代码吧,忽略自定义View的流程,及一些熟悉的代码,大家都狠聪明的
1 在onSizeChanged里面做些尺寸的初始化工作
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.mHeight = h;
this.mWidth = (int) (h * WIDTH_HEIGHT_RATIO);
this.roundRadius = mHeight / 2;
this.circleRadius = (mHeight - circleMargin) / 2;
circleThumbStartX = circleRadius + circleMargin;
//circle center move range [roundRadius,mWidth - roundRadius]
rangeLeft = circleRadius + circleMargin;
rangeRight = mWidth - circleRadius - circleMargin;
roundBgF = new RectF(0, 0, mWidth, mHeight);
}
2 让自定义View适配WrapContent 和 matchParent模式(相对于父容器Parent)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DE_H, getResources().getDisplayMetrics()), MeasureSpec.EXACTLY);
}
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DE_W, getResources().getDisplayMetrics()), MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
3 首先绘制出底部的bg
private void drawBg(Canvas canvas) {
Paint paint = createPaint();
paint.setStrokeWidth(roundBgStrokeWidth);
if(mState == MoveState.STATE_CLOSE){
paint.setColor(roundBgClosedColor);
}else {
paint.setColor(roundBgOpenedColor);
}
canvas.drawRoundRect(roundBgF, roundRadius, roundRadius, paint);
}
4 绘制CircleThumb
private void drawLeftThumb(Canvas canvas) {
Paint paint = createPaint();
paint.setStyle(Paint.Style.FILL);
if(mState == MoveState.STATE_CLOSE){
paint.setColor(circleClosedColor);
}else {
paint.setColor(circleOpenedColor);
}
canvas.drawCircle(circleThumbStartX, mHeight / 2, circleRadius, paint);
}
5 onTouch里边实现圆圈的移动 我们已经分析了这个边界的处理,代码如下
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) event.getX();
if (moveX - downX > canMoveX || downX - moveX > canMoveX) {
if (moveX <= rangeLeft && mState == MoveState.STATE_OPEN) {
circleThumbStartX = rangeLeft;
mState = MoveState.STATE_CLOSE;
} else if (moveX >= rangeRight && mState == MoveState.STATE_CLOSE) {
circleThumbStartX = rangeRight;
mState = MoveState.STATE_OPEN;
} else if (rangeLeft < moveX && moveX < rangeRight) {
circleThumbStartX = moveX;
}
invalidate();
updageState(mState);
}
break;
case MotionEvent.ACTION_UP:
int upX = (int) (event.getX() - downX);
if (0 <= upX && upX <= canMoveX) {
if (mState == MoveState.STATE_CLOSE) {
startRightAnimation();
} else if (mState == MoveState.STATE_OPEN) {
startLeftAnimation();
}
} else {
if (circleThumbStartX > mWidth / 2) {
circleThumbStartX = rangeRight;
mState = MoveState.STATE_OPEN;
} else {
circleThumbStartX = rangeLeft;
mState = MoveState.STATE_CLOSE;
}
invalidate();
updageState(mState);
}
break;
}
return true;
}
6 让其有线性动画的移动效果,则使用属性动画来实现,使用线性的插值器
动画代码如下,模版代码
setClickable(false);
ValueAnimator rightAnimation = ValueAnimator.ofFloat(0, 1);
rightAnimation.setTarget(this);
rightAnimation.setDuration(200);
rightAnimation.setRepeatCount(0);
rightAnimation.setInterpolator(new LinearInterpolator());
rightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
Float aFloat = (Float) valueAnimator.getAnimatedValue();
circleThumbStartX = (int) ((int) (rangeRight * aFloat) + rangeLeft * (1 - aFloat));
invalidate();
}
});
rightAnimation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
mState = MoveState.STATE_OPEN;
updageState(mState);
setClickable(true);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
rightAnimation.start();
7 最后要暴漏给外部使用 及 View 的一些状态处理
private static class MoveState {
private static final int STATE_CLOSE = 0;
private static final int STATE_OPEN = 1;
}
private void updageState(int state) {
this.mState = state;
if (state == MoveState.STATE_CLOSE) {
isOpen = false;
} else if (state == MoveState.STATE_OPEN) {
isOpen = true;
}
invalidateMainUI(isOpen);
}
private void invalidateMainUI(boolean state) {
if (null != mSlideOpenCloseListener) {
mSlideOpenCloseListener.switchOk(state);
}
}
public interface slideOpenCloseListener {
public void switchOk(boolean isOpen);
}
总结如下:
1 首先要理解安卓的坐标系 这个区域是一个Rect,Rect中是一个圆圈,让圆圈在这个Rect中来回移动即可,我们只需要处理下边界
2 Canvas paint的api,这个多练即可,无需多说
3 属性动画ValueAnimation的熟练使用及掌握,主要是插植器的理解,厉害的自己写出插值器,不过系统的基本能满足现在的需求
代码就不传了,其实就一个主类,如果需要,我会在csdn建立代码块