Android开发仿bilibili刷新按钮的实现代码
一、简述
最近跟小伙伴一起讨论了一下,决定一起仿一个bilibili的app(包括android端和ios端),我们并没有打算把这个项目完全做完,毕竟我们的重点是掌握一些新框架的使用,并在实战过程中发现并弥补自身的不足。
本系列将记录我(android端)在开发过程中的一些我觉得有必要记录的功能实现而已,并不是完整的从0到1的完整教程,若个别看官大爷觉得不好请出门左拐谢谢。
以下是该项目将会完成的功能。
- 视频播放功能
- 直播功能
- 弹幕功能
- 换肤功能
- …
本系列文章,将会有记录以上功能的实现但不仅仅只有这些,还会有一些其他,比如自定义控件、利用fiddler抓包等,接下来就进入本篇的主题——《仿bilibili刷新按钮的实现》。
二、实战
1、分析
先来看看原版效果:
该按钮由3部分组成,分别是圆角矩形、文字、旋转图标。在点击按钮后,开始加载数据,旋转图标发生旋转,数据加载完成后,旋转图标复位并停止旋转。话不多说,开始敲代码。
2、绘制
这里,我们要绘制的部分有3个,分别是上面提到的圆角矩形、文字、旋转图标。那么这里就为这3部分分别声明了一些属性。
要注意的一点是,这个类中有3个构造函数,因为有部分属性需要在构造函数中初始化(也为之后自定义属性做准备),所以,将第1个与第2个构造函数中的super修改为this。
public class lqrrefreshbutton extends view { // 圆角矩形属性 private int bordercolor = color.parsecolor("#fb7299"); private float borderwidth = 0; private float borderradius = 120; // 文字属性 private string text = "点击换一批"; private int textcolor = color.parsecolor("#fb7299"); private float textsize = 28; // 旋转图标属性 private int iconsrc = r.mipmap.tag_center_refresh_icon; private float iconsize = 28; private bitmap iconbitmap; private float space4textandicon = 20; // 画笔 private paint mpaint = new paint(paint.anti_alias_flag); public lqrrefreshbutton(context context) { this(context, null); } public lqrrefreshbutton(context context, @nullable attributeset attrs) { this(context, attrs, 0); } public lqrrefreshbutton(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 将图标资源实例化为bitmap iconbitmap = bitmapfactory.decoderesource(getresources(), r.mipmap.tag_center_refresh_icon); } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); // 1、画圆角矩形 // 2、画字 // 3、画刷新图标 } }
接下来着重完成ondraw()方法的实现:
@override protected void ondraw(canvas canvas) { super.ondraw(canvas); // 1、画圆角矩形 mpaint.setstyle(paint.style.stroke); mpaint.setcolor(bordercolor); mpaint.setstrokewidth(borderwidth); canvas.drawroundrect(new rectf(0, 0, getwidth(), getheight()), borderradius, borderradius, mpaint); // 2、画字 mpaint.settextsize(textsize); mpaint.setcolor(textcolor); mpaint.setstyle(paint.style.fill); float measuretext = mpaint.measuretext(text); float measureandicon = measuretext + space4textandicon + iconsize; float textstartx = getwidth() / 2 - measureandicon / 2; float textbasey = getheight() / 2 + (math.abs(mpaint.ascent()) - mpaint.descent()) / 2; canvas.drawtext(text, textstartx, textbasey, mpaint); // 3、画刷新图标 float iconstartx = textstartx + measuretext + space4textandicon; canvas.drawbitmap(iconbitmap, iconstartx, getheight() / 2 - iconsize / 2, mpaint); }
先来看看效果:
我给该控件设置了宽为200dp,高为100dp。
可以看到效果还不错,但还是有一点点问题的,下面就分别说说这3部分是怎么画的,及存在的小问题。
1)画圆角矩形
其实画圆角矩形很简单,设置好画笔的样式、颜色、线粗,再调用canvas的drawroundrect()方法即可实现。
因为我们要画的圆角矩形只需要画线,所以画笔的样式便设置为paint.style.stroke。
canvas的drawroundrect()方法中,第一个参数是绘制范围,这里就直接按该控件的大小来设置即可。第二、三个参数是x轴和y轴的圆角半径,第三个参数是画笔(要画东西当然需要画笔~)。
但你有没有发现,此时的 线粗为0(borderwidth=0),矩形线怎么还有?这是因为画笔的样式为paint.style.stroke,当线粗为0时,还要画出1px的线,因为对画笔来说,最小的线粗就是1px。所以,上面的代码需要做如下改动:
// 1、画圆角矩形 if (borderwidth > 0) { mpaint.setstyle(paint.style.stroke); mpaint.setcolor(bordercolor); mpaint.setstrokewidth(borderwidth); canvas.drawroundrect(new rectf(0, 0, getwidth(), getheight()), borderradius, borderradius, mpaint); }
2)画字
画字的一般步骤是设置文字大小、文字颜色、画笔样式,绘制起点。其中后2个最为重要。
画笔样式对画出的字是有影响的,当画笔样式为paint.style.stroke时,画出来的字是镂空的(不信你可以试试),我们需要的是实心的字,所以需要修改画笔的样式为paint.style.fill。
在安卓中,文字的绘制跟其它绘制是不同的,例如,圆角矩形和旋转图标的绘制起点是左上角,而文字则是按文字左下字为起点,也就是按基线(baseline)来绘制,故需要得到基线起点的坐标。
如上图中,现在要获得的就是文字左下角的点,这要怎么求呢?
先说x,一般需要让文字居中显示(跟文字的对齐方式也有关系,这里以默认的左对齐为例),所以计算公式一般为: x = 控件宽度/2 - 文字长度/2。但我们这个控件有点不同,它还需要考虑到旋转图标的位置问题,所以x应该这么求: x = 控件宽度/2 - (文字长度+空隙+旋转图标宽度)/2。
// 得到文字长度 float measuretext = mpaint.measuretext(text); // 得到 文字长度+空隙+旋转图标宽度 float measureandicon = measuretext + space4textandicon + iconsize; // 得到文字绘制起点 float textstartx = getwidth() / 2 - measureandicon / 2;
再说y,如图所示:
如果直接用控件的高度的一半作为文字绘制的基线,那么绘制出来的文字肯定偏上,这是因为ascent的高度比descent的高度要高的多,我们在计算baseline时,需要在ascent中减去descent的高度得到两者高度差,再让控件中心y坐标加上(下降)这个高度差的一半。故:
float textbasey = getheight() / 2 + (math.abs(mpaint.ascent()) - mpaint.descent()) / 2;
3)画刷新图标
最后就是画刷新图标了,它是以左上角为起点的,通过canvas的drawbitmap()方法进行绘制即可。
但是,有一点需要注意,iconsize是我自己定的一个大小,并不是图标的实际大小,所以在往后做旋转动画时获取到的旋转中心会有误差,将导致图标旋转时不是按中心进行旋转。所以,这里需要对图标大小进行调整:
public class lqrrefreshbutton extends view { ... public lqrrefreshbutton(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // icon iconbitmap = bitmapfactory.decoderesource(getresources(), iconsrc); iconbitmap = zoomimg(iconbitmap, iconsize, iconsize); } public bitmap zoomimg(bitmap bm, float newwidth, float newheight) { // 获得图片的宽高 int width = bm.getwidth(); int height = bm.getheight(); // 计算缩放比例 float scalewidth = ((float) newwidth) / width; float scaleheight = ((float) newheight) / height; // 取得想要缩放的matrix参数 matrix matrix = new matrix(); matrix.postscale(scalewidth, scaleheight); // 得到新的图片 bitmap newbm = bitmap.createbitmap(bm, 0, 0, width, height, matrix, true); return newbm; } ... }
3、动画
现在,要实现旋转图标的旋转功能了。原理就是在canvas绘制图标时,将canvas进行旋转,canvas旋转着绘制图标也很简单,只需要4步:
canvas.save(); canvas.rotate(degress, centerx, centery); canvas.drawbitmap(iconbitmap, iconstartx, getheight() / 2 - iconsize / 2, mpaint); canvas.restore();
接下来要做的,就是计算出旋转中心,旋转角度,并不停止的去调用ondraw()编制图标,可以使用valueanimator或objectanimator实现这个功能,这里选用objectanimator。实现如下:
public class lqrrefreshbutton extends view { ... private float degress = 0; private objectanimator manimator; public lqrrefreshbutton(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 旋转动画 manimator = objectanimator.ofobject(this, "degress", new floatevaluator(), 360, 0); manimator.setduration(2000); manimator.setrepeatmode(objectanimator.restart); manimator.setinterpolator(new linearinterpolator()); manimator.setrepeatcount(objectanimator.infinite); } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); ... // 3、画刷新图标 float iconstartx = textstartx + measuretext + space4textandicon; canvas.save(); float centerx = iconstartx + iconsize / 2; int centery = getheight() / 2; canvas.rotate(degress, centerx, centery); canvas.drawbitmap(iconbitmap, iconstartx, getheight() / 2 - iconsize / 2, mpaint); canvas.restore(); } public void start() { manimator.start(); } public void stop() { manimator.cancel(); setdegress(0); } public float getdegress() { return degress; } public void setdegress(float degress) { this.degress = degress; invalidate(); } }
使用objectanimator可以对任意属性值进行修改,所以需要在该控件中声明一个旋转角度变量(degress),并编写getter和setter方法,还需要在setter方法中调用invalidate(),这样才能在角度值发生变换时,让控件回调ondraw()进行图标的旋转绘制。objectanimator的使用也不复杂,这里就不详细介绍了。来看下动画效果吧:
4、自定义属性
一个自定义控件,是不能把属性值写死在控件里的,所以我们需要自定义属性,从外界获取这些属性值。
1)属性文件编写
在attrs.xml中编写如下代码:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="lqrrefreshbutton"> <attr name="refresh_btn_bordercolor" format="color"/> <attr name="refresh_btn_borderwidth" format="dimension"/> <attr name="refresh_btn_borderradius" format="dimension"/> <attr name="refresh_btn_text" format="string"/> <attr name="refresh_btn_textcolor" format="color"/> <attr name="refresh_btn_textsize" format="dimension"/> <attr name="refresh_btn_iconsrc" format="reference"/> <attr name="refresh_btn_iconsize" format="dimension"/> <attr name="refresh_btn_space4textandicon" format="dimension"/> </declare-styleable> </resources>
2)属性值获取
在控件的第三个构造函数中获取这些属性值:
public class lqrrefreshbutton extends view { public lqrrefreshbutton(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 获取自定义属性值 typedarray ta = context.obtainstyledattributes(attrs, r.styleable.lqrrefreshbutton); bordercolor = ta.getcolor(r.styleable.lqrrefreshbutton_refresh_btn_bordercolor, color.parsecolor("#fb7299")); borderwidth = ta.getdimension(r.styleable.lqrrefreshbutton_refresh_btn_borderwidth, diptopx(0)); borderradius = ta.getdimension(r.styleable.lqrrefreshbutton_refresh_btn_borderradius, diptopx(60)); text = ta.getstring(r.styleable.lqrrefreshbutton_refresh_btn_text); if (text == null) text = ""; textcolor = ta.getcolor(r.styleable.lqrrefreshbutton_refresh_btn_textcolor, color.parsecolor("#fb7299")); textsize = ta.getdimension(r.styleable.lqrrefreshbutton_refresh_btn_textsize, sptopx(14)); iconsrc = ta.getresourceid(r.styleable.lqrrefreshbutton_refresh_btn_iconsrc, r.mipmap.tag_center_refresh_icon); iconsize = ta.getdimension(r.styleable.lqrrefreshbutton_refresh_btn_iconsize, diptopx(14)); space4textandicon = ta.getdimension(r.styleable.lqrrefreshbutton_refresh_btn_space4textandicon, diptopx(10)); ta.recycle(); ... } }
这里有一点需要留意:
ta.getdimension(属性id, 默认值)
1
2
通过typedarray对象可以从外界到的的值会根据单位(如:dp、sp)的不同自动转换成px,但默认值的单位是一定的,为px,所以为了符合安卓规范,不要直接使用px,所以需要手动做个转换。最后还需要调用recycle()方法回收typedarray。
3)在布局文件中应用
<com.lqr.biliblili.mvp.ui.widget.lqrrefreshbutton android:id="@+id/btn_refresh" android:layout_width="118dp" android:layout_height="32dp" android:layout_gravity="center" android:layout_marginbottom="3dp" android:layout_margintop="8dp" app:refresh_btn_borderradius="25dp" app:refresh_btn_borderwidth="1dp" app:refresh_btn_iconsize="16dp" app:refresh_btn_text="点击换一批" app:refresh_btn_textcolor="@color/bottom_text_live" app:refresh_btn_textsize="14sp"/>
总结
以上所述是小编给大家介绍的android 仿bilibili刷新按钮的实现,希望对大家有所帮助