带尖角的水平进度条

   项目中需要使用到带尖角的水平进度条。效果如下图所示。

1)用户点看好的时候,突出显示看好的百分比;

2)用户点不看好时,突出显示不看好的百分比进度。

分析:此控件与系统标准的进度条控件相比,尖角的显示会是一个比较麻烦的事。如果采用系统的shape的方式定义,进度条进度进度一般是直角,或者是圆角。特殊样式,9宫格drawable对象的的拉升也能实现。但是相对于多种样式的变化,相对又比较麻烦。

设计思路:

采用绘图的方法,绘制进度,尖角通过绘制三角形的方式来实现。绘制步骤:

1)绘制进度条背景;

2)绘制进度条。当没有百分百进度时,绘制带尖角的进度;当百分百进度时,绘制完整的进度条(圆角)。

/**
 * 带尖角的水平进度条
 *
 * @author Joe
 * @version 1.0 2018/07/30
 */
public class HorizontalProgressBarWithAngle extends View {
    private static final String TAG = HorizontalProgressBarWithAngle.class.getSimpleName();
    /** Duration of smooth progress animations. */
    private static final int PROGRESS_ANIM_DURATION = 80;
    private static final int MAX_LEVEL = 10000;
    private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
    /** Interpolator used for smooth progress animations. */
    private static final DecelerateInterpolator PROGRESS_ANIM_INTERPOLATOR =
            new DecelerateInterpolator();

    protected final static String MATERIALDESIGNXML = "http://schemas.android.com/apk/res-auto";
    protected final static String ANDROIDXML = "http://schemas.android.com/apk/res/android";
    /**
     * 是否需要刷新
     */
    private boolean mNoInvalidate;
    /**
     * 不确定的
     */
    private boolean mIndeterminate;
    int mMinWidth;
    int mMaxWidth;
    int mMinHeight;
    int mMaxHeight;

    /** Value used to track progress animation, in the range [0...1]. */
    private float mVisualProgress;
    /**
     * 圆角半径
     */
    int mCornerRadius;

    /**
     * 当前进度
     */
    private int mProgress;
    /**
     * 第二进度
     */
    private int mSecondaryProgress;
    /**
     * 最小值
     */
    private int mMin;
    private boolean mMinInitialized;
    /**
     * 最大进度值
     */
    private int mMax;
    private boolean mMaxInitialized;
    /**
     * 进度条背景色
     */
    protected int mBarColor = Color.GRAY;
    /**
     * 加载进度条的颜色
     */
    protected int mProgressColor = 0xFF00CDB0;
    /**
     * 第二加载进度条的颜色
     */
    protected int mSecondaryProgressColor = 0xffe6e6e6;
    /**
     * UI线程
     */
    private long mUiThreadId;
    /**
     * 画笔
     */
    private Paint mPaint;
    /**
     * 进度条背景区域
     */
    private Rect mRect;
    /**
     * 进度条的区域
     */
    private Rect mProgressRect;
    Path mPath = new Path();
    /**
     * 进度条背景区域
     */
    private GradientDrawable mBarDrawable;
    private GradientDrawable mProgressDrawable;
    private Drawable mCurrentDrawable;
    private AccessibilityEventSender mAccessibilityEventSender;
    /**
     * 进度刷新Runnable
     */
    private RefreshProgressRunnable mRefreshProgressRunnable;
    /**
     * 刷新数据列表
     */
    private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>();
    private boolean mAttached;
    private boolean mRefreshIsPosted;
    /**
     * Command for sending an accessibility event.
     */
    private class AccessibilityEventSender implements Runnable {
        public void run() {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }

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

    public HorizontalProgressBarWithAngle(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HorizontalProgressBarWithAngle(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HorizontalProgressBarWithAngle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr);
        mUiThreadId = Thread.currentThread().getId();
        //初始化进度条
        initProgressBar();
        //初始化进度条的值
        initAttributes(context, attrs, defStyleAttr, defStyleRes);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRect = new Rect(0, 0, mMaxWidth, mMaxHeight);
        mBarDrawable = new GradientDrawable();
        mProgressDrawable = new GradientDrawable();
        mCornerRadius = 8;
    }

