Android 自定義View 音量控制View

類似iOS的控制中心里,音量的上下滑動(dòng)增大甩牺、減小音量。

view.gif

簡(jiǎn)單說一下需求:

  • 需要進(jìn)度條有圓角累奈,使用Canvas裁切即可
  • 手勢(shì)拖動(dòng)進(jìn)度條更新進(jìn)度贬派,也可以代碼調(diào)用更新進(jìn)度
  • 支持XML和Java代碼設(shè)置最大值、最小值澎媒、當(dāng)前值搞乏,以及進(jìn)度的顏色、圓角半徑等

自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="VerticalSeekBar">
        <!-- 背景進(jìn)度顏色 -->
        <attr name="vsb_bg" format="color" />
        <!-- 已有進(jìn)度顏色 -->
        <attr name="vsb_progress_bg" format="color" />
        <!-- 進(jìn)度條圓角半徑 -->
        <attr name="vsb_bg_radius" format="integer|float|dimension" />
        <!-- 當(dāng)前進(jìn)度值 -->
        <attr name="vsb_progress" format="float" />
        <!-- 最小進(jìn)度值 -->
        <attr name="vsb_min_progress" format="float" />
        <!-- 最大進(jìn)度值 -->
        <attr name="vsb_max_progress" format="float" />
    </declare-styleable>
</resources>

完整代碼

/**
 * 垂直拽托進(jìn)度條
 */
public class VerticalSeekBar extends View {
    /**
     * View默認(rèn)最小寬度
     */
    private int mDefaultWidth;
    /**
     * View默認(rèn)最小高度
     */
    private int mDefaultHeight;
    /**
     * 控件寬
     */
    private int mViewWidth;
    /**
     * 控件高
     */
    private int mViewHeight;
    /**
     * 背景顏色
     */
    private int mBgColor;
    /**
     * 進(jìn)度背景顏色
     */
    private int mProgressBgColor;
    /**
     * 進(jìn)度條的圓角半徑
     */
    private int mBgRadius;
    /**
     * 當(dāng)前進(jìn)度
     */
    private float mProgress;
    /**
     * 最小進(jìn)度值
     */
    private float mMin;
    /**
     * 最大進(jìn)度值
     */
    private float mMax;
    /**
     * 背景畫筆
     */
    private Paint mBgPaint;
    /**
     * 進(jìn)度畫筆
     */
    private Paint mProgressPaint;
    /**
     * 進(jìn)度更新監(jiān)聽
     */
    private VerticalSeekBar.OnProgressUpdateListener mOnProgressUpdateListener;

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

