Android - imitating Youku's progress bar with time points

A new requirement was encountered in a recent project: the progress bar of the player displays the title and end, and the keyframes and key clips are dotted, as shown in the figure below: the

start time of the keyframe is marked with a white point, and the end time is marked with a yellow point.

At the beginning, I found a few good projects on github, such as: BubbleSeekBar , IndicatorSeekBar , but these two projects could not be used directly. Later, my colleagues asked me to draw some content by myself. Well, I was a little lazy at first and felt that the drawing was too much. It's complicated, and it's good to have ready-made ones. Later, I couldn't find a suitable open source library, so I just bit the bullet and wrote it myself. . . Let's talk about the implementation process below.

Generally speaking, they rewrite the content of a set of SeekBar for custom Dotting SeekBar, and I want to be lazy, so I directly inherited the SeekBar and drew Dot Effect on the SeekBar, but you must know that the canvas is layered, In this case, I am drawing the content on the top layer of the SeekBar, which will cause the dots to be displayed on the drag slider when the dots and drag and slide overlap, which is obviously not the effect we want. , so what should I do? I didn't think of a good method. I refer to the implementation of the android:tickMark attribute of SeekBar in API25. It is also very simple. It is to redraw the content in the onDraw method of SeekBar, but you will find that the slider is displayed. On TickMark, this may be due to the rewrite of the drawableStateChanged method, but I did not find the processing logic when the slider and tickMark overlap, and I will verify it later. After talking so much, you must feel dizzy, then look at the code, it is clear at a glance.

AppCompatSeekBar.java

public class AppCompatSeekBar extends SeekBar {

    private AppCompatSeekBarHelper mAppCompatSeekBarHelper;

    public AppCompatSeekBar(Context context) {
        this(context, null);
    }

    public AppCompatSeekBar(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.seekBarStyle);
    }

    public AppCompatSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mAppCompatSeekBarHelper = new AppCompatSeekBarHelper(this);
        mAppCompatSeekBarHelper.loadFromAttributes(attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mAppCompatSeekBarHelper.drawTickMarks(canvas);
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        mAppCompatSeekBarHelper.drawableStateChanged();
    }

    @RequiresApi(11)
    @TargetApi(11)
    @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();
        mAppCompatSeekBarHelper.jumpDrawablesToCurrentState();
    }
}

The drawTickMarks() method is implemented:

    /**
     * Draw the tick marks.
     */
    void drawTickMarks(Canvas canvas) {
        if (mTickMark != null) {
            final int count = mView.getMax();
            if (count > 1) {
                final int w = mTickMark.getIntrinsicWidth();
                final int h = mTickMark.getIntrinsicHeight();
                final int halfW = w >= 0 ? w / 2 : 1;
                final int halfH = h >= 0 ? h / 2 : 1;
                mTickMark.setBounds(-halfW, -halfH, halfW, halfH);

                final float spacing = (mView.getWidth() - mView.getPaddingLeft()
                        - mView.getPaddingRight()) / (float) count;
                final int saveCount = canvas.save();
                canvas.translate(mView.getPaddingLeft(), mView.getHeight() / 2);
                for (int i = 0; i <= count; i++) {
                    mTickMark.draw(canvas);
                    canvas.translate(spacing, 0);
                }
                canvas.restoreToCount(saveCount);
            }
        }
    }

drawableStateChanged() method implementation:

 void drawableStateChanged() {
        final Drawable tickMark = mTickMark;
        if (tickMark != null && tickMark.isStateful()
                && tickMark.setState(mView.getDrawableState())) {
            mView.invalidateDrawable(tickMark);
        }
    }

As can be seen from the above code, the drawableStateChanged() method is actually redrawing the View. When the state of the View changes, this method will be called to redraw the view. As can be seen from the source code, setSelected(), setActivated(), dragging the View, focus changes, setEnabled(), setPressed(), onWindowFocusChanged(), setHovered(), dispatchAttachedToWindow(), etc. will all call back, so when we drag the SeekBar, this method will be triggered. At this time, redrawing the SeekBar can redraw the View and determine whether they overlap. If they overlap, the dotted circle will not be drawn. To solve the problem that the dotted dot is suspended on the content of the drag slider.

Sorting out the ideas and how to solve the problems encountered has been said, let's take a look at the implementation and use of the code.

TickSeekBar code implementation:

public class TickSeekBar extends AppCompatSeekBar {
    private int mPaddingLeft;
    private float mSeekBlockLength;//每个片段的宽度
    private float mTrackY;//时间点y坐标
    private int mMaxProgress;//进度条最大进度
    private float mThumbCenterX;//滑块位置
    private float mLeft, mRight;//seekbar的左右位置
    private int mMeasureHeight;

