Android自定义View模仿虎扑直播界面的打赏按钮功能

前言

作为一个资深篮球爱好者,我经常会用虎扑app看比赛直播,后来注意到文字直播界面右下角加了两个按钮,可以在直播过程中送虎扑币,为自己支持的球队加油。

具体的效果如下图所示:

我个人觉得挺好玩的,所以决定自己实现下这个按钮,废话不多说,先看实现的效果吧:

这个效果看起来和popupwindow差不多,但我是采用自定义view的方式来实现,下面说说过程。

实现过程

首先从虎扑的效果可以看到,它这两个按钮时浮在整个界面之上的,所以它需要和FrameLayout结合使用,因此我让它的宽度跟随屏幕大小,高度根据dpi固定,它的实际尺寸时这样的:

另外这个view初始化出来我们看到可以分为三块,背景圆、圆内文字、圆上方数字,所以正常状态下,只需要在onDraw方法中画出这三块内容即可。先在初始化方法中将自定义的属性和画笔以及初始化数据准备好:

private void init(Context context, AttributeSet attrs) {
//获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HoopView);
mThemeColor = typedArray.getColor(R.styleable.HoopView_theme_color, Color.YELLOW);
mText = typedArray.getString(R.styleable.HoopView_text);
mCount = typedArray.getString(R.styleable.HoopView_count);

mBgPaint = new Paint();
mBgPaint.setAntiAlias(true);
mBgPaint.setColor(mThemeColor);
mBgPaint.setAlpha(190);
mBgPaint.setStyle(Paint.Style.FILL);

mPopPaint = new Paint();
mPopPaint.setAntiAlias(true);
mPopPaint.setColor(Color.LTGRAY);
mPopPaint.setAlpha(190);
mPopPaint.setStyle(Paint.Style.FILL_AND_STROKE);

mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_text_size));

mCountTextPaint = new TextPaint();
mCountTextPaint.setAntiAlias(true);
mCountTextPaint.setColor(mThemeColor);
mCountTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_count_text_size));

typedArray.recycle();

mBigRadius = context.getResources().getDimension(R.dimen.hoop_big_circle_radius);
mSmallRadius = context.getResources().getDimension(R.dimen.hoop_small_circle_radius);
margin = (int) context.getResources().getDimension(R.dimen.hoop_margin);
mHeight = (int) context.getResources().getDimension(R.dimen.hoop_view_height);
countMargin = (int) context.getResources().getDimension(R.dimen.hoop_count_margin);

mDatas = new String[] {"1", "10", "100"};
// 计算背景框改变的长度,默认是三个按钮
mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);}

在onMeasure中测出view的宽度后,根据宽度计算出背景圆的圆心坐标和一些相关的数据值。

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
mWidth = getDefaultSize(widthSize, widthMeasureSpec);
setMeasuredDimension(mWidth, mHeight);

// 此时才测出了mWidth值,再计算圆心坐标及相关值
cx = mWidth - mBigRadius;
cy = mHeight - mBigRadius;
// 大圆圆心
circle = new PointF(cx, cy);
// 三个按钮的圆心
circleOne = new PointF(cx - mBigRadius - mSmallRadius - margin, cy);
circleTwo = new PointF(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy);
circleThree = new PointF(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy);
// 初始的背景框的边界即为大圆的四个边界点
top = cy - mBigRadius;
bottom = cy + mBigRadius;
}

因为这里面涉及到点击按钮展开和收缩的过程,所以我定义了如下几种状态,只有在特定的状态下才能进行某些操作。

private int mState = STATE_NORMAL;//当前展开收缩的状态
private boolean mIsRun = false;//是否正在展开或收缩

//正常状态
public static final int STATE_NORMAL = 0;
//按钮展开
public static final int STATE_EXPAND = 1;
//按钮收缩
public static final int STATE_SHRINK = 2;
//正在展开
public static final int STATE_EXPANDING = 3;
//正在收缩
public static final int STATE_SHRINKING = 4;

接下来就执行onDraw方法了,先看看代码:

@Override protected void onDraw(Canvas canvas) {
switch (mState) {
case STATE_NORMAL:
drawCircle(canvas);
break;
case STATE_SHRINK:
case STATE_SHRINKING:
drawBackground(canvas);
break;
case STATE_EXPAND:
case STATE_EXPANDING:
drawBackground(canvas);
break;
}
drawCircleText(canvas);
drawCountText(canvas);
}

圆上方的数字和圆内的文字是整个过程中一直存在的,所以我将这两个操作放在switch之外,正常状态下绘制圆和之前两部分文字,点击展开时绘制背景框展开过程和文字,展开状态下再次点击绘制收缩过程和文字,当然在绘制背景框的方法中也需要不断绘制大圆,大圆也是一直存在的。