    /**
     * 初始化属性值
     *
     * @param context
     * @param attrs
     */
    private void initAttributes(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        //获取通用属性
        mMinWidth = attrs.getAttributeIntValue(ANDROIDXML, "minWidth", mMinWidth);
        mMaxWidth = attrs.getAttributeIntValue(ANDROIDXML, "maxWidth", mMaxWidth);
        mMinHeight = attrs.getAttributeIntValue(ANDROIDXML, "minHeight", mMinHeight);
        mMaxHeight = attrs.getAttributeIntValue(ANDROIDXML, "maxHeight", mMaxHeight);
        setMin(attrs.getAttributeIntValue(ANDROIDXML, "min", mMin));
        setMax(attrs.getAttributeIntValue(ANDROIDXML, "max", mMax));
        setProgress(attrs.getAttributeIntValue(ANDROIDXML, "progress", mProgress));

        //读取自定义属性
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.HorizontalProgressBarWithAngle);
        //背景颜色
        mBarColor = a.getColor(R.styleable.HorizontalProgressBarWithAngle_hpb_barColor, Color.GRAY);
        //进度条颜色
        mProgressColor = a.getColor(R.styleable.HorizontalProgressBarWithAngle_hpb_progressColor, 0xFF00CDB0);
        a.recycle();

        mNoInvalidate = true;
    }

    /**
     * <p>Return the upper limit of this progress bar's range.</p>
     *
     * @return a positive integer
     *
     * @see #setMax(int)
//     * @see #getProgress()
//     * @see #getSecondaryProgress()
     */
    @ViewDebug.ExportedProperty(category = "progress")
    public synchronized int getMax() {
        return mMax;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mRefreshData != null) {
            synchronized (this) {
                final int count = mRefreshData.size();
                for (int i = 0; i < count; i++) {
                    final RefreshData rd = mRefreshData.get(i);
                    doRefreshProgress(rd.progress, rd.fromUser, true, rd.animate);
                    rd.recycle();
                }
                mRefreshData.clear();
            }
        }
        mAttached = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        if (mRefreshProgressRunnable != null) {
            removeCallbacks(mRefreshProgressRunnable);
            mRefreshIsPosted = false;
        }
        if (mAccessibilityEventSender != null) {
            removeCallbacks(mAccessibilityEventSender);
        }
        // This should come after stopAnimation(), otherwise an invalidate message remains in the
        // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
        super.onDetachedFromWindow();
        mAttached = false;
    }

    /**
     * <p>Set the lower range of the progress bar to <tt>min</tt>.</p>
     *
     * @param min the lower range of this progress bar
     *
//     * @see #getMin()
//     * @see #setProgress(int)
//     * @see #setSecondaryProgress(int)
     */
//    @android.view.RemotableViewMethod
    public synchronized void setMin(int min) {
        if (mMaxInitialized) {
            if (min > mMax) {
                min = mMax;
            }
        }
        mMinInitialized = true;
        if (mMaxInitialized && min != mMin) {
            mMin = min;
            postInvalidate();

            if (mProgress < min) {
                mProgress = min;
            }
            refreshProgress(mProgress, false, false);
        } else {
            mMin = min;
        }
    }

    /**
     * <p>Set the upper range of the progress bar <tt>max</tt>.</p>
     *
     * @param max the upper range of this progress bar
     *
     * @see #getMax()
//     * @see #setProgress(int)
//     * @see #setSecondaryProgress(int)
     */
//    @android.view.RemotableViewMethod
    public synchronized void setMax(int max) {
        if (mMinInitialized) {
            if (max < mMin) {
                max = mMin;
            }
        }
        mMaxInitialized = true;
        if (mMinInitialized && max != mMax) {
            mMax = max;
            postInvalidate();

            if (mProgress > max) {
                mProgress = max;
            }
            refreshProgress(mProgress, false, false);
        } else {
            mMax = max;
        }
    }

    /**
     * Sets the current progress to the specified value. Does not do anything
     * if the progress bar is in indeterminate mode.
     * <p>
     * This method will immediately update the visual position of the progress
     * indicator. To animate the visual position to the target value, use
     * {@link #setProgress(int, boolean)}}.
     *
     * @param progress the new progress, between 0 and {@link #getMax()}
     *
//     * @see #setIndeterminate(boolean)
//     * @see #isIndeterminate()
//     * @see #getProgress()
//     * @see #incrementProgressBy(int)
     */
