使用自定义view绘制灵活贴图的seekbar

安卓原生的seekbar太难用?自定义view解君愁!自定义seekbar好处如下:

  • 可以自由设定进度条的前景背景和滑块
  • 可以设定各进度条的长宽比例
  • 可以设定竖向进度条

效果

UI给了下图左边的效果图(其实是作者自己画的),并给了下图右边的三张切图,分别是进度条前景、背景、滑块。
在这里插入图片描述
如何使用这些资源做自定义view呢?直观地将效果图做切分,我们可以得到这几个部分:
切分
首先绘制完整的进度条背景,再根据进度截取前景绘制,最后将滑块绘制在进度条和前景的接缝处即可。

解读

进度条在数据层很好解释,只需要一个范围和一个当前值就够了,这里主要解读的是UI层面:
自定义view的宽和高取决于每个切图较宽的那个,如在这样的竖向seekbar中,宽为滑块的宽,而长并非进度条的高度,滑块的极限位置如图所示:
滑块位置
通过分析可以发现,滑块的极限位置并非是让一条边和进度条的边重合,而是有一段距离。一方面这是因为切图的边不一定就是图像的边,另一方面由于滑块半径和进度条的圆角半径不同,滑到极限位置的时候滑块肯定会比进度条极限多一些的。这里就需要定义一些值:

进度条宽高和滑块半径 最基础的,想要把它们画上去需要这些值确定比例
进度条背景绘制的位置 以滑块在最上面的极限位置右上角确定坐标0.0,进度条背景实际开始绘制的坐标
进度条实际的进度范围 上图两个“点”的位置才是进度条实际上表现出来的极限进度。

宽高和滑块半径(包括它们的原图)通过attr输入,其它的则需要计算。

代码

宽高设置

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        widthMeasureSpec = View.MeasureSpec.getSize(widthMeasureSpec);//dip2px(context,MeasureSpec.getSize(widthMeasureSpec));
        heightMeasureSpec = MeasureSpec.getSize(heightMeasureSpec);//dip2px(context,MeasureSpec.getSize(heightMeasureSpec));
        mThumbRadius = widthMeasureSpec / 2;
        mProgressWidth = mThumbRadius * oriWidth / oriR;
        mProgressHeight = mProgressWidth * oriHeight / oriWidth;
        if (heightMeasureSpec < mProgressHeight) {
            mProgressHeight = heightMeasureSpec - 2 * mThumbRadius;
        }
        mLeft = mThumbRadius - mProgressWidth / 2;
        mTop = mLeft;

        //长方形进度条
        //mMinY = mThumbRadius;
        //mMaxY = mProgressHeight + mThumbRadius;
        //椭圆形进度条
        mMinY = mThumbRadius;
        mMaxY = mProgressHeight-mProgressWidth +mThumbRadius;
        //LogUtil.d("measuring..."+ rawMax +" "+ progress);
        if (calProgress() != progress) {
            //计算progress是否改变,如果改变了(在定义mMaxX之前改变),则重设progress
            updateProgress();
            LogUtil.v("measured progress changed: " + progress);

        }
        //LogUtil.d("测量结果","measured progress changed: " + progress);
        setMeasuredDimension(2 * mThumbRadius, mProgressHeight + 2 * mTop);

    }

获取了view的大致区域之后,先设置滑块宽度等于区域的宽度,再根据比例算出其它的值。
有时测量需要进行很多次才能量准,因此反复确定真正的进度值。
最后将算出来的宽和高用作后续计算。

进度的读写

由于这是一个用于绘制的widget,作者将进度的读设置为一个抽象方法,继承该类的子类需要重写初始化方法和进度改变监听。

	protected abstract void setOnSeekbarChanged(int progress, int maxProgress);

    protected abstract void setDefaultPreference();

重写点击或者拖动进度条的监听事件,需要注意的是,如果拖动超过了进度条的极限,需要对进度进行限制。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float y = event.getY();

        //不可以越界
        if (y < mMinY) {
            y = mMinY;
        } else if (y > mMaxY) {
            y = mMaxY;
        }

        //传给相应的地方

        //这里与横向不同的地方就是:它的进度是下面那段
        setOnSeekbarChanged(mMaxY - (int) y, mMaxY - mMinY);

        return true;
    }

收到点击事件之后,子类重写的setOnSeekbarChanged就会被触发,例如设置音量就可以在相应的方法做到。

绘制

绘制方法需要一个progress,它是由初始化或后续点击等监听获取到的值,我们将它转化为进度条的有效长度。

    public void setRawProgress(int rawProgress, int rawMax) {
        if (rawProgress > rawMax) {
            LogUtil.w("calculated error! " + rawProgress + ">" + rawMax + ", refuse to draw.");
            return;
        }

        this.rawMax = rawMax;
        this.rawProgress = rawProgress;
        updateProgress();
        LogUtil.i("draw progress : " + rawProgress + " / " + rawMax);
        invalidate();
    }

    private int calProgress() {
        //此处有可能在mMax-mMin=0的时候取到值,所以在测量的地方会召回progress
        int calProgress = rawProgress * (mMaxY - mMinY) / rawMax;
        //防止计算越界错误
        if (calProgress >= mMaxY - mMinY){
            calProgress = mMaxY - mMinY - 1;
        } else if (calProgress<=0) {
            calProgress=1;
        }
        return calProgress;
    }

