1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 自定义View从实现到原理(七)- 类似迅雷实现水波纹波浪加载效果

自定义View从实现到原理(七)- 类似迅雷实现水波纹波浪加载效果

时间:2019-03-01 07:01:40

相关推荐

自定义View从实现到原理(七)- 类似迅雷实现水波纹波浪加载效果

自定义View从实现到原理(七)

已经到这一步了啊,这一篇写完基本上自定义View就不会写了,以后有可能的话,也许会写一下自定义ViewGroup或者是自定义View的仿真书籍翻页效果,不过那也是以后的事情了,今天就来实现以下水波纹加载效果,先看一下效果图:

类似这种的效果,其实也就是一个自定义的View,接下来我们来一步步实现一下:

定义属性

首先还是一样,根据效果图,先定义这个View的属性,这个效果我觉得需要圆形的背景颜色,圆形的半径,显示的进度,显示文字的大小,显示文字的颜色,定义属性的代码:

<?xml version="1.0" encoding="utf-8"?><resources><declare-styleable name="WaterView"><!-- 背景颜色,圆的半径,进度,进度字号,进度颜色 --><attr name="backgroundColor" format="color" /><attr name="radius" format="dimension" /><attr name="text" format="string" /><attr name="textSize" format="dimension" /><attr name="textColor" format="color" /></declare-styleable></resources>

按照正常步骤,下一步在自定义的View中的构造函数获取属性:

private int backgroundColor, textColor;private float radius, textSize;private String text;private Paint backgroundPaint, textPaint;public WaterView(Context context) {this(context, null);}public WaterView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);TypedArray waterTypeArray = context.obtainStyledAttributes(attrs, R.styleable.WaterView);backgroundColor = waterTypeArray.getColor(R.styleable.WaterView_backgroundColor, Color.BLACK);textColor = waterTypeArray.getColor(R.styleable.WaterView_textColor, Color.WHITE);radius = waterTypeArray.getDimension(R.styleable.WaterView_radius, 260f);textSize = waterTypeArray.getDimension(R.styleable.WaterView_textSize, 24f);text = waterTypeArray.getString(R.styleable.WaterView_text);//记得回收waterTypeArray.recycle();}

绘制进度文字

首先我们要初始化画笔,在自定义View中我们通过画笔可以绘制我们想要的所有效果:

/*** 初始化画笔*/private void initPaint() {//初始化背景画笔backgroundPaint = new Paint();backgroundPaint.setColor(backgroundColor);//抗锯齿backgroundPaint.setAntiAlias(true);//初始化显示文字画笔textPaint = new Paint();textPaint.setTextSize(textSize);textPaint.setColor(textColor);textPaint.setAntiAlias(true);//字体为粗体textPaint.setFakeBoldText(true);}

定义完画笔之后,我们首先绘制出底层的圆形,有圆形才能在圆形中心绘制文字:

canvas.drawCircle(getWidth()/2, getHeight()/2, radius, backgroundPaint);

别忘了在构造函数中调用initPaint()方法,要不然会有空指针异常的错误抛出的。

写到这里我们想看一下能不能画出圆形,所以在xml中简单引用一下:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".Activity.WaterActivity"><com.example.day1.View.WaterViewandroid:layout_width="260dp"android:layout_height="260dp"android:layout_centerInParent="true"/></RelativeLayout>

效果图如下:

可真是朴实无华的一个圆形,既然能画出来,那我们之前的工作就没错,在中心显示一下文字:

canvas.drawText(text, getWidth() / 2, getHeight() / 2, textPaint);

我第一次是这么写的,感觉很正常,在Width的中间与Height的中间绘制出显示的文字,结果运行出来出了问题:

震惊,什么鬼?我仔细看了一下这个文字的位置,发现它的左下角,应该就是我们设置的中心位置,就离谱,遇到了问题就要解决,我就去搜了一下自定义View怎么将文字显示在中心,这里参考了这篇文章:

Android自定义View之文字居中

首先在initPaint()方法中设置居中,不过注意这只能做到水平居中:

textPaint.setTextAlign(Paint.Align.CENTER);

这一行代码就可以了,那么接下来我们处理竖直方向居中,按理来说上面这一行代码就足以解决了,但是为什么会有一点偏上呢,这就是因为在文字显示的时候,有一个基线,这个基线正是在我们设置的竖直居中位置,我们的文字在基线上部,因此就会比中心位置提高一点,对于这个问题有两种解决方法:

getTextBounds()方法

先来看一下这个方法的使用:

getTextBounds(String text, int start, int end, Rect bounds)

text是要显示的文本,start以及end是文本的开始显示位置与结束位置,bounds是存储文字显示位置的对象,最后的结果会写进bounds中。我们使用就是通过这个方法获得文字的边框bounds,由于基线在文字的下方,因此我们想要竖直居中的话就得向下平移文字高度的一半,就是 (bounds.top+bound.bottom)/2 这个值,但是要注意这个是一个负数,代表的是文字的高度一半的位置而不是真正意义的高度一半,getTextBounds()方法会有一个自己的坐标系:

可以看得出来在这个坐标系下,top以及bottom都是负值,因此我们向下平移需要减去之前算出的文字高度中心位置,那么具体的用法就是下面的代码:

Rect bounds = new Rect();textPaint.getTextBounds(text, 0, text.length(), bounds);float offset = (bounds.bottom + bounds.top) / 2;canvas.drawText(text, getWidth() / 2, (getHeight() / 2) - offset, textPaint);

可行,我们接下来看一下另外一种方法;

FontMetircs 方法

和之前的方法类似,我们看一下这个图,有了之前的经验,我们可以看出需要的两个数据为ascent以及descent这两个,不过要注意的是,这几个值,是固定不会变的,也就是无论你绘制的内容怎么改变,这几个值都不会变,类比上面的代码,我们来看一下这个:

Paint.FontMetrics fontMetrics= new Paint.FontMetrics();textPaint.getFontMetrics(fontMetrics);float offset = (fontMetrics.descent+fontMetrics.ascent)/2;canvas.drawText(text, getWidth() / 2, (getHeight() / 2) - offset , textPaint);

效果的话和上面是一样的,那么这两种方法,我们应该如何区别使用呢:

对于第一种,getTextBounds()方法,我们是获取到了文字的中间高度,那么随着内容的改变,我们的中间位置可能也会发生改变;

第二种,FontMetircs方法,这是固定的文字测量工具,不管内容如何改变,他的中间位置不会发生改变。

这样我们就可以总结出:

1.当我们绘制的内容会有动态改变操作时,使用FontMetircs()方法;

2.当绘制内容固定的时候,我们用哪个都可以,第一种看起来更加直观。

设置onMeasure

通常我们都会在xml布局中将控件设为wrapContent类型,按照我们之前写过的博客,我们也应该要重写onMeasure函数,这部分就不多说了,之前也介绍过:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);int width, height;if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {height = width = (int) radius * 2;setMeasuredDimension(width, height);} else {setMeasuredDimension(widthSpecSize, heightSpecSize);}}

简简单单的一串代码,朴实无华,效果也没什么变化,就这样,下一步。

显示文字模拟下载

下载自然是从0%到100%,我们开启线程进行模拟,在规定时间内跑完,并动态更新文字内容的显示:

private SingleTapThread singleTapThread;private GestureDetector detector;private int currentProgress = 0;private int maxProgress = 100;text = currentProgress + "%";

做好准备活动,接下来会开启线程模拟下载:

@SuppressLint("ClickableViewAccessibility")@Overridepublic boolean onTouchEvent(MotionEvent event) {if (event.getAction() == MotionEvent.ACTION_UP) {startProgressAnimation();}return super.onTouchEvent(event);}private void startProgressAnimation() {if (singleTapThread == null) {singleTapThread = new SingleTapThread();getHandler().postDelayed(singleTapThread, 100);}}private class SingleTapThread implements Runnable {@Overridepublic void run() {int maxProgress = 100;if (currentProgress < maxProgress) {invalidate();getHandler().postDelayed(singleTapThread, 100);currentProgress++;} else {getHandler().removeCallbacks(singleTapThread);}}}

