Original article, reproduced please indicate the source, juejin.cn/post/684490…

0 x0 preface

A design network often have a lot of good beautiful interaction design work, one day, encounter such effect, smooth animation, interactive nature, so in its anatomy of these elements, a open source control, ten days have one hundred star, feel very welcome, wrote this scribble a few pen today, share the crime after, hope to help peers.

0 x1 preparation

rendering

Effect analysis

The curve part

1, default state: straight line, beginning and end annotation, default value annotation.

2. Finger press down: the curve animation is executed, and the small circle is enlarged regularly. The marking value moves upward at the same speed according to the relative position of the curve crest.

3. Dragging effect of finger: curve, small circle and annotation move with touch point and update annotation value at the same time.

4, finger off the screen: curve back animation execution, marking the small circle rule shrinks to the default state, the selected value follows the wave peak to sink to the default state, marking the background gradually disappear, the end of animation.

Circular indicator section

There are three parts to disassemble the state: default state, in-process state and post-touch state. By default, the indicator is a small circle with an agreed distance below the horizontal curve. When pressed, the coordinates at the bottom of the circle remain unchanged, the diameter of the circle gradually increases, and the distance between the dome and the curve remains unchanged until the end of the animation.

Functional analysis

1, elements in the control: ruler, annotation with small circle, selected value, can be configured with their own color,

2. Range of configurable values,

3. The default value can be configured.

4. The selected value can be monitored in real time.

5, can display the unit.

Technical analysis

The curve part

Static screenshots show that the main elements of this control for touch triggered curve and its expansion effect. After disassembling, the touch part is a six-order Bezier curve, with five reference points and four control points. We can divide it into two third-order curves. Among them, the Y coordinate of the first and last reference points is fixed, the X coordinate moves relative to the touch position, the X coordinate of the remaining reference points is relatively fixed, and the Y coordinate rises and falls according to the animation law. As for the control point, in order to ensure that the curve part is horizontal by default, the Y coordinate of the first and the last two control points is fixed, and the X coordinate is relatively fixed. The Y coordinate of the two control points in the middle is consistent with the reference point in the middle, and X is fixed relative to the reference point in the middle. (Through the above disassembly, the curve can be a horizontal line by default, and in the down state, with the horizontal position, the crest position, can have a more natural excessive arc, not so stiff.)

Circular indicator section

Normal round, relative to the bottom of its own larger up, reduced down reduction.

The animation part

Press down the animation using ordinary ValueAnimator, uniform LinearInterpolator. There is also a background change of the selected value. Change the brush Alpha value according to the animation progress. // Write some animation code here.

0x2 code implementation

Make a cup of tea and roll up your sleeves!

Inheriting the View:

public class BezierSeekBar extends View {
    public BezierSeekBar(Context context) {
        super(context);
        init(context, null);

    }

    public BezierSeekBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public BezierSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public BezierSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        this.context = context; }}Copy the code

First draw the curve effect:

        // The first third-order curve
        bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
        bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3.this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);

        // Second third order curve
        bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
        bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3.this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);

Copy the code

Change its Y coordinate to restore the curve to its default state: draw a full line:

        //line1
        bezierPath.reset();
        bezierPath.moveTo(0, (float) 2 * height / 3);
        bezierPath.lineTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);

        //bezier1
        bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
        bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3.this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);

        //bezier2
        bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
        bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3.this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);

        //line2
        bezierPath.lineTo(width, (float) 2 * height / 3);
        canvas.drawPath(bezierPath, bezierPaint);

Copy the code

Add Touch event blocking, display curve when pressed:

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                fingerX = event.getX();
                if (fingerX < 0F) fingerX = 0F;
                if (fingerX > width) fingerX = width;
                // Touch the media into the control to begin the animation transition
                this.animatorFingerIn.start();
                break;

            case MotionEvent.ACTION_MOVE:
                fingerX = event.getX();
                if (fingerX < 0F) fingerX = 0F;
                if (fingerX > width) fingerX = width;
                postInvalidate();
                break;

            case MotionEvent.ACTION_UP:
                // Touch the media away from the control to perform the animation
                this.animatorFingerOut.start();
                break;
        }
        valueSelected = Integer.valueOf(decimalFormat.format(valueMin + (valueMax - valueMin) * fingerX / width));

        if(selectedListener ! =null) {
            selectedListener.onSelected(valueSelected);
        }
        return true;
    }