调用invalidate之后开始画,这里需要将资源文件的bitmap缩放操作,亲测使用createScaledBitmap()创造出来的图会比直接imageview糊不少,于是采用了网上大佬的缩放方法。

 private void drawProgress(Canvas canvas) {
        if(bitmapBackground==null){
            bitmapBackground = getResizerBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight);
            bitmapWholeProgress = getResizerBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight);
            bitmapThumb = getResizerBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius);
        }
//        bitmapBackground = Bitmap.createScaledBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight, false);
//        bitmapWholeProgress = Bitmap.createScaledBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight, false);
//        bitmapThumb = Bitmap.createScaledBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius, false);

        LogUtil.v("测量view: " + progress + "/(" + mMaxY + "-" + mMinY + ")  长:" + mProgressHeight + " 宽: " + mProgressWidth);
        //background
        canvas.drawBitmap(bitmapBackground, mLeft, mTop, paint);
        //绘图的中心点,也就是前文的Y
        int touchY = mMaxY - progress;
        //progress
        if (touchY > mTop && touchY < mProgressHeight + mTop) {
            Rect srcRect = new Rect(0, touchY-mTop, mProgressWidth, mProgressHeight);
            Rect dstRect = new Rect(mLeft, touchY, mLeft + mProgressWidth, mTop + mProgressHeight);
            canvas.drawBitmap(bitmapWholeProgress, srcRect, dstRect, paint);
        }

        //thumb
        canvas.drawBitmap(bitmapThumb, 0, touchY-mThumbRadius, paint);
    }

    public static Bitmap getResizerBitmap(Bitmap bitmap,int newWidth,int newHeight) {
        Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

        float ratioX = newWidth / (float) bitmap.getWidth();
        float ratioY = newHeight / (float) bitmap.getHeight();
        float middleX = newWidth / 2.0f;
        float middleY = newHeight / 2.0f;

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.setMatrix(scaleMatrix);
        canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2f, middleY - bitmap.getHeight() / 2f, new Paint(Paint.FILTER_BITMAP_FLAG));

        return scaledBitmap;

    }

此处绘制进度条前景截取的步骤是:首先srcRect画出原图应该被截取的部分,然后dstRect指示应该把截取的图绘制在哪个坐标中,调用相应方法绘制。

附录

完整代码如下,首先在attr中定义需要用到的参数:

<declare-styleable name="SeekbarVerticalWithThumb">
        <!--自定义的进度条资源-->
        <attr name="src_bk" format="reference"/>
        <attr name="src_front" format="reference"/>
        <attr name="src_thumb" format="reference"/>
        <attr name="thumb_r" format="integer"/>
        <attr name="progress_width" format="integer"/>
        <attr name="progress_height" format="integer"/>
    </declare-styleable>

然后将SeekBarVerticalWithThumb放到widget中:其中LogUtil是管理LOG的,可以改为Log正常查看输出。

package com.demo.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.demo.R;
import com.demo.util.LogUtil;

public abstract class SeekbarVerticalWithThumb extends View {

    private Context context;


    private int progress = 0;
    private int rawMax = 1, rawProgress = 0;
    //进度条的宽高
    private int mProgressWidth, mProgressHeight;
    private int mThumbRadius;

    //进度条定位
    private int mLeft, mTop;
    //滑块能拖动的范围
    private int mMinY, mMaxY;

    Paint paint;
    Bitmap bitmapProgress, bitmapBackground, bitmapWholeProgress, bitmapThumb;
    Bitmap bitmapOriginProgress, bitmapOriginBackground, bitmapOriginThumb;

    public SeekbarVerticalWithThumb(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;

        init(attrs);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float y = event.getY();

        //不可以越界
        if (y < mMinY) {
            y = mMinY;
        } else if (y > mMaxY) {
            y = mMaxY;
        }

        //传给相应的地方

        //这里与横向不同的地方就是:它的进度是下面那段
        setOnSeekbarChanged(mMaxY - (int) y, mMaxY - mMinY);

        return true;
    }


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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        widthMeasureSpec = View.MeasureSpec.getSize(widthMeasureSpec);//dip2px(context,MeasureSpec.getSize(widthMeasureSpec));
        heightMeasureSpec = MeasureSpec.getSize(heightMeasureSpec);//dip2px(context,MeasureSpec.getSize(heightMeasureSpec));
        mThumbRadius = widthMeasureSpec / 2;
        mProgressWidth = mThumbRadius * oriWidth / oriR;
        mProgressHeight = mProgressWidth * oriHeight / oriWidth;
        if (heightMeasureSpec < mProgressHeight) {
            mProgressHeight = heightMeasureSpec - 2 * mThumbRadius;
        }
        mLeft = mThumbRadius - mProgressWidth / 2;
        mTop = mLeft;

