项目实战-仿bilibili刷新按钮的实现

原创 2017年10月24日 13:00:19

一、简述

       最近跟小伙伴一起讨论了一下,决定一起仿一个BiliBili的app(包括android端和iOS端),我们并没有打算把这个项目完全做完,毕竟我们的重点是掌握一些新框架的使用,并在实战过程中发现并弥补自身的不足。

       本系列将记录我(android端)在开发过程中的一些我觉得有必要记录的功能实现而已,并不是完整的从0到1的完整教程,若个别看官大爷觉得不好请出门左拐谢谢。

以下是该项目将会完成的功能。

  1. 视频播放功能
  2. 直播功能
  3. 弹幕功能
  4. 换肤功能

本系列文章,将会有记录以上功能的实现但不仅仅只有这些,还会有一些其他,比如自定义控件、利用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, 默认值)

通过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"/>

最后附近完整代码

LQRRefreshButton.java

版权声明:本文为博主原创文章,未经博主允许不得转载。

Android 点击Button更新接口数据刷新页面状态

欢迎使用Markdown编辑器写博客本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦: Markdown和扩展Markdown简洁的语法 代码块高亮 图片链接和...
  • blqs_2015
  • blqs_2015
  • 2015年12月05日 16:58
  • 2902

MUI开发实战第二集---仿今日头条

这集的主要是内容仿今日头条的顶部 在使用mui的开发过程中,需要按照mui指定好的规则进行开发,头部和底部需要放到内容之前 这是代码截图 仿今日头条---您关心的...
  • myfirtyou
  • myfirtyou
  • 2015年06月10日 15:34
  • 3870

项目实战-仿bilibili刷新按钮的实现

一、简述        最近跟小伙伴一起讨论了一下,决定一起仿一个BiliBili的app(包括android端和iOS端),我们并没有打算把这个项目完全做完,毕竟我们的重点是掌握一些新框架的使...
  • mmp591
  • mmp591
  • 2017年10月25日 18:31
  • 115

项目实战 - 使用Fiddler抓取bilibili安卓客户端口数据并分析(http、https)

一、简述经过了一个多星期的时间(自2017/10/16开始),到目前(2017/10/24)为止,项目框架的搭建已基本完成、还完成了首页中「直播」与「推荐」Fragment的数据填充,可以说相仿度很高...
  • CSDN_LQR
  • CSDN_LQR
  • 2017年10月24日 16:57
  • 847

Fragment中实现自动定位当前城市,点击刷新按钮更新天气信息

定位用的是百度SDKv4.2接口
  • u011603302
  • u011603302
  • 2014年09月02日 19:02
  • 2080

安卓Android工具栏刷新按钮的动画旋转效果实现

  • 2015年10月25日 22:52
  • 1.44MB
  • 下载

项目实战:超实用小工具isFastClick解决重复点击按钮问题

相信大家在项目开发中都遇到过这种case,点击按钮时,由于手机一时卡顿或者手速过快,造成按钮重复多次点击,跳转生成多个Activity(然后一个个关)或者其他重复操作。 现在,就让我们通过几行代码一...
  • hxqneuq2012
  • hxqneuq2012
  • 2017年04月01日 13:46
  • 564

项目实战:微交互之按钮选择器

相信大家在很多APP中都见过这种微交互,点击按钮时,发现点击前和点击时按钮(甚至是按钮里的文字)的样式不一样,给用户一个友好的小提示,你确实点击了这个按钮,今天我们做一下这个效果。 首先,你以为的就是...
  • hxqneuq2012
  • hxqneuq2012
  • 2017年05月22日 20:00
  • 636

猫猫学iOS 之微博项目实战(4)微博自定义tabBar中间的添加按钮

猫猫分享,必须精品原创文章,欢迎转载。转载请注明:翟乃玉的博客 地址:http://blog.csdn.net/u013357243一:效果图自定义tabBar实现最下面中间的添加按钮 二:思路首...
  • u013357243
  • u013357243
  • 2015年07月27日 10:00
  • 2324

iOS【微博项目实战(4)微博自定义tabBar中间的添加按钮】

AJ分享,必须精品   一:效果图 自定义tabBar实现最下面中间的添加按钮 二:思路 首先在自己的tabBarController中把系统的tabBar设置成自己的tabBar(NYTabBa...
  • luolianxi
  • luolianxi
  • 2015年11月29日 00:44
  • 168
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:项目实战-仿bilibili刷新按钮的实现
举报原因:
原因补充:

(最多只允许输入30个字)