//    @android.view.RemotableViewMethod
    public synchronized void setProgress(int progress) {
        setProgressInternal(progress, false, false);
    }

    /**
     * Sets the current progress to the specified value, optionally animating
     * the visual position between the current and target values.
     * <p>
     * Animation does not affect the result of getProgress(), which
     * will return the target value immediately after this method is called.
     *
     * @param progress the new progress value, between 0 and {@link #getMax()}
     * @param animate {@code true} to animate between the current and target
     *                values or {@code false} to not animate
     */
    public void setProgress(int progress, boolean animate) {
        setProgressInternal(progress, false, animate);
    }

    /**
     * 设置进度条背景色
     *
     * @param mBarColor
     */
    public void setBarColor(int mBarColor) {
        this.mBarColor = mBarColor;
        postInvalidate();
    }

    /**
     * 设置进度条颜色
     *
     * @param mProgressColor
     */
    public void setProgressColor(int mProgressColor) {
        this.mProgressColor = mProgressColor;
        postInvalidate();
    }

    //    @android.view.RemotableViewMethod
    synchronized boolean setProgressInternal(int progress, boolean fromUser, boolean animate) {
        if (mIndeterminate) {
            // Not applicable.
            return false;
        }

        progress = MathUtils.clamp(progress, mMin, mMax);

        if (progress == mProgress) {
            // No change from current.
            return false;
        }

        mProgress = progress;
        refreshProgress(mProgress, fromUser, animate);
        return true;
    }

    /**
     * 刷新进度
     *
     * @param progress
     * @param fromUser
     * @param animate
     */
    private synchronized void refreshProgress(int progress, boolean fromUser,
                                              boolean animate) {
        if (mUiThreadId == Thread.currentThread().getId()) {
            doRefreshProgress(progress, fromUser, true, animate);
        } else {
            if (mRefreshProgressRunnable == null) {
                mRefreshProgressRunnable = new RefreshProgressRunnable();
            }

            final RefreshData rd = RefreshData.obtain(progress, fromUser, animate);
            mRefreshData.add(rd);
            if (mAttached && !mRefreshIsPosted) {
                post(mRefreshProgressRunnable);
                mRefreshIsPosted = true;
            }
        }
    }

    /**
     * 进度刷新任务
     */
    private class RefreshProgressRunnable implements Runnable {
        public void run() {
            synchronized (HorizontalProgressBarWithAngle.this) {
                final int count = mRefreshData.size();
                for (int i = 0; i < count; i++) {
                    final RefreshData rd = mRefreshData.get(i);
                    doRefreshProgress(rd.progress, rd.fromUser, true, rd.animate);
                    rd.recycle();
                }
                mRefreshData.clear();
                mRefreshIsPosted = false;
            }
        }
    }

    private static class RefreshData {
        private static final int POOL_MAX = 24;
        private static final Pools.SynchronizedPool<RefreshData> sPool =
                new Pools.SynchronizedPool<RefreshData>(POOL_MAX);

        public int progress;
        public boolean fromUser;
        public boolean animate;

        public static RefreshData obtain(int progress, boolean fromUser, boolean animate) {
            RefreshData rd = sPool.acquire();
            if (rd == null) {
                rd = new RefreshData();
            }
            rd.progress = progress;
            rd.fromUser = fromUser;
            rd.animate = animate;
            return rd;
        }

        public void recycle() {
            sPool.release(this);
        }
    }

    private synchronized void doRefreshProgress(int progress, boolean fromUser,
                                                boolean callBackToApp, boolean animate) {
        int range = mMax - mMin;
        final float scale = range > 0 ? (progress - mMin) / (float) range : 0;

        if (animate) {
            final ObjectAnimator animator;
            animator = ObjectAnimator.ofFloat(this, "visual_progress", mVisualProgress, scale);
            animator.setDuration(PROGRESS_ANIM_DURATION);
            animator.setInterpolator(PROGRESS_ANIM_INTERPOLATOR);
            animator.start();
        } else {
            setVisualProgress(scale);
        }

        if (callBackToApp) {
            onProgressRefresh(scale, fromUser, progress);
        }
    }

    void onProgressRefresh(float scale, boolean fromUser, int progress) {

    }

    /**
     * Schedule a command for sending an accessibility event.
     * </br>
     * Note: A command is used to ensure that accessibility events
     *       are sent at most one in a given time frame to save
     *       system resources while the progress changes quickly.
     */
    private void scheduleAccessibilityEventSender() {
        if (mAccessibilityEventSender == null) {
            mAccessibilityEventSender = new AccessibilityEventSender();
        } else {
            removeCallbacks(mAccessibilityEventSender);
        }
        postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
    }

    /**
     * Sets the visual state of a progress indicator.
     *
     * @param progress the visual progress in the range [0...1]
     */
    private void setVisualProgress(float progress) {
        mVisualProgress = progress;

        //刷新进度
        invalidate();
    }

    /**
     * <p>
     * Initialize the progress bar's default values:
     * </p>
     * <ul>
     * <li>progress = 0</li>
     * <li>max = 100</li>
     * <li>animation duration = 4000 ms</li>
     * <li>indeterminate = false</li>
     * <li>behavior = repeat</li>
     * </ul>
     */
    private void initProgressBar() {
        mMin = 0;
        mMax = 100;
        mProgress = 0;
        mSecondaryProgress = 0;
        mMinWidth = 24;
        mMaxWidth = 48;
        mMinHeight = 24;
        mMaxHeight = 48;
        mCornerRadius = 4;
    }

    /**
     * 测量进度条的宽高
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int dw = 0;
        int dh = 0;

        dw = Math.max(mMinWidth, mMaxWidth);
        dh = Math.max(mMinHeight, mMaxHeight);

        //增加边距
        dw += getPaddingLeft() + getPaddingRight();
        dh += getPaddingTop() + getPaddingBottom();

        final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
        final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
        Log.d(TAG, "measuredWidth: " + measuredWidth + " measuredHeight:" + measuredHeight);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    /**
     * 绘制进度条
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制背景
        drawBarBackground(canvas);

        //绘制进度条
        drawProgress(canvas);
    }

    /**
     * 绘制进度条背景
     *
     * @param canvas
     */
    private void drawBarBackground(Canvas canvas) {
        float r = mCornerRadius;
        canvas.save();
        //绘制背景色
        Log.d(TAG, "onDraw measuredWidth: " + getMeasuredWidth() + " measuredHeight:" + getMeasuredHeight());
        mRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
        mBarDrawable.setBounds(mRect);
        mBarDrawable.setColor(mBarColor);
        mBarDrawable.setShape(GradientDrawable.RECTANGLE);
        mBarDrawable.setGradientRadius((float)(Math.sqrt(2) * 60));
        setCornerRadii(mBarDrawable, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius);
        mBarDrawable.draw(canvas);
        canvas.restore();
    }

    /**
     * 绘制进度条
     *
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        if(mProgress <= 0) {
            return;
        }
        //特殊情况(100%进度不需要展示尖角)
        if(mProgress >= mMax) {
            //已经是全部进度
            drawFullProgress(canvas);
            return;
        }
        //计算当前的进度区域
        canvas.save();
        if(null == mProgressRect) {
            mProgressRect = new Rect();
        }
        int width = getMeasuredWidth() / mMax * mProgress;
        mProgressRect.set(0, 0, width, getMeasuredHeight());
        mPath.reset();
        canvas.clipPath(mPath); // makes the clip empty
        //mpathd的起始位置
        mPath.moveTo(0, 0);
        //从起始位置划线到(200, 200)坐标
        mPath.lineTo(width, 0);
        mPath.lineTo(width - getMeasuredHeight() / 3, getMeasuredHeight());
        mPath.lineTo(0, getMeasuredHeight());
        //将mpath封闭,也可以写 mpath.lineTo(100, 100);代替
        mPath.close();
        //裁剪一个三角形
        canvas.clipPath(mPath, Region.Op.REPLACE);

        //绘制进度图形
        mProgressDrawable.setBounds(mProgressRect);
        mProgressDrawable.setColor(mProgressColor);
        mProgressDrawable.setShape(GradientDrawable.RECTANGLE);
        mProgressDrawable.setGradientRadius((float)(Math.sqrt(2) * 60));
        setCornerRadii(mProgressDrawable, mCornerRadius, 0, mCornerRadius, 0);
        mProgressDrawable.draw(canvas);
        canvas.restore();
    }

    /**
     * 绘制全部进度
     *
     * @param canvas
     */
    private void drawFullProgress(Canvas canvas) {
        canvas.save();
        //绘制背景色
        if(null == mProgressRect) {
            mProgressRect = new Rect();
        }
        Log.d(TAG, "drawFullProgress measuredWidth: " + getMeasuredWidth() + " measuredHeight:" + getMeasuredHeight());
        mProgressRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
        mProgressDrawable.setBounds(mProgressRect);
        mProgressDrawable.setColor(mProgressColor);
        mProgressDrawable.setShape(GradientDrawable.RECTANGLE);
        mProgressDrawable.setGradientRadius((float)(Math.sqrt(2) * 60));
        setCornerRadii(mProgressDrawable, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius);
        mProgressDrawable.draw(canvas);
        canvas.restore();
    }

    /**
     * 设置矩形的边角半径
     *
     * @param drawable
     * @param leftTop
     * @param rightTop
     * @param leftBottom
     * @param rightBottom
     */
    static void setCornerRadii(GradientDrawable drawable, float leftTop,
                               float rightTop, float leftBottom, float rightBottom) {
        drawable.setCornerRadii(new float[] { leftTop, leftTop, rightTop, rightTop,
                rightBottom, rightBottom, leftBottom, leftBottom });
    }

    private final FloatProperty<HorizontalProgressBarWithAngle> VISUAL_PROGRESS =
            new FloatProperty<HorizontalProgressBarWithAngle>("visual_progress") {
                @Override
                public void setValue(HorizontalProgressBarWithAngle object, float value) {
                    object.mVisualProgress = value;
                }

                @Override
                public Float get(HorizontalProgressBarWithAngle object) {
                    return object.mVisualProgress;
                }
            };
}