我们从上到下看,首先设置了点击事件,只要有触碰抬起的事件发生,那么就启动startProgressAnimation()方法,在这个方法中我们首先检测了是否有线程在运行,如果没有的话开启线程,延迟100ms后启动SingleTapThread()线程,在这个线程中定义了最大值为100,如果当前值小于100就会刷新View并且100ms后再次重启,同时进度加一,如果已经完成则回收这个线程,效果图如下:

文字效果已经实现,那么接下来才是主要的部分,实现水波纹波浪效果:

实现水波纹波浪效果

简单来说,水波纹波浪效果,就是二阶贝塞尔曲线的一个应用,来看一下二阶贝塞尔曲线效果:

就是这样,我们这个效果可以看成是曲线在固定范围内的不断变换,我们这里就简要了解一下二阶贝塞尔曲线的应用就好了,如果需要深入在后面我再研究研究。

在Android SDK中提供了关于绘制贝塞尔曲线的方法:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2)

这四个参数都是相对值,我们来看一下上面的哪个动图,曲线起点是P0,终点P2,控制点P1,而相对的我们这四个参数的值,为:

1.dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,可为负值,正值表示相加,负值表示相减;

2.dy1:控制点Y坐标,相对上一个终点Y坐标的位移坐标。同样可为负值,正值表示相加,负值表示相减;

3.dx2:终点X坐标,同样是一个相对坐标,相对上一个终点X坐标的位移值,可为负值,正值表示相加,负值表示相减;

4.dy2:终点Y坐标,同样是一个相对,相对上一个终点Y坐标的位移值。可为负值,正值表示相加,负值表示相减;

这四个参数都是传递的都是相对值,相对上一个终点的位移值。

接下来正式绘制波浪特效:

首先初始化波浪的画笔:

//初始化波浪画笔progressPaint = new Paint();progressPaint.setAntiAlias(true);progressPaint.setColor(waterColor);//两层在一起我在上面progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

最后这行设置如果两层绘制在了同一个界面,这个绘制的会在上部显示,并且只显示交集,这是绘制圆形波浪的关键;

既然这个画布是有遮盖效果的,那么就应该设置在同一个bitmap上,我们定义一个bitmap,为了在这个bitmap上进行绘制,我们还要定义一个bitmapCanvas,来载入bitmap:

if (bitmap == null) {bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);bitmapCanvas = new Canvas(bitmap);}

这是初始化bitmap以及bitmapCanvas部分,定义部分自己在开始写一下吧,既然已经有了bitmapCanvas,我们就来把之前的画圆以及绘制文字都载入一下:

super.onDraw(canvas);width = getWidth();height = getHeight();if (bitmap == null) {bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);bitmapCanvas = new Canvas(bitmap);}bitmapCanvas.save();//移动坐标系bitmapCanvas.translate(0, height / 4);//绘制圆bitmapCanvas.drawCircle(radius, radius, radius, backgroundPaint);//绘制PATH//重置绘制路线path.reset();float percent = currentProgress * 1.0f / maxProgress;float y = (1 - percent) * radius * 2;//起点移动到右下path.moveTo(width, y);//移动到右下方path.lineTo(width, height);//移动到最左下边path.lineTo(0, height);//移动到左上边// path.lineTo(0, y);//实现左右波动,根据progress来平移path.lineTo(-(1 - percent) * radius * 2, y);if (currentProgress != 0.0f) {//根据直径计算绘制贝赛尔曲线的次数float count = radius * 2 / 30;//控制-控制点y的坐标float point = (1 - percent) * 15;for (int i = 0; i < count; i++) {path.rQuadTo(15, -point, 30, 0);path.rQuadTo(15, point, 30, 0);}}//闭合path.close();bitmapCanvas.drawPath(path, progressPaint);//绘制文字String text = currentProgress + "%";Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;bitmapCanvas.drawText(text, width / 2, radius - offset, textPaint);bitmapCanvas.restore();canvas.drawBitmap(bitmap, 0, 0, null);