Copy the code

Add animation effects:

        this.animatorFingerIn = ValueAnimator.ofFloat(0f, 1f);
        this.animatorFingerIn.setDuration(200L);
        this.animatorFingerIn.setInterpolator(new LinearInterpolator());

        this.animatorFingerOut = ValueAnimator.ofFloat(1f, 0f);
        this.animatorFingerOut.setDuration(200L);
        this.animatorFingerOut.setInterpolator(new LinearInterpolator());

        this.animatorFingerOut.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float progress = (float) animation.getAnimatedValue(); AnimInFinshed = (progress >= 0.15f); TxtSelectedBgPaint. SetAlpha ((int) (255 * (progress to 0.15 F)));if (progress >= 0.95F) {
                    textPaint.setColor(colorValueSelected);
                } else {
                    textPaint.setColor(colorValue);
                }

                bezierHeight = circleRadiusMax * 1.5F * progress;
                circleRadius = circleRadiusMin + (circleRadiusMax - circleRadiusMin) * progress;
                spaceToLine = circleRadiusMin* 2 * (1F - progress); postInvalidate(); }});Copy the code

Draw a circular indicator, changing its size according to the animation progress:

 canvas.drawCircle(this.fingerX, (float) 2 * height / 3 + spaceToLine + circleRadius, circleRadius, ballPaint);

Copy the code

After adding the other helper elements, configure the common attributes and throw the public methods:

 <declare-styleable name="BezierSeekBar">// Curve color<attr name="bsBar_color_line" format="reference|color" />// Circular indicator color<attr name="bsBar_color_ball" format="reference|color" />// The text color of the threshold<attr name="bsBar_color_value" format="reference|color" />// Select the text color of the value<attr name="bsBar_color_value_selected" format="reference|color" />// Select the text color background of the value<attr name="bsBar_color_bg_selected" format="reference|color" />// Minimum threshold<attr name="bsBar_value_min" format="integer" />// The threshold is maximum<attr name="bsBar_value_max" format="integer" />// The default value is selected<attr name="bsBar_value_selected" format="integer" />/ / unit<attr name="bsBar_unit" format="reference|string" />
 </declare-styleable>
Copy the code
private void initAttr(Context context, AttributeSet attrs) {
        if(attrs ! =null) {
            TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.BezierSeekBar);

            this.colorBall = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_ball, Color.BLACK);
            this.colorLine = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_line, Color.BLACK);
            this.colorValue = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value, Color.BLACK);
            this.colorValueSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value_selected, Color.WHITE);
            this.colorBgSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_bg_selected, Color.BLACK);
            this.valueMin = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_min, 30);
            this.valueMax = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_max, 150);
            this.valueSelected = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_selected, 65);
            this.unit = attributes.getString(R.styleable.BezierSeekBar_bsBar_unit) + ""; attributes.recycle(); }}Copy the code

Finally, test this out:

 <tech.nicesky.bezierseekbar.BezierSeekBar
        android:id="@+id/bsBar_test"
        app:bsBar_color_ball="@android:color/white"
        app:bsBar_color_bg_selected="@android:color/white"
        app:bsBar_color_line="@android:color/white"
        app:bsBar_color_value="@android:color/white"
        app:bsBar_color_value_selected="#ef5350"
        app:bsBar_value_min="30"
        app:bsBar_value_max="120"
        app:bsBar_value_selected="65"
        app:bsBar_unit="kg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
Copy the code

Perfect:)! Attached is the Demo APK:

Summary:

This control mainly involves the basic understanding and application of Bessel curve, the basic application of animation, the routine process of custom control, the focus is still skilled in the analysis and disassembly of various UI effects and ideas.

Welcome to Star, open source address:

https://github.com/fairytale110/BezierSeekBar
Copy the code