代码分析

1)带尖角的进度怎么绘制?可通过以下步骤实现:

1、裁剪掉三角形区域,即裁剪出一个带尖角进度的区域。

mPath.reset();
canvas.clipPath(mPath); // makes the clip empty
//mpathd的起始位置
mPath.moveTo(0, 0);
//从起始位置划线到(200, 200)坐标
mPath.lineTo(width, 0);
mPath.lineTo(width - getMeasuredHeight() / 3, getMeasuredHeight());
mPath.lineTo(0, getMeasuredHeight());
//将mpath封闭,也可以写 mpath.lineTo(100, 100);代替
mPath.close();
//裁剪一个三角形
canvas.clipPath(mPath, Region.Op.REPLACE);

裁剪出需要的带尖角的区域。

/**
 * 设置矩形的边角半径
 *
 * @param drawable
 * @param leftTop
 * @param rightTop
 * @param leftBottom
 * @param rightBottom
 */
static void setCornerRadii(GradientDrawable drawable, float leftTop,
                           float rightTop, float leftBottom, float rightBottom) {
    drawable.setCornerRadii(new float[] { leftTop, leftTop, rightTop, rightTop,
            rightBottom, rightBottom, leftBottom, leftBottom });
}

2、在尖角画布区域绘制带颜色的尖角进度条。

