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开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

时间: 2024-09-17 04:12:08

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

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

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

Android自定义View制作仪表盘界面_Android

前言 最近我跟自定义View杠上了,甚至说有点上瘾到走火入魔了.身为菜鸟的我自然要查阅大量的资料,学习大神们的代码,这不,前两天正好在郭神在微信公众号里推送一片自定义控件的文章--一步步实现精美的钟表界面.正适合我这种菜鸟来学习,闲着没事,我就差不多依葫芦画瓢也写了一个自定义表盘View,现在纯粹最为笔记记录下来.先展示下效果图: 下面进入正题 自定义表盘属性 老规矩,先在attrs文件里添加表盘自定义属性 <declare-styleable name="WatchView"&

界面-Android自定义View画笔颜色改变问题(画笔颜色改变了,但是实际上ABCD这些字母的颜色没变)

问题描述 Android自定义View画笔颜色改变问题(画笔颜色改变了,但是实际上ABCD这些字母的颜色没变) package com.eage.tbw.view; import com.eage.tbw.R; import com.eage.tbw.R.color; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import andro

Android自定义View——自定义搜索框(SearchView)

概述 在Android开发中,当系统数据项比较多时,常常会在app添加搜索功能,方便用户能快速获得需要的数据.搜索栏对于我们并不陌生,在许多app都能见到它,比如豌豆荚 在某些情况下,我们希望我们的自动补全信息可以不只是纯文本,还可以像豌豆荚这样,能显示相应的图片和其他数据信息,因此Android给我们提供的AutoCompleteTextView往往就不够用,在大多情况下我们都需要自己去实现搜索框. 分析 根据上面这张图,简单分析一下自定义搜索框的结构与功能,有  1. 搜索界面大致由三部门组

我的Android进阶之旅------&amp;gt;Android自定义View实现带数字的进度条(NumberProgressBar)

今天在Github上面看到一个来自于 daimajia所写的关于Android自定义View实现带数字的进度条(NumberProgressBar)的精彩案例,在这里分享给大家一起来学习学习!同时感谢daimajia的开源奉献! 第一步.效果展示 图1.蓝色的进度条 图2.红色的进度条 图3.多条颜色不同的进度条 图4.多条颜色不同的进度条 版权声明:本文为[欧阳鹏]原创文章,欢迎转载,转载请注明出处! [http://blog.csdn.net/ouyang_peng/article/deta

Android自定义View实现左右滑动选择出生年份_Android

自定义view的第三篇,模仿的是微博运动界面的个人出生日期设置view,先看看我的效果图: 支持设置初始年份,左右滑动选择出生年份,对应的TextView的值也会改变.这个动画效果弄了好久,感觉还是比较生硬,与微博那个还是有点区别.大家有改进的方案,欢迎一起交流. 自定义View四部曲,这里依旧是这个套路,看看怎么实现的. 1.自定义view的属性: 在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性以及声明我们的整个样式. <?xml version="1.

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

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

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

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

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

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