        //长方形进度条
        //mMinY = mThumbRadius;
        //mMaxY = mProgressHeight + mThumbRadius;
        //椭圆形进度条
        mMinY = mThumbRadius;
        mMaxY = mProgressHeight-mProgressWidth +mThumbRadius;
        //LogUtil.d("measuring..."+ rawMax +" "+ progress);
        if (calProgress() != progress) {
            //计算progress是否改变,如果改变了(在定义mMaxX之前改变),则重设progress
            updateProgress();
            LogUtil.v("measured progress changed: " + progress);

        }
		
        //LogUtil.d("测量结果","measured progress changed: " + progress);
        setMeasuredDimension(2 * mThumbRadius, mProgressHeight + 2 * mTop);

    }


    public void setRawProgress(int rawProgress, int rawMax) {
        if (rawProgress > rawMax) {
            LogUtil.w("calculated error! " + rawProgress + ">" + rawMax + ", refuse to draw.");
            return;
        }

        this.rawMax = rawMax;
        this.rawProgress = rawProgress;
        updateProgress();
        LogUtil.i("draw progress : " + rawProgress + " / " + rawMax);
        invalidate();
    }

    private int calProgress() {
        //此处有可能在mMax-mMin=0的时候取到值,所以在测量的地方会召回progress
        int calProgress = rawProgress * (mMaxY - mMinY) / rawMax;
        //防止计算越界错误
        if (calProgress >= mMaxY - mMinY){
            calProgress = mMaxY - mMinY - 1;
        } else if (calProgress<=0) {
            calProgress=1;
        }
        return calProgress;
    }

    private void updateProgress() {
        progress = calProgress();
    }

    protected abstract void setOnSeekbarChanged(int progress, int maxProgress);

    protected abstract void setDefaultPreference();


    int oriR,oriWidth,oriHeight;
    private void init(AttributeSet attrs) {
        paint = new Paint();
        paint.setAntiAlias(true);
        //假设是一个thumb比progress宽的view
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SeekbarVerticalWithThumb);
        int bkId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_bk,0);
        int frontId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_front,0);
        int thumbId = typedArray.getResourceId(R.styleable.SeekbarVerticalWithThumb_src_thumb,0);
        oriR = typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_thumb_r,0);
        oriWidth= typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_progress_width,0);
        oriHeight= typedArray.getInt(R.styleable.SeekbarVerticalWithThumb_progress_height,0);

        Drawable drawableBackground = context.getDrawable(bkId);
        Drawable drawableProgress = context.getDrawable(frontId);
        Drawable drawableThumb=context.getDrawable(thumbId);
        bitmapOriginBackground = ((BitmapDrawable) drawableBackground).getBitmap();
        bitmapOriginProgress = ((BitmapDrawable) drawableProgress).getBitmap();
        bitmapOriginThumb = ((BitmapDrawable) drawableThumb).getBitmap();
        typedArray.recycle();
    }

    private void drawProgress(Canvas canvas) {
        if(bitmapBackground==null){
            bitmapBackground = getResizerBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight);
            bitmapWholeProgress = getResizerBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight);
            bitmapThumb = getResizerBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius);
        }
//        bitmapBackground = Bitmap.createScaledBitmap(bitmapOriginBackground, mProgressWidth, mProgressHeight, false);
//        bitmapWholeProgress = Bitmap.createScaledBitmap(bitmapOriginProgress, mProgressWidth, mProgressHeight, false);
//        bitmapThumb = Bitmap.createScaledBitmap(bitmapOriginThumb, 2 * mThumbRadius, 2 * mThumbRadius, false);

        LogUtil.v("测量view: " + progress + "/(" + mMaxY + "-" + mMinY + ")  长:" + mProgressHeight + " 宽: " + mProgressWidth);
        //background
        canvas.drawBitmap(bitmapBackground, mLeft, mTop, paint);
        //绘图的中心点,也就是前文的Y
        int touchY = mMaxY - progress;
        //progress
        if (touchY > mTop && touchY < mProgressHeight + mTop) {
            Rect srcRect = new Rect(0, touchY-mTop, mProgressWidth, mProgressHeight);
            Rect dstRect = new Rect(mLeft, touchY, mLeft + mProgressWidth, mTop + mProgressHeight);
            canvas.drawBitmap(bitmapWholeProgress, srcRect, dstRect, paint);
        }

        //thumb
        canvas.drawBitmap(bitmapThumb, 0, touchY-mThumbRadius, paint);
    }

    public static Bitmap getResizerBitmap(Bitmap bitmap,int newWidth,int newHeight) {
        Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);

        float ratioX = newWidth / (float) bitmap.getWidth();
        float ratioY = newHeight / (float) bitmap.getHeight();
        float middleX = newWidth / 2.0f;
        float middleY = newHeight / 2.0f;

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.setMatrix(scaleMatrix);
        canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2f, middleY - bitmap.getHeight() / 2f, new Paint(Paint.FILTER_BITMAP_FLAG));

        return scaledBitmap;

    }
}

祝使用愉快

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值