上面的绘制方法:

/**
 * 画背景大圆
 * @param canvas
 */
private void drawCircle(Canvas canvas) {
left = cx - mBigRadius;
right = cx + mBigRadius;
canvas.drawCircle(cx, cy, mBigRadius, mBgPaint);
}

/**
 * 画大圆上面表示金币数的文字
 * @param canvas
 */
private void drawCountText(Canvas canvas) {
canvas.translate(0, -countMargin);
//计算文字的宽度
float textWidth = mCountTextPaint.measureText(mCount, 0, mCount.length());
canvas.drawText(mCount, 0, mCount.length(), (2 * mBigRadius - textWidth - 35) / 2, 0.2f, mCountTextPaint);
}

/**
 * 画大圆内的文字
 * @param canvas
 */
private void drawCircleText(Canvas canvas) {
StaticLayout layout = new StaticLayout(mText, mTextPaint, (int) (mBigRadius * Math.sqrt(2)), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, true);
canvas.translate(mWidth - mBigRadius * 1.707f, mHeight - mBigRadius * 1.707f);
layout.draw(canvas);
canvas.save();
}

/**
 * 画背景框展开和收缩
 * @param canvas
 */
private void drawBackground(Canvas canvas) {
left = cx - mBigRadius - mChange;
right = cx + mBigRadius;
canvas.drawRoundRect(left, top, right, bottom, mBigRadius, mBigRadius, mPopPaint);
if ((mChange > 0) && (mChange <= 2 * mSmallRadius + margin)) {
// 绘制第一个按钮
canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
// 绘制第一个按钮内的文字
canvas.drawText(mDatas[0], cx - (mBigRadius - mSmallRadius) - mChange, cy + 15, mTextPaint);
} else if ((mChange > 2 * mSmallRadius + margin) && (mChange <= 4 * mSmallRadius + 2 * margin)) {
// 绘制第一个按钮
canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
// 绘制第一个按钮内的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint);

// 绘制第二个按钮
canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
// 绘制第二个按钮内的文字
canvas.drawText(mDatas[1], cx - mChange - 20, cy + 15, mTextPaint);
} else if ((mChange > 4 * mSmallRadius + 2 * margin) && (mChange <= 6 * mSmallRadius + 3 * margin)) {
// 绘制第一个按钮
canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
// 绘制第一个按钮内的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

// 绘制第二个按钮
canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
// 绘制第二个按钮内的文字
canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);

// 绘制第三个按钮
canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
// 绘制第三个按钮内的文字
canvas.drawText(mDatas[2], cx - mChange - 34, cy + 15, mTextPaint);
} else if (mChange > 6 * mSmallRadius + 3 * margin) {
// 绘制第一个按钮
canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint);
// 绘制第一个按钮内的文字
canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

// 绘制第二个按钮
canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
// 绘制第二个按钮内的文字
canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);

// 绘制第三个按钮
canvas.drawCircle(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy, mSmallRadius, mBgPaint);
// 绘制第三个按钮内的文字
canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint);
}
drawCircle(canvas);

}

然后是点击事件的处理,只有触摸点在大圆内时才会触发展开或收缩的操作,点击小圆时提供了一个接口给外部调用。

@Override public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//如果点击的时候动画在进行,不处理
if (mIsRun) return true;
PointF pointF = new PointF(event.getX(), event.getY());
if (isPointInCircle(pointF, circle, mBigRadius)) { //如果触摸点在大圆内,根据弹出方向弹出或者收缩按钮
if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) {
//展开
mIsRun = true;//这是必须先设置true,因为onAnimationStart在onAnimationUpdate之后才调用
showPopMenu();
} else {
//收缩
mIsRun = true;
hidePopMenu();
}
} else { //触摸点不在大圆内
if (mState == STATE_EXPAND) { //如果是展开状态
if (isPointInCircle(pointF, circleOne, mSmallRadius)) {
listener.clickButton(this, Integer.parseInt(mDatas[0]));
} else if (isPointInCircle(pointF, circleTwo, mSmallRadius)) {
listener.clickButton(this, Integer.parseInt(mDatas[1]));
} else if (isPointInCircle(pointF, circleThree, mSmallRadius)) {
listener.clickButton(this, Integer.parseInt(mDatas[2]));
}
mIsRun = true;
hidePopMenu();
}
}
break;
}
return super.onTouchEvent(event);
}

展开和收缩的动画是改变背景框的宽度属性的动画,并监听这个属性动画,在宽度值改变的过程中去重新绘制整个view。因为一开始我就确定了大圆小圆的半径和小圆与背景框之间的间距,所以初始化时已经计算好了背景框的宽度:

mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);
/**
 * 弹出背景框
 */
private void showPopMenu() {
if (mState == STATE_SHRINK || mState == STATE_NORMAL) {
ValueAnimator animator = ValueAnimator.ofInt(0, mChangeWidth);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
if (mIsRun) {
mChange = (int) animation.getAnimatedValue();
invalidate();
} else {
animation.cancel();
mState = STATE_NORMAL;
}
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mIsRun = true;
mState = STATE_EXPANDING;
}

@Override public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mIsRun = false;
mState = STATE_NORMAL;
}

@Override public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mIsRun = false;
//动画结束后设置状态为展开
mState = STATE_EXPAND;
}
});
animator.setDuration(500);
animator.start();
}
}
/**
 * 隐藏弹出框
 */
private void hidePopMenu() {
if (mState == STATE_EXPAND) {
ValueAnimator animator = ValueAnimator.ofInt(mChangeWidth, 0);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
if (mIsRun) {
mChange = (int) animation.getAnimatedValue();
invalidate();
} else {
animation.cancel();
}
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mIsRun = true;
mState = STATE_SHRINKING;
}

@Override public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mIsRun = false;
mState = STATE_EXPAND;
}

@Override public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mIsRun = false;
//动画结束后设置状态为收缩
mState = STATE_SHRINK;
}
});
animator.setDuration(500);
animator.start();
}
}

这个过程看起来是弹出或收缩,实际上宽度值每改变一点,就将所有的组件重绘一次,只是文字和大圆等内容的尺寸及位置都没有变化,只有背景框的宽度值在变,所以才有这种效果。

在xml中的使用:

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:layout_alignParentRight="true"
android:orientation="vertical">

<com.xx.hoopcustomview.HoopView
android:id="@+id/hoopview1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
app:text="支持火箭"
app:count="1358"
app:theme_color="#31A129"/>

<com.xx.hoopcustomview.HoopView
android:id="@+id/hoopview2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
app:text="热火无敌"
app:count="251"
app:theme_color="#F49C11"/>
</LinearLayout>

activity中使用:

hoopview1 = (HoopView) findViewById(R.id.hoopview1);
hoopview1.setOnClickButtonListener(new HoopView.OnClickButtonListener() {
@Override public void clickButton(View view, int num) {
Toast.makeText(MainActivity.this, "hoopview1增加了" + num, Toast.LENGTH_SHORT).show();
}
});

大致实现过程就是这样,与原始效果还是有点区别,我这个还有很多瑕疵,比如文字的位置居中问题,弹出或收缩时,小圆内的文字的旋转动画我没有实现。

总结