    public VerticalSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        initAttr(context, attrs, defStyleAttr);
        //取消硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        //背景畫筆
        mBgPaint = new Paint();
        mBgPaint.setAntiAlias(true);
        mBgPaint.setColor(mBgColor);
        mBgPaint.setStyle(Paint.Style.FILL);
        //進(jìn)度畫筆
        mProgressPaint = new Paint();
        mProgressPaint.setColor(mProgressBgColor);
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setStyle(Paint.Style.FILL);
        //計(jì)算默認(rèn)寬戒努、高
        mDefaultWidth = dip2px(context, 36f);
        mDefaultHeight = dip2px(context, 114f);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        int defaultBgColor = Color.parseColor("#EDF0FA");
        int defaultProgressBgColor = Color.parseColor("#6D79FE");
        int defaultBgRadius = dip2px(context, 8f);
        float defaultProgress = 0;
        float defaultMinProgress = 0;
        int defaultMaxProgress = 100;
        if (attrs != null) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.VerticalSeekBar, defStyleAttr, 0);
            //進(jìn)度背景顏色
            mBgColor = array.getColor(R.styleable.VerticalSeekBar_vsb_bg, defaultBgColor);
            //已有進(jìn)度的背景顏色
            mProgressBgColor = array.getColor(R.styleable.VerticalSeekBar_vsb_progress_bg, defaultProgressBgColor);
            //進(jìn)度條的圓角
            mBgRadius = array.getDimensionPixelSize(R.styleable.VerticalSeekBar_vsb_bg_radius, defaultBgRadius);
            //當(dāng)前進(jìn)度值
            mProgress = array.getFloat(R.styleable.VerticalSeekBar_vsb_progress, defaultProgress);
            //最小進(jìn)度值
            mMin = array.getFloat(R.styleable.VerticalSeekBar_vsb_min_progress, defaultMinProgress);
            //最大進(jìn)度值
            mMax = array.getFloat(R.styleable.VerticalSeekBar_vsb_max_progress, defaultMaxProgress);
            array.recycle();
        } else {
            mBgColor = defaultBgColor;
            mProgressBgColor = defaultProgressBgColor;
            mProgress = defaultProgress;
            mMin = defaultMinProgress;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //裁切圓角
        clipRound(canvas);
        //畫背景
        drawBg(canvas);
        //畫進(jìn)度條
        drawProgress(canvas);
    }

    //------------ getFrameXxx()方法都是處理padding ------------

    private float getFrameLeft() {
        return getPaddingStart();
    }

    private float getFrameRight() {
        return mViewWidth - getPaddingEnd();
    }

    private float getFrameTop() {
        return getPaddingTop();
    }

    private float getFrameBottom() {
        return mViewHeight - getPaddingBottom();
    }

    //------------ getFrameXxx()方法都是處理padding ------------

    /**
     * 裁剪圓角
     */
    private void clipRound(Canvas canvas) {
        Path path = new Path();
        RectF roundRect = new RectF(getFrameLeft(), getFrameTop(), getFrameRight(), getFrameBottom());
        path.addRoundRect(roundRect, mBgRadius, mBgRadius, Path.Direction.CW);
        canvas.clipPath(path);
    }

    /**
     * 畫背景
     */
    private void drawBg(Canvas canvas) {
        canvas.drawRect(new RectF(getFrameLeft(), getFrameTop(),
                        getFrameRight(), getFrameBottom()),
                mBgPaint);
    }

    /**
     * 畫進(jìn)度
     */
    private void drawProgress(Canvas canvas) {
        float contentHeight = mViewHeight - getPaddingTop() - getPaddingBottom();
        //計(jì)算出當(dāng)前進(jìn)度應(yīng)該有個(gè)top值请敦,因?yàn)檫M(jìn)度是從小往上,所以百分比要被1減去
        float progressRatio = getProgressRatio();
        float top;
        if (progressRatio == 0) {
            top = contentHeight;
        } else {
            top = contentHeight * (1 - progressRatio);
        }
        //畫進(jìn)度矩形
        RectF rect = new RectF(getFrameLeft(),
                top,
                getFrameRight(),
                getFrameBottom());
        canvas.drawRect(rect, mProgressPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(handleMeasure(widthMeasureSpec, true),
                handleMeasure(heightMeasureSpec, false));
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //攔截Down事件储玫,然后讓父類不進(jìn)行攔截
        if (action == MotionEvent.ACTION_DOWN) {
            getParent().requestDisallowInterceptTouchEvent(true);
            if (mOnProgressUpdateListener != null) {
                mOnProgressUpdateListener.onStartTrackingTouch(this);
            }
            return true;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int contentHeight = mViewHeight - getPaddingTop() - getPaddingBottom();
        if (action == MotionEvent.ACTION_DOWN) {
            return true;
        } else if (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_UP) {
            //Move或Up的時(shí)候侍筛,計(jì)算拽托進(jìn)度
            float endY = event.getY();
            //限制拉到頂
            if (endY < 0) {
                endY = 0;
            }
            //限制拉到底
            if (endY > contentHeight) {
                endY = contentHeight;
            }
            //計(jì)算觸摸點(diǎn)和高度的差值
            float distanceY = Math.abs(contentHeight - endY);
            float ratio = distanceY / contentHeight;
            //計(jì)算百分比應(yīng)該有的進(jìn)度:進(jìn)度 = 總進(jìn)度 * 進(jìn)度百分比值
            float progress = mMax * ratio;
            setProgress(progress, true);
            if (action == MotionEvent.ACTION_UP) {
                if (mOnProgressUpdateListener != null) {
                    mOnProgressUpdateListener.onStopTrackingTouch(this);
                }
            }
            return true;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 處理MeasureSpec
     */
    private int handleMeasure(int measureSpec, boolean isWidth) {
        int result;
        if (isWidth) {
            result = mDefaultWidth;
        } else {
            result = mDefaultHeight;
        }
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            //處理wrap_content的情況
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * 設(shè)置進(jìn)度背景顏色
     */
    public void setBgColor(int bgColor) {
        //沒有變化,不重繪
        if (bgColor == mBgColor) {
            return;
        }
        this.mBgColor = bgColor;
        mBgPaint.setColor(bgColor);
        invalidate();
    }

    /**
     * 設(shè)置已有進(jìn)度的背景顏色
     */
    public void setProgressBgColor(int progressBgColor) {
        //沒有變化撒穷,不重繪
        if (progressBgColor == mProgressBgColor) {
            return;
        }
        this.mProgressBgColor = progressBgColor;
        mProgressPaint.setColor(progressBgColor);
        invalidate();
    }

    /**
     * 設(shè)置進(jìn)度
     */
    public void setProgress(float progress) {
        setProgress(progress, false);
    }

    /**
     * 設(shè)置進(jìn)度
     *
     * @param fromUser 是否是用戶觸摸發(fā)生的改變
     */
    public void setProgress(float progress, boolean fromUser) {
        //忽略相同進(jìn)度的設(shè)置
        if (mProgress == progress) {
            return;
        }
//        if (progress > mMin && progress < mMax) {
//        }
        this.mProgress = progress;
        invalidate();
        if (mOnProgressUpdateListener != null) {
            mOnProgressUpdateListener.onProgressUpdate(this, progress, fromUser);
        }
    }

    /**
     * 獲取當(dāng)前進(jìn)度
     */
    public float getProgress() {
        return mProgress;
    }

    /**
     * 設(shè)置進(jìn)度最小值
     */
    public void setMin(float min) {
        this.mMin = min;
        invalidate();
    }

    /**
     * 獲取最小進(jìn)度
     */
    public float getMin() {
        return mMin;
    }

    /**
     * 設(shè)置進(jìn)度最大值
     */
    public void setMax(float max) {
        this.mMax = max;
        invalidate();
    }

    /**
     * 獲取最大進(jìn)度
     */
    public float getMax() {
        return mMax;
    }

    public interface OnProgressUpdateListener {
        /**
         * 按下時(shí)回調(diào)
         */
        void onStartTrackingTouch(VerticalSeekBar seekBar);

        /**
         * 進(jìn)度更新時(shí)回調(diào)
         *
         * @param progress 當(dāng)前進(jìn)度
         * @param fromUser 是否是用戶改變的
         */
        void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser);

        /**
         * 松手時(shí)回調(diào)
         */
        void onStopTrackingTouch(VerticalSeekBar seekBar);
    }

    public static class SimpleProgressUpdateListener implements OnProgressUpdateListener {
        @Override
        public void onStartTrackingTouch(VerticalSeekBar seekBar) {
        }

        @Override
        public void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser) {
        }

        @Override
        public void onStopTrackingTouch(VerticalSeekBar seekBar) {
        }
    }

    public void setOnProgressUpdateListener(
            VerticalSeekBar.OnProgressUpdateListener onProgressUpdateListener) {
        mOnProgressUpdateListener = onProgressUpdateListener;
    }

    /**
     * 獲取當(dāng)前進(jìn)度值比值
     */
    public float getProgressRatio() {
        return mProgress / (mMax * 1.0f);
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}

使用

  • xml中添加控件匣椰,記得改控件的包名
  <com.zh.cavas.sample.widget.VerticalSeekBar
      android:id="@+id/volume_seek_bar"
      android:layout_width="36dp"
      android:layout_height="114dp"
      tools:vsb_progress="50"
      app:vsb_min_progress="1"
      app:vsb_max_progress="100"
      app:vsb_bg="#EDF0FA"
      app:vsb_progress_bg="#3DDA9B"
      app:vsb_bg_radius="8dp" />
  • 設(shè)置監(jiān)聽回調(diào),最小值端礼、最大值窝爪,當(dāng)前進(jìn)度
VerticalSeekBar volumeSeekBar = findViewById(R.id.volume_seek_bar);
volumeSeekBar.setOnProgressUpdateListener(new VerticalSeekBar.SimpleProgressUpdateListener() {
    @SuppressLint("SetTextI18n")
    @Override
    public void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser) {
        //設(shè)置音量
        Log.d(TAG, "VolumeSeekBar onProgressUpdate => progress = " + progress);
    }
});
volumeSeekBar.setMin(0f);
volumeSeekBar.setMax(1f);
volumeSeekBar.setProgress(0.6f);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弛车,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蒲每,更是在濱河造成了極大的恐慌纷跛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邀杏,死亡現(xiàn)場(chǎng)離奇詭異贫奠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)望蜡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門唤崭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脖律,你說我怎么就攤上這事暖庄』聘眨” “怎么了殊校?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵肢预,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我微姊,道長(zhǎng)酸茴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任兢交,我火速辦了婚禮薪捍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘配喳。我一直安慰自己酪穿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布晴裹。 她就那樣靜靜地躺著被济,像睡著了一般。 火紅的嫁衣襯著肌膚如雪息拜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天净响,我揣著相機(jī)與錄音少欺,去河邊找鬼。 笑死馋贤,一個(gè)胖子當(dāng)著我的面吹牛赞别,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播配乓,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼仿滔,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼惠毁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起崎页,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤鞠绰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后飒焦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜈膨,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年牺荠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翁巍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡休雌,死狀恐怖灶壶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杈曲,我是刑警寧澤驰凛,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站鱼蝉,受9級(jí)特大地震影響洒嗤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜魁亦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一渔隶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洁奈,春花似錦间唉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至印叁,卻和暖如春被冒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轮蜕。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工昨悼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跃洛。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓率触,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親汇竭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子葱蝗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容