业务上需要实现自定义进度条的样式,且样式不是常规的形状,需要自定义绘制实现,先来看下效果
一、实现效果说明
进度条背景和进度由自定义Path区域去绘制实现,Path区域菱形倾斜X轴方向的偏移量由直角三角形的方式去计算。
1. 渐变滑块实现问题
因为滑块是菱形渐变色,处理起来稍微麻烦点,从实际的实现测试来看,不能用图片,渐变滑块图片配置上去之后,中间部分有一条明显的竖线无法去除。
之后用LinearGradient去实现渐变色,但是LinearGradient的渐变色是在固定区域的,比如配置了X轴0到25的渐变色,滑块滑出的范围超过25之后显示就有异常,之后使用折衷的方式,把整个进度条拆分成50份渐变色区域,滑动的时候根据当前的进度获取对应的渐变LinearGradient,这种实现方式需要缓存50份LinearGradient对象,浪费资源。
最终是使用canvas.translate的方式,只需要创建一个LinearGradient对象,通过移动画布的方式来实现渐变滑块的绘制。
2. 滑动进度不跟随鼠标点击位置问题
当进度条最大值比较小时,问题会比较明显,如进度条最大值设置为30,那么每一个进度值就对应一小段进度区间,鼠标点击的时候,获取当前的进度值,绘制的进度可能是在这一小段进度区间的起始位置,而实际鼠标点击的位置可能在这一小段偏后部分,看起来就是当前显示的进度没有跟随鼠标的点击位置。
这边使用的容错方式是监听onTouchEvent,获取当前鼠标的点击位置来校正进度绘制的参数值。
二、具体代码实现
这边的控件实现是继承AppCompatSeekBar的方式去实现的,你可以继承其他的进度条控件,因为这边主要是自定义进度条的样式绘制,重写了onDraw方法,不影响继承的进度条控件原有的接口逻辑。
在集成使用的时候,在资源文件里面自己加下prismatic_seekbar_thumb_min_width(滑块最小宽度)、prismatic_seekbar_background(默认进度条背景色)、prismatic_seekbar_progress(默认进度条颜色)的参数值。另外也自己定义下PrismaticSeekBar的style样式,加下seekBarType的参数类型值,现在实现的提供normal和colorful两种自定义样式。normal为0,colorful为1。
在XML布局中配置的时候,需要指定下width和height的具体大小,不能设置为wrap_content,因为这边没有实现控件宽高的最小值显示,可以自己优化下处理逻辑。
1. Java实现
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class PrismaticSeekBar extends androidx.appcompat.widget.AppCompatSeekBar {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_COLORFUL = 1;
private Paint mPaint;
private Path mBackgroundPath = null;
private Path mProgressPath = null;
private Path mThumbPath = null;
private LinearGradient mProgressGradient;
private LinearGradient mThumbGradientCache;
private float mAreaOffsetX = 0, mAreaOffsetHalfX = 0;
private final int THUMB_WIDTH = getContext().getResources().getDimensionPixelSize(R.dimen.prismatic_seekbar_thumb_min_width);
private int COLOR_BACKGROUND_NORMAL = getContext().getResources().getColor(R.color.prismatic_seekbar_background);
private int COLOR_PROGRESS_NORMAL = getContext().getResources().getColor(R.color.prismatic_seekbar_progress);
private int mSeekBarType = 0;
private float mCurrentProgressPos = 0f;
private float mTouchPos = 0f;
private boolean mIsClick = false;
private boolean mIsTouch = false;
private float mDownX = 0;
private float mDownY = 0;
private float mTempX = 0;
private float mTempY = 0;
private static final int MAX_DISTANCE_FOR_CLICK = 5;
public PrismaticSeekBar(Context context) {
this(context, null);
}
public PrismaticSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public PrismaticSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initPath();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mOnTouchListener.onTouch(this, event);
return super.onTouchEvent(event);
}
private OnTouchListener mOnTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsTouch = true;
mDownX = event.getX();
mDownY = event.getY();
trackTouchEvent(event, true);
break;
case MotionEvent.ACTION_MOVE:
mIsTouch = true;
mTempX = event.getX();
mTempY = event.getY();
trackTouchEvent(event, true);
break;
case MotionEvent.ACTION_UP:
mTempX = (int) event.getX();
mTempY = (int) event.getY();
if (Math.abs(mTempX - mDownX) < MAX_DISTANCE_FOR_CLICK
&& Math.abs(mTempY - mDownY) < MAX_DISTANCE_FOR_CLICK) {
onClick(event);
}
trackTouchEvent(event, false);
mIsTouch = false;
break;
case MotionEvent.ACTION_CANCEL:
mIsTouch = false;
trackTouchEvent(event, false);
break;
}
return true;
}
};
private void onClick(MotionEvent event) {
mIsClick = true;
mTouchPos= event.getX();
invalidate();
}
private void trackTouchEvent(MotionEvent event, boolean mIsTouch) {
if (!mIsTouch) {
mCurrentProgressPos = mTouchPos;
}
mTouchPos = event.getX();
invalidate();
}
private void initPath() {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int halfHeight = height / 2;
mAreaOffsetX = (int) (height / Math.sqrt(3));
mAreaOffsetHalfX = (int) (height / (2 * Math.sqrt(3)));
mBackgroundPath = new Path();
mBackgroundPath.moveTo(0, height);
mBackgroundPath.lineTo(mAreaOffsetHalfX, halfHeight);
mBackgroundPath.lineTo(width - mAreaOffsetHalfX, halfHeight);
mBackgroundPath.lineTo(width - mAreaOffsetX, height);
mBackgroundPath.close();
// 设置绘制为填充效果
mBackgroundPath.setFillType(Path.FillType.WINDING);
mProgressPath = new Path();
mThumbPath = new Path();
mCurrentProgressPos = getMeasuredWidth() * getProgress() / getMax();
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PrismaticSeekBar);
if (a != null) {
try {
mSeekBarType = a.getInt(R.styleable.PrismaticSeekBar_seekBarType, 0);
} catch (UnsupportedOperationException | Resources.NotFoundException e) {
e.printStackTrace();
} finally {
a.recycle();
}
}
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@Override
public synchronized void setProgress(int progress) {
super.setProgress(progress);
mCurrentProgressPos = getMeasuredWidth() * progress / getMax();
if (mIsClick) {
mIsClick = false;
mCurrentProgressPos = mTouchPos;
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw background
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setColor(COLOR_BACKGROUND_NORMAL);
canvas.drawPath(mBackgroundPath, mPaint);
// Draw progress
float progressRight = 0f;
if (mIsTouch) {
progressRight = mTouchPos;
mCurrentProgressPos = mTouchPos;
} else {
progressRight = mCurrentProgressPos;
}
drawProgress(canvas, progressRight, getMeasuredHeight(), mPaint);
// Draw thumb
int position = getProgress() * 50 / getMax();
float progressRatio = getProgress() / (float) getMax();
int gradientRight = (int) (getMeasuredWidth() * progressRatio);
drawThumb(canvas, progressRight, getMeasuredHeight(), mPaint, position, gradientRight);
}
private void drawProgress(Canvas canvas, float currentPosition, int height, Paint paint) {
if (currentPosition > mAreaOffsetX) {
paint.reset();
paint.setAntiAlias(true);
if (mSeekBarType == TYPE_NORMAL) {
paint.setColor(COLOR_PROGRESS_NORMAL);
} else if (mSeekBarType == TYPE_COLORFUL) {
if (mProgressGradient == null) {
mProgressGradient = new LinearGradient(0, 0, getWidth(), 0,
new int[]{0xFF67A7F6, 0xFFFEAF6F}, null, Shader.TileMode.CLAMP);
}
paint.setShader(mProgressGradient);
}
mProgressPath.reset();
float rightPosX = currentPosition > getMeasuredWidth() ? getMeasuredWidth() : currentPosition;
float leftPosX = (rightPosX - mAreaOffsetX) > 0 ? (rightPosX - mAreaOffsetX) : 0;
mProgressPath.moveTo(0, height);
mProgressPath.lineTo(mAreaOffsetX, 0);
mProgressPath.lineTo(rightPosX, 0);
mProgressPath.lineTo(leftPosX, height);
mProgressPath.close();
canvas.drawPath(mProgressPath, paint);
}
}
/**
* 使用画布translate的方式移动之后绘制渐变色滑块
* 使用PNG图片存在渐变色中间部分有明显数显分隔显示的问题,不能用资源图片
*
* @param canvas
* @param currentPosition
* @param height
* @param paint
* @param position
*/
private void drawThumb(Canvas canvas, float currentPosition, int height, Paint paint, int position, int gradientRight) {
if (currentPosition > mAreaOffsetX) {
paint.reset();
paint.setAntiAlias(true);
if (mThumbGradientCache == null) {
mThumbGradientCache = new LinearGradient(0, 0, THUMB_WIDTH, 0,
new int[]{0x00FFFFFF, 0xFFFFFFFF}, null, Shader.TileMode.CLAMP);
}
paint.setShader(mThumbGradientCache);
mThumbPath.reset();
float rightPosX = currentPosition > getMeasuredWidth() ? getMeasuredWidth() : currentPosition;
if (rightPosX > THUMB_WIDTH) {
canvas.save();
canvas.translate(rightPosX - THUMB_WIDTH, 0);
mThumbPath.moveTo(THUMB_WIDTH, 0);
mThumbPath.lineTo(THUMB_WIDTH - mAreaOffsetX, height);
mThumbPath.lineTo(0, height);
mThumbPath.lineTo(mAreaOffsetX, 0);
mThumbPath.close();
canvas.drawPath(mThumbPath, paint);
canvas.restore();
} else {
mThumbPath.moveTo(rightPosX, 0);
mThumbPath.lineTo(rightPosX - mAreaOffsetX, height);
mThumbPath.lineTo(0, height);
mThumbPath.lineTo(mAreaOffsetX, 0);
mThumbPath.close();
canvas.drawPath(mThumbPath, paint);
}
}
}
}
2. Kotlin实现
import android.content.Context
import android.content.res.Resources
import android.content.res.TypedArray
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatSeekBar
class PrismaticSeekBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1
) : AppCompatSeekBar(context, attrs, defStyleAttr) {
companion object {
const val TYPE_NORMAL = 0
const val TYPE_COLORFUL = 1
const val MAX_DISTANCE_FOR_CLICK = 5
}
private val mPaint: Paint = Paint().apply { isAntiAlias = true }
private var mBackgroundPath: Path? = null
private var mProgressPath: Path? = null
private var mThumbPath: Path? = null
private var mProgressGradient: LinearGradient? = null
private var mThumbGradientCache: LinearGradient? = null
private var mAreaOffsetX = 0f
private var mAreaOffsetHalfX = 0f
private val THUMB_WIDTH = context.resources.getDimensionPixelSize(R.dimen.prismatic_seekbar_thumb_min_width)
private val COLOR_BACKGROUND_NORMAL = context.resources.getColor(R.color.prismatic_seekbar_background)
private val COLOR_PROGRESS_NORMAL = context.resources.getColor(R.color.prismatic_seekbar_progress)
private var mSeekBarType = 0
private var mCurrentProgressPos = 0f
private var mTouchPos = 0f
private var mIsClick = false
private var mIsTouch = false
private var mDownX = 0f
private var mDownY = 0f
private var mTempX = 0f
private var mTempY = 0f
init {
init(context, attrs)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
initPath()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
mOnTouchListener.onTouch(this, event)
return super.onTouchEvent(event)
}
private val mOnTouchListener = OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mIsTouch = true
mDownX = event.x
mDownY = event.y
trackTouchEvent(event, true)
}
MotionEvent.ACTION_MOVE -> {
mIsTouch = true
mTempX = event.x
mTempY = event.y
trackTouchEvent(event, true)
}
MotionEvent.ACTION_UP -> {
mTempX = event.x
mTempY = event.y
if (Math.abs(mTempX - mDownX) < MAX_DISTANCE_FOR_CLICK &&
Math.abs(mTempY - mDownY) < MAX_DISTANCE_FOR_CLICK) {
onClick(event)
}
trackTouchEvent(event, false)
mIsTouch = false
}
MotionEvent.ACTION_CANCEL -> {
mIsTouch = false
trackTouchEvent(event, false)
}
}
true
}
private fun onClick(event: MotionEvent) {
mIsClick = true
mTouchPos = event.x
invalidate()
}
private fun trackTouchEvent(event: MotionEvent, mIsTouch: Boolean) {
if (!mIsTouch) {
mCurrentProgressPos = mTouchPos
}
mTouchPos = event.x
invalidate()
}
private fun initPath() {
val width = measuredWidth
val height = measuredHeight
val halfHeight = height / 2
mAreaOffsetX = (height / Math.sqrt(3.0)).toFloat()
mAreaOffsetHalfX = (height / (2 * Math.sqrt(3.0))).toFloat()
mBackgroundPath = Path().apply {
moveTo(0f, height.toFloat())
lineTo(mAreaOffsetHalfX, halfHeight.toFloat())
lineTo((width - mAreaOffsetHalfX), halfHeight.toFloat())
lineTo((width - mAreaOffsetX), height.toFloat())
close()
fillType = Path.FillType.WINDING
}
mProgressPath = Path()
mThumbPath = Path()
mCurrentProgressPos = measuredWidth * progress / max.toFloat()
}
private fun init(context: Context, attrs: AttributeSet?) {
val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.PrismaticSeekBar)
a?.run {
try {
mSeekBarType = getInt(R.styleable.PrismaticSeekBar_seekBarType, 0)
} catch (e: UnsupportedOperationException) {
e.printStackTrace()
} catch (e: Resources.NotFoundException) {
e.printStackTrace()
} finally {
recycle()
}
}
}
override fun setProgress(progress: Int) {
super.setProgress(progress)
mCurrentProgressPos = measuredWidth * progress / max.toFloat()
if (mIsClick) {
mIsClick = false
mCurrentProgressPos = mTouchPos
}
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Draw background
mPaint.reset()
mPaint.isAntiAlias = true
mPaint.color = COLOR_BACKGROUND_NORMAL
canvas.drawPath(mBackgroundPath!!, mPaint)
// Draw progress
val progressRight: Float = if (mIsTouch) {
mTouchPos
mCurrentProgressPos = mTouchPos
} else {
mCurrentProgressPos
}
drawProgress(canvas, progressRight, measuredHeight, mPaint)
// Draw thumb
val position = progress * 50 / max
val progressRatio = progress / max.toFloat()
val gradientRight = (measuredWidth * progressRatio).toInt()
drawThumb(canvas, progressRight, measuredHeight, mPaint, position, gradientRight)
}
private fun drawProgress(canvas: Canvas, currentPosition: Float, height: Int, paint: Paint) {
if (currentPosition > mAreaOffsetX) {
paint.reset()
paint.isAntiAlias = true
paint.color = if (mSeekBarType == TYPE_NORMAL) COLOR_PROGRESS_NORMAL else {
if (mProgressGradient == null) {
mProgressGradient = LinearGradient(0f, 0f, width.toFloat(), 0f,
intArrayOf(0xFF67A7F6.toInt(), 0xFFFEAF6F.toInt()), null, Shader.TileMode.CLAMP)
}
paint.shader = mProgressGradient
paint.color
}
mProgressPath!!.reset()
val rightPosX = if (currentPosition > measuredWidth) measuredWidth.toFloat() else currentPosition
val leftPosX = if ((rightPosX - mAreaOffsetX) > 0) (rightPosX - mAreaOffsetX) else 0f
mProgressPath!!.moveTo(0f, height.toFloat())
mProgressPath!!.lineTo(mAreaOffsetX, 0f)
mProgressPath!!.lineTo(rightPosX, 0f)
mProgressPath!!.lineTo(leftPosX, height.toFloat())
mProgressPath!!.close()
canvas.drawPath(mProgressPath!!, paint)
}
}
private fun drawThumb(canvas: Canvas, currentPosition: Float, height: Int, paint: Paint, position: Int, gradientRight: Int) {
if (currentPosition > mAreaOffsetX) {
paint.reset()
paint.isAntiAlias = true
if (mThumbGradientCache == null) {
mThumbGradientCache = LinearGradient(0f, 0f, THUMB_WIDTH.toFloat(), 0f,
intArrayOf(0x00FFFFFF, 0xFFFFFFFF.toInt()), null, Shader.TileMode.CLAMP)
}
paint.shader = mThumbGradientCache
mThumbPath!!.reset()
val rightPosX = if (currentPosition > measuredWidth) measuredWidth.toFloat() else currentPosition
if (rightPosX > THUMB_WIDTH) {
canvas.save()
canvas.translate(rightPosX - THUMB_WIDTH, 0f)
mThumbPath!!.moveTo(THUMB_WIDTH.toFloat(), 0f)
mThumbPath!!.lineTo(THUMB_WIDTH - mAreaOffsetX, height.toFloat())
mThumbPath!!.lineTo(0f, height.toFloat())
mThumbPath!!.lineTo(mAreaOffsetX, 0f)
mThumbPath!!.close()
canvas.drawPath(mThumbPath!!, paint)
canvas.restore()
} else {
mThumbPath!!.moveTo(rightPosX, 0f)
mThumbPath!!.lineTo(rightPosX - mAreaOffsetX, height.toFloat())
mThumbPath!!.lineTo(0f, height.toFloat())
mThumbPath!!.lineTo(mAreaOffsetX, 0f)
mThumbPath!!.close()
canvas.drawPath(mThumbPath!!, paint)
}
}
}
}
扩展阅读:
- Android 弧形 RecyclerView 实现(Kotlin)
- 美图手机音乐Widget动画实现
- Android 心率动画自定义控件实现
- Android 卡片旋转切换动效实现详解
- Android 残影数字动画实现详解
- Android 自定义菱形横向滑动指示器控件
微信公众号
转载请注明出处:陈文管的博客 – Android 自定义棱形样式进度条控件