安卓原生的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;
}
}
祝使用愉快