Android自定义评View实例教程~自定义可拖拽评价进度条二「完全自定义」
实际效果,在上一节的基础上给控件下方加入了一个随进度移动的评价文字。
并且这个文字是有范围的变化,移动过程中也有透明度和大小的变化。
文字有4个等级,分别是体验很差,体验一般,体验还行,体验很好,对应的其实就是我们4个评价的等级。
并且当用户滑动到某个评价区域内离开手指后,要有一个回弹的效果,毕竟我们评价的等级个数是确定的嘛,又不是百分比。
当然我们也可以根据自己的需求修改评价文字的等级个数,比如修改为三个等级,【效果很差,效果还行,效果很好】等。
项目的源代码已经上传到Github仓库的AProgress文件夹下了,该章节是在上一章节的基础上进行开发的。
- 自定义可拖拽评价进度条一
- 自定义可拖拽评价进度条二
其中L.d()是日志封装工具类,等同于Log.d("HelloWord",);
进度条边距,高度的处理
虽然是在上一节的代码上继续写,不过为了区别,我把上一节的EasyProgress复制,改名为AEasyProgress继续开发。先声明几个必要属性。
//文本画笔
private Paint mTextPaint;
//文本的大小
private float mTextSize = ADensity.dip2px(14);
/**
* 评价文字集合,如体验很好,体验一般,体验不错,体验很好4个等级
* 之所以使用集合,不使用String数组是想让它保持可扩展性,我们后面课题提供一个接口,让用户自己决定要设置多少个评价等级
*/
private List evaluates = new ArrayList<>();
//文本的宽度
private int textWidth;
//文本的高度
private int textHeight;
//文本的透明度
private float textAlpha;
//文本缩放后的最小的大小
private float textSizeRadio = 0.8f;
//一段评价区间的大小
private float distance;
//评价区间一半的大小
private float half = distance / 2;
初始化画笔
//初始化文本画笔
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setStrokeWidth(3);
mTextPaint.setColor(Color.parseColor("#9a9a9a"));
mTextPaint.setTextSize(mTextSize);
接着我们原本应该是要对进度条边距进行处理了,还记得上一节对圆形进度指示器的处理吗?
其实这里文字也是一样,因为它也是永远将文字中点对准当前进度的,所以要进行边距处理
但在处理边距之前,我们先弄清一个概念吧,就是一个评价所属的范围,也就是distance区间。
这个区间是评价到达的点前面或者后面吗?No,我们应该是使得它在左右两侧的特定范围都有一个回弹的效果,大概是这样的效果
我特意在中间绘制了一条线,只要圆点指示器在这条线左右的一定范围类滑动,就都属于效果还行的“地盘”。
当你在这个区域滑动,然后松开手指的时候,就会回弹到中间效果还行的坐标位置
那这个范围是多少呢,还是看图
中间的框框就是一个评价的区域,而在最左边和最右边的评价等级其实也拥有一块完整,大小相同的区域,只是因为他们是在屏幕的最边缘,所以有一半我们是触碰不到的(超过进度条范围也是触摸不到的)
所以实际上上面整个进度条,拥有的文字区间个数是0.5+1+0.5,等于两个,而我们的评价集合里面有三个元素
所以到这里就可以推导一个文字区间的大小了,等于(进度条的实际长度)/集合长度-1。
这个时候我们再去onLayout方法里面对文字的对边距进行处理,然后顺便把我们上面讲的文字区间的大小也初始化好
//初始化几个距离参数
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
width = getWidth();//view的宽度
height = getHeight();//view的高度
//让左边距至少为半个圆点指示器的距离
paddingLeft = getPaddingLeft();//距离左边的距离
if (getPaddingLeft() < mCircleRadius) {
L.d("onLayout");
paddingLeft = mCircleRadius;
}
//让右边距至少为半个圆点指示器的距离
paddingRight = getPaddingRight();//距离右边的距离
if (getPaddingRight() < mCircleRadius) {
paddingRight = mCircleRadius;
}
//暂时在这里给列表设置三个数据
evaluates.add("效果很差");
evaluates.add("效果还行");
evaluates.add("效果很棒");
//定义一个标准的文本宽度
textWidth = (int) mTextPaint.measureText("效果很差");
//让左边距至少为文字宽度的一半
if (paddingLeft < textWidth / 2) {
paddingLeft = textWidth / 2;
}
//让右边距至少为文字宽度的一半
if (paddingRight < textWidth / 2) {
paddingRight = textWidth / 2;
}
//如果当前进度小于左边距
setCurrentProgress();
//最大进度长度等于View的宽度-(左边的内边距+右边的内边距)
maxProgress = width - paddingLeft - paddingRight;
//规定评价进度条至少要有两个元素值
if (evaluates.size() < 2) {
Toast.makeText(App.getContext(), "数据设置出错", Toast.LENGTH_SHORT).show();
} else {
//一个文字区间的宽度
distance = maxProgress / (evaluates.size() - 1);
}
//半个文字区间的宽度
half = distance / 2;
}
同样,因为高度增加了文字的高度,也要重新处理
//重新计算控件的宽,高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
//获取文本高度
textHeight = (int) (mTextPaint.descent() - mTextPaint.ascent());
/**
*进度条下部分的高度(圆点指示器圆点一下)应该为为圆点指示器半径*1.8+文本高度,
*因为进度条永远在中心,所以上面高度也要一样,*2
*/
int minHeight = (int) ((mCircleRadius * 1.8 + textHeight) * 2);
int height = resolveSize(minHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
之所以文字高度在这里处理是因为onMeasure的生命周期的执行存续在onLayout上面,依次为, onMeasure() -> onSizeCahnged() -> onLayout() ->onDraw();
接着到我们的onDraw方法测试一下吧,这里我们暂时不对评价等级做处理,先看看文字绘制是否成功,Activity和xml布局的代码(除了进度条控件换成这个进度条)和上一节一模一样就可以了
/**
* onDraw方法里面
* 绘制文字
*/
canvas.drawText(evaluates.get(0), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
就是这样的效果
文字随着我们的进度的改变而改变,我们下一步要做的就是给他添加范围区域等级的变化和回弹效果。
这一小段的完整代码
/**
* Created by 舍长 on 2019/1/10
* describe:一个简单的进度条,下方带评价文字
*/
public class AEasyProgress extends View {
//灰色背景线段的画笔
private Paint bgPaint;
//实际进度绿色线段的画笔
private Paint progressPaint;
//圆点指示器的画笔
private Paint circlePaint;
//圆点指示器的半径
private int mCircleRadius = ADensity.dip2px(12);
//进度条的最大宽度
private float maxProgress;
//进度条当前的宽度
private float currentProgress;
//当前View的宽度
private int width;
//当前View的高度
private int height;
//距离左边的内边距
private int paddingLeft;
//距离右边的内边距
private int paddingRight;
//文本画笔
private Paint mTextPaint;
//文本的大小
private float mTextSize = ADensity.dip2px(14);
/**
* 评价文字集合,如体验很好,体验一般,体验不错,体验很好4个等级
* 之所以使用集合,不使用String数组是想让它保持可扩展性,我们后面课题提供一个接口,
* 让用户自己决定要设置多少个评价等级
*/
private List evaluates = new ArrayList<>();
//文本的宽度
private int textWidth;
//文本的高度
private int textHeight;
//文本的透明度
private float textAlpha;
//文本缩放后的最小的大小
private float textSizeRadio = 0.8f;
//一段评价区间的大小
private float distance;
//一半评价区间的大小
private float half = distance / 2;
public AEasyProgress(Context context) {
super(context);
}
public AEasyProgress(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AEasyProgress(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();//初始化画笔
}
/**
* 初始化画笔
*/
private void initPaint() {
//进度条背景画笔
bgPaint = new Paint();
bgPaint.setColor(Color.parseColor("#F0F0F0"));//灰色
bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);//填充且描边
bgPaint.setAntiAlias(true);//抗锯齿
bgPaint.setStrokeCap(Paint.Cap.ROUND);//线冒的头是圆的
bgPaint.setStrokeWidth(ADensity.dip2px(3));//大小为3dp转px
//设置进度画笔
progressPaint = new Paint();
progressPaint.setColor(Color.parseColor("#0DE6C2"));//绿色
progressPaint.setStyle(Paint.Style.FILL_AND_STROKE);//填充且描边
progressPaint.setAntiAlias(true);//抗锯齿
progressPaint.setStrokeCap(Paint.Cap.ROUND);//线冒的头圆原的
progressPaint.setStrokeWidth(ADensity.dip2px(3));//大小为3dp转px
//圆点指示器
circlePaint = new Paint();
circlePaint.setAntiAlias(true);//设置抗锯齿
circlePaint.setColor(Color.parseColor("#fafafa"));//颜色
circlePaint.setShadowLayer(ADensity.dip2px(2), 0, 0,
Color.parseColor("#38000000"));//外阴影颜色
circlePaint.setStyle(Paint.Style.FILL);//填充
//初始化文本画笔
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setStrokeWidth(3);
mTextPaint.setColor(Color.parseColor("#9a9a9a"));
mTextPaint.setTextSize(mTextSize);
}
//重新计算控件的宽,高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
//获取文本高度
textHeight = (int) (mTextPaint.descent() - mTextPaint.ascent());
//最小部分为你那个相等的部分,下部分为圆点指示器半径*1.8+文本高度,因为进度条永远在中心,所以上面高度也要一样,*2
int minHeight = (int) ((mCircleRadius * 1.8 + textHeight) * 2);
int height = resolveSize(minHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
//返回高度值,作用和resolveSize方法一样
private int measureHeight(int heightMeasureSpec) {
int result;
int mode = MeasureSpec.getMode(heightMeasureSpec);//获取高度类型
int size = MeasureSpec.getSize(heightMeasureSpec);//获取高度数值
//制定的最小高度标准
int minHeight = mCircleRadius * 2 + (ADensity.dip2px(2) * 2);
//如果用户设定了指定大小
if (mode == MeasureSpec.EXACTLY) {
/**
* 虽然用户已经指定了大小,但是万一指定的大小小于圆点指示器的高度,
* 还是会出现显示不全的情况,所以还要进行判断
*/
L.d("EXACTLY");
if (size < minHeight) {
result = minHeight;
} else {
result = size;
}
}
//如果用户没有设定明确的值
else {
//设定高度为圆点指示器的直径
result = minHeight;
}
return result;
}
//初始化几个距离参数
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
width = getWidth();//view的宽度
height = getHeight();//view的高度
//让左边距至少为半个圆点指示器的距离
paddingLeft = getPaddingLeft();//距离左边的距离
if (getPaddingLeft() < mCircleRadius) {
L.d("onLayout");
paddingLeft = mCircleRadius;
}
//让右边距至少为半个圆点指示器的距离
paddingRight = getPaddingRight();//距离右边的距离
if (getPaddingRight() < mCircleRadius) {
paddingRight = mCircleRadius;
}
//暂时在这里给列表设置三个数据
evaluates.add("效果很差");
evaluates.add("效果还行");
evaluates.add("效果很棒");
//定义一个标准的文本宽度
textWidth = (int) mTextPaint.measureText("效果很差");
//让左边距至少为文字宽度的一半
if (paddingLeft < textWidth / 2) {
paddingLeft = textWidth / 2;
}
//让右边距至少为文字宽度的一半
if (paddingRight < textWidth / 2) {
paddingRight = textWidth / 2;
}
//如果当前进度小于左边距
setCurrentProgress();
//最大进度长度等于View的宽度-(左边的内边距+右边的内边距)
maxProgress = width - paddingLeft - paddingRight;
//规定评价进度条至少要有两个元素值
if (evaluates.size() < 2) {
Toast.makeText(App.getContext(), "数据设置出错", Toast.LENGTH_SHORT).show();
} else {
//一个文字区间的宽度
distance = maxProgress / (evaluates.size() - 1);
}
//半个文字区间的宽度
half = distance / 2;
}
//绘制控件
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 绘制背景线段
*/
canvas.drawLine(paddingLeft, height / 2, width - paddingRight, height / 2, bgPaint);
/**
* 绘制实际进度线段
*/
canvas.drawLine(paddingLeft, height / 2, currentProgress, height / 2, progressPaint);
//要支持阴影下过必须关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);//发光效果不支持硬件加速
/**
* 绘制圆点指示器
*/
canvas.drawCircle(currentProgress, getHeight() / 2, mCircleRadius, circlePaint);
/**
* 绘制文字
*/
canvas.drawText(evaluates.get(0), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
// canvas.drawText(evaluates.get(0), currentProgress, mCircleRadius * 2 + (ADensity.dip2px(2) * 2), mTextPaint);
}
//触摸
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按住
case MotionEvent.ACTION_DOWN:
//设置进度值
setMotionProgress(event);
return true;
//移动
case MotionEvent.ACTION_MOVE:
//获取当前触摸点,赋值给当前进度
setMotionProgress(event);
return true;
}
return super.onTouchEvent(event);
}
//设置进度值
private void setMotionProgress(MotionEvent event) {
//获取当前触摸点,赋值给当前进度
currentProgress = (int) event.getX();
//如果当前进度小于左边距
setCurrentProgress();
//看数学公式就可以了,实际百分比进度数值
float result = ((currentProgress - paddingLeft) * 100) / maxProgress;
//进行空值判断
if (onProgressListener != null) {
onProgressListener.onSelect((int) result);
}
invalidate();
}
//设置当前进度条进度,从1到100
public void setProgress(int progress) {
if (progress > 100 || progress < 0) {
Toast.makeText(App.getContext(), "输入的进度值不符合规范", Toast.LENGTH_SHORT).show();
}
setCurrentProgress();
//设置当前进度的宽度
currentProgress = ((progress * maxProgress) / 100) + paddingLeft;
onProgressListener.onSelect(progress);
invalidate();
}
private void setCurrentProgress() {
if (currentProgress < paddingLeft) {
currentProgress = paddingLeft;
}
//如果当前进度大于宽度-右边距
else if (currentProgress > width - paddingRight) {
currentProgress = width - paddingRight;
}
}
//当前选中进度的回调
private OnProgressListener onProgressListener;
public interface OnProgressListener {
void onSelect(int progress);
}
public void setOnProgressListener(OnProgressListener onProgressListener) {
this.onProgressListener = onProgressListener;
}
}
范围的计算和抬手回弹效果
之前我们已经安排了一个评价等级的范围,那时候是用一整块文字区间来讲解,我们接下来看看这张图
这张图显示的,是半个文字区间,半个文字区间的范围。想想也是,因为我们是要对等级的前后都进行判断,所以用一半一半,这样的来判断是最方便的,用代码来表示就是这样
所以我们只要判断当前的进度位置在哪个等级的范围,我们就将文本绘制出来
int progress = (int) (currentProgress - paddingLeft);
if (progress < half) {
canvas.drawText(evaluates.get(0), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
//第二段范围
if ((progress > half) && (progress <= half * 3)) {
canvas.drawText(evaluates.get(1), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
//第三段范围,这里写4也可以,但其实5才是完整的一块文字区间,另外半个在屏幕外
if ((progress > half * 3) && (progress <= half * 5)) {
canvas.drawText(evaluates.get(2), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
我们只要对着上面的图,去数格子就行了。到这里如果运行程序其实就已经完成了,这样的方式无疑是最容易理解的,但是可称不上最优解
因为维护起来其实不方便,上面我们说过我们要给评价等级设置集合,如果等级有4,5,6,7,8个呢?我们就需要照着写8个if,不大方便。所以我们得想办法,观看这三个if,其实可以找到某种规律的。
如果说progress的距离要在x
所以我们可以将代码推导成这样,其中distance为一整块文字区间,half为一半的文字权健
//第一段范围
int progress = (int) (currentProgress - paddingLeft);
for (int i = 0; i < evaluates.size(); i++) {
if ((progress > ((i * distance) - half)) && (progress<=(half+(i*distance)))) {
canvas.drawText(evaluates.get(i), currentProgress, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
}
这样我们就实现了和上面一模一样的功能,而且它是可扩展的,不管你添加多少个等级都可以。
接下来我们还使用这条公式,给进度条添加一个回弹的效果,它会在我们的触摸方法里面进行处理。
不过和前面的章节不同的是,我们不能让进度条按到哪里就跳到哪里了,而是只能靠拖动老移动距离。
因为如果我们还是让它按到哪里就跳到哪里,再加上我们还有回弹的效果,整个控件就会跳来跳去,就会让人看了很迷茫,体验很糟糕。
//开始x
float startX;
//结束x
float endX;
//当前的进度百分比
float result;
//触摸
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按住
case MotionEvent.ACTION_DOWN:
// //设置进度值
// setMotionProgress(event);
startX = event.getX();
return true;
//移动
case MotionEvent.ACTION_MOVE:
endX = event.getX();
float dx = endX - startX;
startX = endX;
currentProgress = currentProgress + dx;
//如果当前进度小于左边距
setCurrentProgress();
//看数学公式就可以了,实际百分比进度数值
result = ((currentProgress - paddingLeft) * 100) / maxProgress;
//进行空值判断
if (onProgressListener != null) {
onProgressListener.onSelect((int) result);
}
invalidate();
return true;
//抬起回弹效果
case MotionEvent.ACTION_UP:
int progress = (int) (currentProgress - paddingLeft);
for (int i = 0; i < evaluates.size(); i++) {
if ((progress > ((i * distance) - half)) && (progress <= (half + (i * distance)))) {
currentProgress = distance * i + paddingLeft;
}
}
//如果当前进度小于左边距
setCurrentProgress();
//看数学公式就可以了,实际百分比进度数值
result = ((currentProgress - paddingLeft) * 100) / maxProgress;
if (result == 100) {
L.d("curr:" + currentProgress);
}
//进行空值判断
if (onProgressListener != null) {
onProgressListener.onSelect((int) result);
}
break;
}
return true;
}
private void setCurrentProgress() {
if (currentProgress < paddingLeft) {
currentProgress = paddingLeft;
}
//如果当前进度大于宽度-右边距
else if (currentProgress > width - paddingRight) {
currentProgress = width - paddingRight;
}
}
上面就是我们修改后的触摸方法啦,到这里我们就完成这一小段了
设置颜色的透明度和大小变化
文字透明度的动画我们通过一个工具类来实现AColor
/**
* Created by 舍长 on 2019/1/14
* describe:修改颜色透明度工具类
*/
public class AColor {
/**
* 修改颜色透明度
* @param color
* @param alpha
* @return
*/
public static int changeAlpha(int color, int alpha) {
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
return Color.argb(alpha, red, green, blue);
}
}
传进来的alpha来控制透明度,255是完全不透明,依次减小,直到0就变成完全透明的状态
我们前面声明了两个属性
//文本的透明度
private int textAlpha;
//文本缩放后的最小的大小
private float textSizeRadio = 0.8f;
textAlpha用来改变透明度,textSizeRadio表示文本最小是多小,缩放后的文本最小也不会小于这个大小。
我们需要根据文字区域的前后来判断透明度是增加还是减少,在前面的区域,是透明度逐渐增加,后面的区域,是透明度逐渐减小,我们修改onDraw方法
int progress = (int) (currentProgress - paddingLeft);
for (int i = 0; i < evaluates.size(); i++) {
if ((progress > ((i * distance) - half)) && (progress <= (half + (i * distance)))) {
//前半段
if ((progress - (i * distance)) < 0) {
L.d("前半段");
float radio = -(progress - (i * distance)) / half;
textAlpha = 1 - radio;
mTextSize = ADensity.dip2px(14);
float size = (mTextSize - mTextSize * textSizeRadio) * (radio);
mTextSize = mTextSize - size;
mTextPaint.setColor(AColor.changeAlpha(Color.parseColor("#9a9a9a"), (int) (textAlpha * 0xff)));
canvas.drawText(evaluates.get(i), progress + paddingLeft, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
//后半段
else {
L.d("后半段");
float radio = (progress - (i * distance)) / half;
//textAlpha之间变小,radio逐渐变大
textAlpha = 1 - radio;
//要减去的范围,当textAlpha等于1时,文本最小
float size = (mTextSize - mTextSize * textSizeRadio) * (radio);
mTextSize = mTextSize - size;
mTextPaint.setColor(AColor.changeAlpha(Color.parseColor("#9a9a9a"), (int) (textAlpha * 0xff)));
canvas.drawText(evaluates.get(i), progress + paddingLeft, (float) (height / 2 + mCircleRadius * 2.8), mTextPaint);
}
}
}
推导过程确实是复杂,只能是慢慢推导,先推导出前半段和后半段的区间,然后再推导文本大小,最后
是透明度。接着将我们原本写在onLayout的集合数据去掉,提供一个方法来初始化集合。
//设置评价字符串集合
public void setEvaluates(String[] strings) {
evaluates.clear();
evaluates.addAll(Arrays.asList(strings));
//每一段文字区间的大小
//当集合的长度为0,没有数据时
if (evaluates.size() < 2) {
Toast.makeText(App.getContext(), "少需要两个评价等级", Toast.LENGTH_SHORT).show();
}
//当有多个数据时,区间距离就是整个进度条除以集合个数-1
else {
distance = maxProgress / (evaluates.size() - 1);
}
最后在Actiivty中调用
easyProgress.setEvaluates(new String[]{"效果很差", "效果还行","效果很棒"});
这一段的代码就不贴了,因为已经和源代码一模一样了,需要的可以去源代码查看AEasyProgress.java文件,和AEasyActiivty
句子控
等世界温柔,不如自己动手