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





The effect is shown here. It looks ok

Train of thought

Drawing control

The whole control is divided into two parts when drawing:

  1. The baseboard, the part of the oval that looks like a racetrack.
  2. 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.