Custom Views mimic iOS UiSwitch controls
This article is original, please indicate the source of reprint. Welcome to my Jane book.
Preface:
Android Switch control I believe we have used, in fact, I think the effect is good, but the company requires the unity of the UI, so let me copy iOS effect, I wonder, why has been to copy iOS, iOS copy Android yao? All your whining’s over, let’s get to work.
Attached are the renderings
Train of thought
Drawing control
The whole control is divided into two parts when drawing:
- The baseboard, the part of the oval that looks like a racetrack.
- Button.
There is nothing special about this part, if there is a bit of custom View base friends should be able to easily fix.
Dynamic effect processing
The motion effect I used here is also the basic flat motion effect, with the base color gradient effect
/** * switch off */
public void toggleOn(a) {
// Handle slot color gradient and handle sliding are achieved through properties animation
ObjectAnimator animator = ObjectAnimator.ofFloat(this."spotStartX".0, mOffSpotX);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
floatfraction = animation.getAnimatedFraction(); calculateColor(fraction, mOffSlotColor, mOpenSlotColor); invalidate(); }}); }/** * switch */
public void toggleOff(a) {
// Handle slot color gradient and handle sliding are achieved through properties animation
ObjectAnimator animator = ObjectAnimator.ofFloat(this."spotStartX", mOffSpotX, 0);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
floatfraction = animation.getAnimatedFraction(); calculateColor(fraction, mOpenSlotColor, mOffSlotColor); invalidate(); }}); }/** * Calculates the color of the handle slot when switching **@paramFraction Animation playback progress *@paramStartColor startColor *@paramEndColor Ends the color */
public void calculateColor(float fraction, int startColor, int endColor) {
final int fb = Color.blue(startColor);
final int fr = Color.red(startColor);
final int fg = Color.green(startColor);
final int tb = Color.blue(endColor);
final int tr = Color.red(endColor);
final int tg = Color.green(endColor);
//RGB three channel linear gradient
int sr = (int) (fr + fraction * (tr - fr));
int sg = (int) (fg + fraction * (tg - fg));
int sb = (int) (fb + fraction * (tb - fb));
// Scope limited
sb = clamp(sb, 0.255);
sr = clamp(sr, 0.255);
sg = clamp(sg, 0.255);
mSlotColor = Color.rgb(sr, sg, sb);
}Copy the code
Touch event and Onclick event
Here I made a gesture tool class, specialized in handling Touch gestures, no difficulty, just too lazy to write a set of repeated code inside each custom control, so I made a tool class, convenient for the future development here also share to everyone (don’t make fun of my class name, I am the Translation of The
/** * Created by caihan on 2017/2/10. */
public class GestureUtils {
private static final String TAG = "GestureUtils";
private float startX = 0f;
private float endX = 0f;
private float startY = 0f;
private float endY = 0f;
private float xDistance = 0f;
private float yDistance = 0f;
public enum Gesture {
PullUp, PullDown, PullLeft, PullRight
}
public GestureUtils(a) {}/** * Set the initial X and Y coordinates ** when event.getAction() == motionEvent.action_down@param event
*/
public void actionDown(MotionEvent event) {
xDistance = yDistance = 0f;
setStartX(event);
setStartY(event);
}
/** * Set the X and Y coordinates of the move ** when event.getAction() == motionEvent.action_move@param event
*/
public void actionMove(MotionEvent event) {
setEndX(event);
setEndY(event);
}
/** * When event.getAction() == motionEvent.action_up * sets the X,Y coordinates of the end **@param event
*/
public void actionUp(MotionEvent event) {
setEndX(event);
setEndY(event);
}
/** * Gestures to determine the interface **@param gesture
* @return* /
public boolean getGesture(Gesture gesture) {
switch (gesture) {
case PullUp:
return isRealPullUp();
case PullDown:
return isRealPullDown();
case PullLeft:
return isRealPullLeft();
case PullRight:
return isRealPullRight();
default:
LogUtils.e(TAG, "getGesture error");
return false; }}/** * Gets the X coordinates of the Touch point relative to the screen origin **@param event
* @return* /
private float gestureRawX(MotionEvent event) {
return event.getRawX(a);
}
/** * get the Y coordinates of the Touch point relative to the screen origin **@param event
* @return* /
private float gestureRawY(MotionEvent event) {
return event.getRawY(a);
}
/** * get the X-axis offset, take absolute value **@param startX
* @param endX
* @return* /
private float gestureDistanceX(float startX, float endX) {
setxDistance(Math.abs(endX - startX));
return xDistance;
}
/** * get the Y offset, take the absolute value **@param startY
* @param endY
* @return* /
private float gestureDistanceY(float startY, float endY) {
setyDistance(Math.abs(endY - startY));
return yDistance;
}
/** ** the endY coordinate is smaller than startY@param startY
* @param endY
* @return* /
private boolean isPullUp(float startY, float endY) {
return (endY - startY) < 0;
}
/** ** the endY coordinate is larger than startY@param startY
* @param endY
* @return* /
private boolean isPullDown(float startY, float endY) {
return (endY - startY) > 0;
}
/** ** the endX coordinate is larger than startX@param startX
* @param endX
* @return* /
private boolean isPullRight(float startX, float endX) {
return (endX - startX) > 0;
}
/** ** the endX coordinate is smaller than startX, subtract the negative number to indicate the gesture left slide **@param startX
* @param endX
* @return* /
private boolean isPullLeft(float startX, float endX) {
return (endX - startX) < 0;
}
/** * Check whether the user is actually sliding up **@return* /
private boolean isRealPullUp(a) {
if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
// The offset of the Y axis is greater than that of the X axis, indicating that the user actually wants to slide up and down
return isPullUp(startY, endY);
}
return false;
}
/** * Determine whether the user's actual operation is down **@return* /
private boolean isRealPullDown(a) {
if (gestureDistanceX(startX, endX) < gestureDistanceY(startY, endY)) {
// The offset of the Y axis is greater than that of the X axis, indicating that the user actually wants to slide up and down
return isPullDown(startY, endY);
}
return false;
}
/** * Check whether the user actually operates the left slide@return* /
private boolean isRealPullLeft(a) {
if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
// The offset of the Y axis is greater than that of the X axis, indicating that the user actually wants to slide up and down
return isPullLeft(startX, endX);
}
return false;
}
/** * Check whether the user actually operates the left slide@return* /
private boolean isRealPullRight(a) {
if (gestureDistanceX(startX, endX) > gestureDistanceY(startY, endY)) {
// The offset of the Y axis is greater than that of the X axis, indicating that the user actually wants to slide up and down
return isPullRight(startX, endX);
}
return false;
}
private GestureUtils setStartX(MotionEvent event) {
this.startX = gestureRawX(event);
return this;
}
private GestureUtils setEndX(MotionEvent event) {
this.endX = gestureRawX(event);
return this;
}
private GestureUtils setStartY(MotionEvent event) {
this.startY = gestureRawY(event);
return this;
}
private GestureUtils setEndY(MotionEvent event) {
this.endY = gestureRawY(event);
return this;
}
private GestureUtils setxDistance(float xDistance) {
this.xDistance = xDistance;
return this;
}
private GestureUtils setyDistance(float yDistance) {
this.yDistance = yDistance;
return this;
}
public float getStartX(a) {
return startX;
}
public float getEndX(a) {
return endX;
}
public float getStartY(a) {
return startY;
}
public float getEndY(a) {
return endY;
}
public float getxDistance(a) {
return xDistance;
}
public float getyDistance(a) {
returnyDistance; }}Copy the code
As you know, the Onclick event is triggered after the Touch MotionEvent.ACTION_UP event. That is, if we dispatchTouchEvent the Touch event, When event.getAction() = motionEvent.action_up, return true, and Onclick will not fire, so that we can do different things for different events, which is what I’m doing here
private boolean mIsToggleOn = false;// Current switch flag
private boolean isTouchEvent = false;// Whether to consume by sliding events
private boolean isMoveing = false;// Is it still in Touch
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mGestureUtils.actionDown(event);
isTouchEvent = false;
isMoveing = false;
break;
case MotionEvent.ACTION_MOVE:
mGestureUtils.actionMove(event);
if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
// Swipe left to close
isTouchEvent = true;
touchToggle(false);
return true;
} else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
// Right swipe to open
isTouchEvent = true;
touchToggle(true);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
isMoveing = false;
if (isTouchEvent) {
// The Onclick event will no longer be triggered
return true;
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
MIsToggleOn is the current state when mIsToggleOn! = open makes the corresponding * *@paramOpen Whether to open */
private void touchToggle(boolean open) {
if(! isMoveing) { isMoveing =true;
if(mIsToggleOn ! = open) {if (mIsToggleOn) {
toggleOff();
} else{ toggleOn(); } mIsToggleOn = ! mIsToggleOn;if(mOnToggleListener ! =null) { mOnToggleListener.onSwitchChangeListener(mIsToggleOn); }}}}/** * the Onclick event triggers */
private void onClickToggle(a) {
if (mIsToggleOn) {
toggleOff();
} else{ toggleOn(); } mIsToggleOn = ! mIsToggleOn;if(mOnToggleListener ! =null) { mOnToggleListener.onSwitchChangeListener(mIsToggleOn); }}Copy the code
Then listen for the button state
public interface OnToggleListener {
void onSwitchChangeListener(boolean switchState);
}
public void setOnToggleListener(OnToggleListener listener) {
mOnToggleListener = listener;
}Copy the code
It’s over, it’s that simple… What? Want the full code? All right
/** * Created by caihan on 2017/2/10
public class IosSwitch extends View implements View.OnClickListener {
private static final String TAG = "IosSwitch";
private final int BORDER_WIDTH = 2;// Frame width
private int mBasePlaneColor = Color.parseColor("#4ebb7f");// Chassis color, layout stroke color
private int mOpenSlotColor = Color.parseColor("#4ebb7f");// The color of the handle sliding slot when it is open
private int mOffSlotColor = Color.parseColor("#EEEEEE");// The color of the handle sliding groove when closed
private int mSlotColor;
private RectF mRect = new RectF();
// Draw parameters
private float mBackPlaneRadius;// The circular radius of the base plate
private float mSpotRadius;// Handle radius
private float spotStartX;// The starting X position of the handle, panning to change it when switching
private float mSpotY;// The starting X position of the handle remains unchanged
private float mOffSpotX;// Horizontal position of handle when closed
private Paint mPaint;/ / brush
private boolean mIsToggleOn = false;// Current switch flag
private boolean isTouchEvent = false;// Whether to consume by sliding events
private boolean isMoveing = false;// Is it still in Touch
private OnToggleListener mOnToggleListener;// Toggle event listener
private GestureUtils mGestureUtils;// Gesture utility class
public interface OnToggleListener {
void onSwitchChangeListener(boolean switchState);
}
public IosSwitch(Context context) {
super(context);
init(context);
}
public IosSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setOnClickListener(this);
setEnabled(true);
mGestureUtils = new GestureUtils();
}
@Override
public void onClick(View v) {
onClickToggle();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mGestureUtils.actionDown(event);
isTouchEvent = false;
isMoveing = false;
break;
case MotionEvent.ACTION_MOVE:
mGestureUtils.actionMove(event);
if (mGestureUtils.getGesture(GestureUtils.Gesture.PullLeft)) {
// Swipe left to close
isTouchEvent = true;
touchToggle(false);
return true;
} else if (mGestureUtils.getGesture(GestureUtils.Gesture.PullRight)) {
// Right swipe to open
isTouchEvent = true;
touchToggle(true);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
isMoveing = false;
if (isTouchEvent) {
// The Onclick event will no longer be triggered
return true;
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSize;
int resultHeight = hSize;
Resources r = Resources.getSystem();
// Specify the default value when lp = wrapContent
if (wMode == MeasureSpec.AT_MOST) {
resultWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
}
if (hMode == MeasureSpec.AT_MOST) {
resultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
}
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mBackPlaneRadius = Math.min(getWidth(), getHeight()) * 0.5f;
mSpotRadius = mBackPlaneRadius - BORDER_WIDTH;
spotStartX = 0;
mSpotY = 0;
mOffSpotX = getMeasuredWidth() - mBackPlaneRadius * 2;
mSlotColor = mOffSlotColor;
}
@Override
protected void onDraw(Canvas canvas) {
/ / base plate
mRect.set(0.0, getMeasuredWidth(), getMeasuredHeight());
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
// Draw the slot for the handle
mRect.set(BORDER_WIDTH,
BORDER_WIDTH,
getMeasuredWidth() - BORDER_WIDTH,
getMeasuredHeight() - BORDER_WIDTH);
mPaint.setColor(mSlotColor);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
// The handle consists of two parts, a dark bottom plate and a white board. The purpose of this is to make the disk have a border
// Handle chassis
mRect.set(spotStartX,
mSpotY,
spotStartX + mBackPlaneRadius * 2,
mSpotY + mBackPlaneRadius * 2);
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
// Round plate for handle
mRect.set(spotStartX + BORDER_WIDTH,
mSpotY + BORDER_WIDTH,
mSpotRadius * 2 + spotStartX + BORDER_WIDTH,
mSpotRadius * 2 + mSpotY + BORDER_WIDTH);
mPaint.setColor(Color.WHITE);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
}
public float getSpotStartX(a) {
return spotStartX;
}
public void setSpotStartX(float spotStartX) {
this.spotStartX = spotStartX;
}
/** * Calculates the color of the handle slot when switching **@paramFraction Animation playback progress *@paramStartColor startColor *@paramEndColor Ends the color */
public void calculateColor(float fraction, int startColor, int endColor) {
final int fb = Color.blue(startColor);
final int fr = Color.red(startColor);
final int fg = Color.green(startColor);
final int tb = Color.blue(endColor);
final int tr = Color.red(endColor);
final int tg = Color.green(endColor);
//RGB three channel linear gradient
int sr = (int) (fr + fraction * (tr - fr));
int sg = (int) (fg + fraction * (tg - fg));
int sb = (int) (fb + fraction * (tb - fb));
// Scope limited
sb = clamp(sb, 0.255);
sr = clamp(sr, 0.255);
sg = clamp(sg, 0.255);
mSlotColor = Color.rgb(sr, sg, sb);
}
private int clamp(int value, int low, int high) {
return Math.min(Math.max(value, low), high);
}
/** * switch off */
public void toggleOn(a) {
// Handle slot color gradient and handle sliding are achieved through properties animation
ObjectAnimator animator = ObjectAnimator.ofFloat(this."spotStartX".0, mOffSpotX);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
floatfraction = animation.getAnimatedFraction(); calculateColor(fraction, mOffSlotColor, mOpenSlotColor); invalidate(); }}); }/** * switch */
public void toggleOff(a) {
// Handle slot color gradient and handle sliding are achieved through properties animation
ObjectAnimator animator = ObjectAnimator.ofFloat(this."spotStartX", mOffSpotX, 0);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
floatfraction = animation.getAnimatedFraction(); calculateColor(fraction, mOpenSlotColor, mOffSlotColor); invalidate(); }}); }public boolean getSwitchState(a) {
return mIsToggleOn;
}
MIsToggleOn is the current state when mIsToggleOn! = open makes the corresponding * *@paramOpen Whether to open */
private void touchToggle(boolean open) {
if(! isMoveing) { isMoveing =true;
if(mIsToggleOn ! = open) {if (mIsToggleOn) {
toggleOff();
} else{ toggleOn(); } mIsToggleOn = ! mIsToggleOn;if(mOnToggleListener ! =null) { mOnToggleListener.onSwitchChangeListener(mIsToggleOn); }}}}/** * the Onclick event triggers */
private void onClickToggle(a) {
if (mIsToggleOn) {
toggleOff();
} else{ toggleOn(); } mIsToggleOn = ! mIsToggleOn;if(mOnToggleListener ! =null) { mOnToggleListener.onSwitchChangeListener(mIsToggleOn); }}public void setOnToggleListener(OnToggleListener listener) {
mOnToggleListener = listener;
}
/** * Set the switch initial state * on the interface@param open
*/
public void setChecked(final boolean open) {
this.postDelayed(new Runnable() {
@Override
public void run(a) { touchToggle(open); }},300); }}Copy the code
Thank you
SwitchButton Custom control (3 steps to complete the switch) Swift- Custom switch control
Done. Call it a day
Welcome to comment on my shortcomings.