这里我们将onDraw()的代码都写了上去,我们就着重来看一下绘制贝塞尔曲线这一部分,逐行分析:

1.首先我们reset了path,path是安卓中用来绘制图形,路径,曲线等多种图案的工具,有必要的时候我会写一篇博客学习一下;

2.定义了float类型变量,这个的值就是当前进度百分比,用 currentProgress * 1.0f / maxProgress 计算得到,这样计算可以保留精度;

3.在之前我们有一行代码需要关注一下:

bitmapCanvas.translate(0, height / 4);

这一行代码,我们移动了bitmapCanvas的坐标系,经过我的反复研究,我们绘制的画布的坐标系原点是在左上角的位置,也就是这里:

原谅我画的图太辣鸡,但是想来应该都会懂得,红点的位置就是原点位置,x轴向右为正,y轴向下为正,在上面那串移动坐标系的代码中,我们可以看到横坐标没有移动,纵坐标移动 height / 4 ,y轴向下为正,所以整体绘制的View就会向下移动 height / 4 距离,呈现的效果就是这样:

好,我们回归第三行代码,我们定义了float类型的y,也就是纵坐标,表达式为 (1 - percent) * radius * 2;,percent是当前的进度,那么y的值就是从radius*2一直变到0,由于y轴向下为正,那么也就是从View的最下端一直到最上部;

4.在这里我们开始对path进行操作,moveto的作用是设置起点,从代码可以看出,起点在(width,y)这个位置,根据上面说的,那也就是右下角位置;

5.lineto操作,代表移动连线,起点已经设置好,但是由于我们这个y是一直在变化的,因此为了保持绘制的完整,我们下一点还是要设置在右下角;

6.和5基本一样,第三个点绘制在左下角;

7.最后一个点,我们为了绘制出的水波浪效果是动态波动的,将最后一个点设置为根据当前进度进行调整,绘制出平滑的波浪我们的终点起点纵坐标要一致,这样才能在同一水平线,横坐标表达式:

-(1 - percent) * radius * 2

也很好理解,一开始位置是-2*radius,最后是0,这样保证了绘制的所有阶段,圆形区域内都是顺滑的,不会出现空白;

8.这个if我们整体说一下,如果当前不是刚开始的时候,就会进入if中,首先我们根据直径来判断要绘制多少次贝塞尔曲线,radius * 2 / 30 ,radius*2不用多说,是直径,至于这个三十,我们贴一张图:

这部分借鉴了这篇文章:

android 自定义view-水波纹进度球

我们选择30为一个周期,这部分我们可以自定,也可以是60,这样计算出count这个值,作为计算的结果,在下一步我们计算控制点的y坐标,我们一开始拟定是15,根据当前进度,进度越大,波动越小;

9.根据8中计算的次数进行循环,根据8中计算的控制点y坐标进行波动,我们回头看一下刚说过的贝塞尔曲线的rQuadTo()方法,里面的参数都是相对起点的值,这里的起点,其实是我们刚说的path的终点,有点绕,自己理解一下;

10.在最后部份,我们使用path.close()方法,将path的起点和终点相连,构成闭合区域,最后绘制,

效果图如下:

当然我这里更改了一下颜色,这样看着更加明显一点。

最后贴一个整体的自定义View代码,以供参考:

package com.example.day1.View;import android.annotation.SuppressLint;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PorterDuff;import android.graphics.PorterDuffXfermode;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import androidx.annotation.Nullable;import com.example.day1.R;public class WaterView extends View {//定义背景颜色,字体颜色;private int backgroundColor, textColor, waterColor;private float radius, textSize;private String text;private Paint backgroundPaint, textPaint, progressPaint;private SingleTapThread singleTapThread;private int currentProgress = 0;private int maxProgress = 100;private Path path = new Path();private Bitmap bitmap;private Canvas bitmapCanvas;private int width, height;public WaterView(Context context) {this(context, null);}public WaterView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);TypedArray waterTypeArray = context.obtainStyledAttributes(attrs, R.styleable.WaterView);backgroundColor = waterTypeArray.getColor(R.styleable.WaterView_backgroundColor, Color.WHITE);textColor = waterTypeArray.getColor(R.styleable.WaterView_textColor, Color.BLACK);radius = waterTypeArray.getDimension(R.styleable.WaterView_radius, 260f);textSize = waterTypeArray.getDimension(R.styleable.WaterView_textSize, 24f);text = waterTypeArray.getString(R.styleable.WaterView_text);waterColor = waterTypeArray.getColor(R.styleable.WaterView_waterColor, Color.GREEN);//记得回收waterTypeArray.recycle();initPaint();}/*** 初始化画笔*/private void initPaint() {//初始化背景画笔backgroundPaint = new Paint();backgroundPaint.setColor(backgroundColor);//抗锯齿backgroundPaint.setAntiAlias(true);//初始化显示文字画笔textPaint = new Paint();textPaint.setTextSize(textSize);textPaint.setColor(textColor);textPaint.setAntiAlias(true);//字体为粗体textPaint.setFakeBoldText(true);textPaint.setTextAlign(Paint.Align.CENTER);//初始化波浪画笔progressPaint = new Paint();progressPaint.setAntiAlias(true);progressPaint.setColor(waterColor);//取两层绘制交集。显示上层progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);int width, height;if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {height = width = (int) radius * 2;setMeasuredDimension(width, height);} else {setMeasuredDimension(widthSpecSize, heightSpecSize);}}/*** 绘制部份** @param canvas 画布*/@SuppressLint("DrawAllocation")@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);width = getWidth();height = getHeight();if (bitmap == null) {bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);bitmapCanvas = new Canvas(bitmap);}bitmapCanvas.save();//移动坐标系bitmapCanvas.translate(0, height / 4);//绘制圆bitmapCanvas.drawCircle(radius, radius, radius, backgroundPaint);//绘制PATH//重置绘制路线path.reset();float percent = currentProgress * 1.0f / maxProgress;float y = (1 - percent) * radius * 2;//起点移动到右下path.moveTo(width, y);//移动到右下方path.lineTo(width, height);//移动到最左下边path.lineTo(0, height);//移动到左上边// path.lineTo(0, y);//实现左右波动,根据progress来平移path.lineTo(-(1 - percent) * radius * 2, y);if (currentProgress != 0.0f) {//根据直径计算绘制贝赛尔曲线的次数float count = radius * 2 / 30;//控制-控制点y的坐标float point = (1 - percent) * 15;for (int i = 0; i < count; i++) {path.rQuadTo(15, -point, 30, 0);path.rQuadTo(15, point, 30, 0);}}//闭合path.close();bitmapCanvas.drawPath(path, progressPaint);//绘制文字String text = currentProgress + "%";Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;bitmapCanvas.drawText(text, width / 2, radius - offset, textPaint);bitmapCanvas.restore();canvas.drawBitmap(bitmap, 0, 0, null);}@SuppressLint("ClickableViewAccessibility")@Overridepublic boolean onTouchEvent(MotionEvent event) {if (event.getAction() == MotionEvent.ACTION_UP) {startProgressAnimation();}return super.onTouchEvent(event);}private void startProgressAnimation() {if (singleTapThread == null) {singleTapThread = new SingleTapThread();getHandler().postDelayed(singleTapThread, 100);}}private class SingleTapThread implements Runnable {@Overridepublic void run() {if (currentProgress < maxProgress) {invalidate();getHandler().postDelayed(singleTapThread, 100);currentProgress++;} else {getHandler().removeCallbacks(singleTapThread);}}}}

就这样,撤退。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。