    //节点时间
    //节点大小--属性
    private int mTickWith;//单位:dp
    private int mTickHeight;
    //自动吸附--属性
    private boolean isAutoAdjustTick;
    //画笔
    private Paint mStockPaint;
    private List<TickData> mTickDataList;

    public TickSeekBar(Context context) {
        super(context);
    }

    public TickSeekBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);
        initStrokePaint();
    }

    public TickSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //初始化属性
    private void initAttrs(Context context, AttributeSet attrs) {
        if (attrs == null) {
            return;
        }
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TickSeekBar);
        mTickWith = ta.getInt(R.styleable.TickSeekBar_tick_with, 2);
        mTickHeight = ta.getInt(R.styleable.TickSeekBar_tick_height, 2);
        isAutoAdjustTick = ta.getBoolean(R.styleable.TickSeekBar_auto_adjust_tick, true);
        ta.recycle();
    }

    //初始化画笔
    private void initStrokePaint() {
        if (mStockPaint == null) {
            mStockPaint = new Paint();
        }
        mStockPaint.setAntiAlias(true);
    }

    private void initSeekBarInfo() {
        int mMeasuredWidth = getMeasuredWidth();
        mMeasureHeight = getMeasuredHeight();
        mPaddingLeft = getPaddingLeft();
        int mPaddingRight = getPaddingRight();
        int mPaddingTop = getPaddingTop();
        float mSeekLength = mMeasuredWidth - mPaddingLeft - mPaddingRight;
        mSeekBlockLength = mSeekLength / mMaxProgress;
        mTrackY = mPaddingTop;
        mMaxProgress = getMax();
        mLeft = getPaddingLeft();
        mRight = getMeasuredWidth() - getPaddingRight();
    }

    private void initTickLocation(List<TickData> tickDataList) {
        if (mTickDataList == null) {
            mTickDataList = new ArrayList<>();
        } else {
            mTickDataList.clear();
        }
        mTickDataList = tickDataList;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        initSeekBarInfo();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawTicks(canvas);
    }

    private void drawTicks(Canvas canvas) {
        if (mTickDataList == null || mTickDataList.size() == 0) {
            return;
        }

        for (int i = 0; i < mTickDataList.size(); i++) {
            float locationX = mSeekBlockLength * mTickDataList.get(i).getLocation();
//            if (getThumbPosOnTick() == i) {//Seekbar滑块和tick点重合则不绘制tick点
//                continue;
//            }

            int rectWidth = mTickWith;
            float top = mTrackY + mMeasureHeight / 2f - mTickWith / 2f;
            RectF roundRect = new RectF(locationX - rectWidth, top, locationX + rectWidth, top +
                    mTickHeight);
            mStockPaint.setColor(getResources().getColor(mTickDataList.get(i).getColor()));
            canvas.drawRoundRect(roundRect, 5, 5, mStockPaint);
        }
    }

    //设置时间点
    public void setTicks(List<TickData> tickDataList) {
        initStrokePaint();
        initTickLocation(tickDataList);
        //requestLayout();
        invalidate();
    }

    public static class TickData {
        private float location;
        private int color;

        public TickData(float location, int color) {
            this.location = location;
            this.color = color;
        }

        public float getLocation() {
            return location;
        }

        public void setLocation(float location) {
            this.location = location;
        }

        public int getColor() {
            return color;
        }

        public void setColor(int color) {
            this.color = color;
        }
    }
}

TickSeekBar usage:
xml file usage:

<com.demo.customseekbar.TickSeekBar
        android:id="@+id/tickSeekbar2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:max="100"
        android:maxHeight="2dp"
        android:progress="10"
        android:progressDrawable="@drawable/seekbar"
        android:thumb="@drawable/thumb"
        android:thumbOffset="2dp"
        app:auto_adjust_tick="true"
        app:tick_height="6"
        app:tick_with="8"/>

java code implementation:

tickSeekBar = (TickSeekBar) findViewById(R.id.tickSeekbar2);
 List<CustomSeekBar.TickData> tickDataList = new ArrayList<>();
        CustomSeekBar.TickData tickData1 = new CustomSeekBar.TickData(10, android.R.color.white);
        tickDataList.add(tickData1);
        CustomSeekBar.TickData tickData2 = new CustomSeekBar.TickData(45, android.R.color
                .holo_orange_dark);
        tickDataList.add(tickData2);
        CustomSeekBar.TickData tickData3 = new CustomSeekBar.TickData(60, android.R.color.white);
        tickDataList.add(tickData3);
        CustomSeekBar.TickData tickData4 = new CustomSeekBar.TickData(90, android.R.color
                .holo_orange_dark);
        tickDataList.add(tickData4);
        customSeekBar.setTicks(tickDataList);

Note:
Be sure to set android:max, but this max is not necessarily 100, you can also customize your own rules, such as: the length of the video;
customSeekBar.setTicks(tickDataList); You can call multiple times to change the position and color of the dots.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326169228&siteId=291194637