前言
本篇文章记录Android
下实现自定义百度贴吧的水波纹Loading效果,主要涉及到的知识点是画布Canvas
、画布上绘制圆drawCircle
、绘制文字drawText
、绘制直线drawLine
、贝塞尔曲线lineTo
, quadTo
,如何实现水波纹效果,如何实现蓝色文字和白色文字叠加的显示效果,下面,梳理下详细的自定义过程。
说明
1、实现效果
实现的效果如下,水波纹上方显示的是蓝色的字,水波纹的下方显示的是白色的文字。
2、实现步骤
下面先梳理下实现的步骤,看上面的动画效果中,有两种颜色的“贴”字,首先是蓝色,白色“贴”从视觉上看是在水波纹之上的,那么绘制的顺序如下:
- 绘制蓝色“贴”
- 绘制水波纹
- 绘制白色“贴”
绘制文字很好处理,这里主要注意一下,文字如何居中显示。那么主要看下水波纹如何实现的,下图是将动画过程拆分出的两帧。
- 首先绘制一个圆,以圆的直径为长度绘制两个周期的正弦波
- 将正弦波向右平移,最大平移长度为圆的直径长度后,重置绘制为图1的状态。
- 如何只保留实际的效果呢?这里就要用的
Canvas
的裁剪方法clipPath
,裁剪的路径就是黄色和圆中蓝色的区域。 - 两个周期的正弦波路径是已知的,黄色区域底部最大的距离是圆的半径,将
P1 、P2、P3、P4
四个点连接起来形成要剪裁的路径。
实现
通过上面的分析,应该对效果有了个直观的概念,下面用代码实现。
1、初始化用到的画笔、以及声明自定义View的属性,文字内容和颜色(同水波纹颜色)可以自定义。
values
下新建attrs.xml
,新建名称为TieBaView
的属性声明文件
<declare-styleable name="TieBaView">
<!--字体和圆的颜色-->
<attr name="textColor" format="color"/>
<!--文字内容-->
<attr name="text" format="color"/>
</declare-styleable>
//声明变量
private var mWidth = 0
private var mHeight = 0
private var centerPointX = 0f
private var centerPointY = 0f
private var textRect: Rect = Rect()
private var textX = 0f
private var textY = 0f
private var radius = 200f
private var fraction = 0.0f
private var text:String = "贴"
private var textColor:Int = 0
/**
* 底部贴字
*/
private val bottomTextPaint = Paint().apply{
textSize = 180f
isDither = true
isAntiAlias = true
style = Paint.Style.FILL
}
/**
* 顶部贴字
*/
private val topTextPaint = Paint().apply {
textSize = 180f
color = context.getColor(R.color.colorWhite)
isDither = true
isAntiAlias = true
style = Paint.Style.FILL
}
/**
* 圆
*/
private val circlePaint = Paint().apply {
strokeWidth = 20f
isDither = true
isAntiAlias = true
style = Paint.Style.FILL
}
init {
initAttrs(attributeSet)
initAnimator()
}
/**
* 初始化属性
*/
private fun initAttrs(attr:AttributeSet) {
val ta = context.obtainStyledAttributes(attr,R.styleable.TieBaView)
text = ta.getString(R.styleable.TieBaView_text).toString()
textColor = ta.getColor(R.styleable.TieBaView_textColor,context.getColor(R.color.colorBlue))
ta.recycle()
bottomTextPaint.color = textColor
circlePaint.color = textColor
}
2、重写onSizeChanged
方法,计算圆的中心点坐标、以及文字居中显示处理
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mHeight = h
//自定义view中心点x坐标
centerPointX = mWidth / 2f
//自定义view中心点u坐标
centerPointY = mHeight / 2f
//获取文字的基线
topTextPaint.getTextBounds(text,0,text.length,textRect)
//计算文字摆放位置
calculateTextPos()
}
/**
* 计算文字绘制坐标
*/
private fun calculateTextPos() {
textX = -abs(textRect.right - textRect.left) / 2f
textY = -abs(textRect.bottom - textRect.top) / 2f
}
3、重写onDraw
方法绘制处理
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//将画布平移到屏幕中央
canvas.withTranslation(centerPointX,centerPointY) {
//绘制底部文字
canvas.drawText(text, - (textRect.right - textRect.left) / 2f,- (textRect.bottom + textRect.top) / 2f,bottomTextPaint)
//计算路径,canvas剪裁保留计算的路径
clipPath(generateWavePath(percent))
//绘制圆形
drawCircle(0f,0f,radius,circlePaint)
//绘制顶部文字
drawText(text, - (textRect.right - textRect.left) / 2f,- (textRect.bottom + textRect.top) / 2f,topTextPaint)
}
}
/**
* 平移正弦波,获取对应的路径path
*/
private fun generateWavePath(fraction:Float):Path{
val clipPath = Path()
val startX = - 3 * radius + 2 * radius * percent
clipPath.moveTo(startX,0f)
//控制点的距离上下起点宽度
val quadWidth = radius / 2
//控制点的高度,是曲线的振幅,这里可以自己定义
val quadHeight = radius * 2 / 6
clipPath.apply {
//绘制第一段曲线
quadTo(startX + quadWidth , - quadHeight ,startX + 2 * quadWidth ,0f)
moveTo(startX + 2 * quadWidth ,0f)
quadTo( startX + 3 * quadWidth , quadHeight , startX + 4 * quadWidth,0f)
// 绘制第二段曲线
moveTo( startX + 4 * quadWidth,0f)
quadTo(startX + 5 * quadWidth ,-quadHeight ,startX + 6 * quadWidth , 0f)
moveTo(startX + 6 * quadWidth , 0f)
quadTo(startX + 7 * quadWidth ,quadHeight , startX + 8 * quadWidth, 0f)
lineTo(startX + 8 * quadWidth,quadHeight * 10)
lineTo(startX ,quadHeight * 10)
lineTo(startX,0f)
}
return clipPath
}
下面看下Canvas
的clipPath
方法
/**
* 裁剪指定相交的路径,这里传入我们计算好生成的路径即可
*
* Intersect the current clip with the specified path.
* @param path The path to intersect with the current clip
* @return true if the resulting clip is non-empty
*/
public boolean clipPath(@NonNull Path path) {
return clipPath(path, Region.Op.INTERSECT);
}
4、使用动画,生成正弦波平移的系数,让正弦波平移起来
上面在调用平移正弦波,生成对应路径方法generateWavePath(fraction:Float)
中传入了系数fraction
,上面已经分析了,正弦波向右平移的最大宽度为圆的直径,只要fraction
的范围在0-1
之间变化即可,当平移了圆的直径后立刻fraction
立刻置为0
,这里使用属性动画中的ValueAnimator
来实现。
/**
* 初始化动画
*/
private fun initAnimator() {
val animator = ValueAnimator.ofFloat(0f,1f).apply {
duration = 1500
repeatMode = ValueAnimator.RESTART
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
}
animator.addUpdateListener { a ->
//获取当前动画的进度
fraction = a.animatedFraction
//重新绘制
invalidate()
}
animator.start()
}
5、XML中使用
<com.xn.customview.widget.TieBaView
android:id="@+id/tbView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:text="@string/tie"
app:textColor="@color/colorBlue" />
总结
本篇自定义贴吧水波纹Loading效果,主要熟悉下画布的有关特性和Path
路径的有关方法,画布在自定义View
中是最重要的角色之一,也是要着重掌握的知识。