//绘制进度图形
mProgressDrawable.setBounds(mProgressRect);
mProgressDrawable.setColor(mProgressColor);
mProgressDrawable.setShape(GradientDrawable.RECTANGLE);
mProgressDrawable.setGradientRadius((float)(Math.sqrt(2) * 60));
setCornerRadii(mProgressDrawable, mCornerRadius, 0, mCornerRadius, 0);
mProgressDrawable.draw(canvas);
canvas.restore();

进度区域即第三步的绿色区域。因为第二步裁剪了有效的区域只有ABDC,因此实际上

显示的图案就是ABDC区域内的带尖角的进度条。

3、百分百时候的进度绘制。这个比较简单,整个进度条区域绘制带圆角即可。

/**
 * 绘制全部进度
 *
 * @param canvas
 */
private void drawFullProgress(Canvas canvas) {
    canvas.save();
    //绘制背景色
    if(null == mProgressRect) {
        mProgressRect = new Rect();
    }
    Log.d(TAG, "drawFullProgress measuredWidth: " + getMeasuredWidth() + " measuredHeight:" + getMeasuredHeight());
    mProgressRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
    mProgressDrawable.setBounds(mProgressRect);
    mProgressDrawable.setColor(mProgressColor);
    mProgressDrawable.setShape(GradientDrawable.RECTANGLE);
    mProgressDrawable.setGradientRadius((float)(Math.sqrt(2) * 60));
    setCornerRadii(mProgressDrawable, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius);
    mProgressDrawable.draw(canvas);
    canvas.restore();
}

 

项目的地址:

https://github.com/joedan0104/HorizontalProgressBarWithAngle

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值