以上就是这篇文章的全部内容了,希望本文的内容对各位Android开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Android仿斗鱼直播的弹幕效果

    记得之前有位朋友在我的公众号里问过我,像直播的那种弹幕功能该如何实现?如今直播行业确实是非常火爆啊,大大小小的公司都要涉足一下直播的领域,用斗鱼的话来讲,现在就是千播之战.而弹幕则无疑是直播功能当中最为重要的一个功能之一,那么今天,我就带着大家一起来实现一个简单的Android端弹幕效果. 分析 首先我们来看一下斗鱼上的弹幕效果,如下图所示: 这是一个Dota2游戏直播的界面,我们可以看到,在游戏界面的上方有很多的弹幕,看直播的观众们就是在这里进行讨论的. 那么这样的一个界面该如何实现呢?其实并

  • Android 实现仿网络直播弹幕功能详解及实例

    Android 网络直播弹幕                最近看好多网络电视,播放器及直播都有弹幕功能,自己周末捣鼓下并实现,以下是网上的资料,大家可以看下. 现在网络直播越来越火,网络主播也逐渐成为一种新兴职业,对于网络直播,弹幕功能是必须要有的,如下图: 首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有人发弹幕消息时,再将消息绘

  • Android实现炫酷的网络直播弹幕功能

    现在网络直播越来越火,网络主播也逐渐成为一种新兴职业,对于网络直播,弹幕功能是必须要有的,如下图: 首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有人发弹幕消息时,再将消息绘制到弹幕的View上面就可以了,下方肯定还有有操作界面View,可以让用户来发弹幕和送礼物的功能,原理示意图如下所示: 参照原理图,下面一步一步来实现这个功能.

  • Android自定义View模仿虎扑直播界面的打赏按钮功能

    前言 作为一个资深篮球爱好者,我经常会用虎扑app看比赛直播,后来注意到文字直播界面右下角加了两个按钮,可以在直播过程中送虎扑币,为自己支持的球队加油. 具体的效果如下图所示: 我个人觉得挺好玩的,所以决定自己实现下这个按钮,废话不多说,先看实现的效果吧: 这个效果看起来和popupwindow差不多,但我是采用自定义view的方式来实现,下面说说过程. 实现过程 首先从虎扑的效果可以看到,它这两个按钮时浮在整个界面之上的,所以它需要和FrameLayout结合使用,因此我让它的宽度跟随屏幕大小

  • Android自定义View模仿即刻点赞数字切换效果实例

    目录 即刻点赞展示 自己如何实现这种数字切换呢? 效果展示 源码 总结 即刻点赞展示 点赞的数字增加和减少并不是整个替换,而是差异化替换.再加上动画效果就看的很舒服. 自己如何实现这种数字切换呢? 下面用一张图来展示我的思路: 现在只需要根据这张图,写出对应的动画即可. 分为2种场景: 数字+1: 差异化的数字从3号区域由渐变动画(透明度 0- 255) + 偏移动画 (3号区域绘制文字的基线,2号区域绘制文字的基线),将数字移动到2号位置处 差异化的数字从2号区域由渐变动画(透明度 255-

  • Android自定义View实现可展开、会呼吸的按钮

    不专门练习的话,自定义View的知识又忘了许多..正好新项目里有这个需求,就再练习一下,代码已上传:地址 可以修改文本.文字大小.各种颜色: 1.按照国际惯例,就是新建attrs,写各种需要的属性,然后获取,新建各种所需的Paint.Rect,重写onMeasure计算宽高,重写onDraw画图搞起.. 2.关于可展开效果,其实就是点击发布时,启动一个ValueAnimator,对一个圆角矩形的左边距离不断改变: int mBackgroundRectFLeft; RectF mBackgrou

  • Android自定义View模仿QQ讨论组头像效果

    首先来看看我们模仿的效果图,相信对于使用过QQ的人来说都不陌生,效果图如下: 在以前的一个项目中,需要实现类似QQ讨论组头像的控件,只是头像数量和布局有一小点不一样:一是最头像数是4个,二是头像数是2个时的布局是横着排的.其实当时GitHub上就有类似的开源控件,只是那个控件在每一次绘制View的时候都会新创建一些Bitmap对象,这肯定是不可取的,而且那个控件头像输入的是Bitmap对象,不满足需求.所以只能自己实现一个了.实现的时候也没有过多的考虑,传入头像Drawable对象,根据数量排列

  • Android自定义view实现阻尼效果的加载动画

    效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动.衰减振动.[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来.这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运

  • Android自定义View绘制随机生成图片验证码

    本篇文章讲的是Android自定义View之随机生成图片验证码,开发中我们会经常需要随机生成图片验证码,但是这个是其次,主要还是想总结一些自定义View的开发过程以及一些需要注意的地方. 按照惯例先看看效果图: 一.先总结下自定义View的步骤: 1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 3.重写onMesure 4.重写onDraw 其中onMesure方法不一定要重写,但大部分情况下还是需要重写的 二.View 的几个构造函数 1.public CustomV

  • Android 自定义view模板并实现点击事件的回调

    Android 自定义view模板并实现点击事件的回调 主要的目的就是仿老版QQ的一个界面做一个模板.然后实现点击事件的回调.先看效果图: 步骤如下: 1.在res/values/目录下新建一个atts.xml文件 内容如下: <resources> <declare-styleable name="topbar"> <attr name="title" format="string"/> <attr n

  • Android自定义View实现带数字的进度条实例代码

    第一步.效果展示 图1.蓝色的进度条 图2.红色的进度条 图3.多条颜色不同的进度条 图4.多条颜色不同的进度条 第二步.自定义ProgressBar实现带数字的进度条 0.项目结构 如上图所示:library项目为自定义的带数字的进度条NumberProgressBar的具体实现,demo项目为示例项目以工程依赖的方式引用library项目,然后使用自定义的带数字的进度条NumberProgressBar来做展示 如上图所示:自定义的带数字的进度条的library项目的结构图 如上图所示:de

  • Android自定义View仿支付宝输入六位密码功能

    跟选择银行卡界面类似,也是用一个PopupWindow,不过输入密码界面是一个自定义view,当输入六位密码完成后用回调在Activity中获取到输入的密码并以Toast显示密码.效果图如下: 自定义view布局效果图及代码如下: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/

  • Android自定义View控件实现刷新效果

    三种得到LinearInflater的方法 a. LayoutInflater inflater = getLayoutInflater(); b. LayoutInflater localinflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); c. LayoutInflater inflater = LayoutInflater.from(context); onDraw 方法

随机推荐