自定义View-仿虎扑直播比赛界面的打赏按钮

移动开发 Android
作为一个资深篮球爱好者,我经常会用虎扑app看比赛直播,后来注意到文字直播界面右下角加了两个按钮,可以在直播过程中送虎扑币,为自己支持的球队加油,我个人觉得挺好玩的,所以决定自己实现下这个按钮。

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

 

 

自定义View-仿虎扑直播比赛界面的打赏按钮  

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

 

 

自定义View-仿虎扑直播比赛界面的打赏按钮  

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

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

 

 

自定义View-仿虎扑直播比赛界面的打赏按钮  

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

  1. private void init(Context context, AttributeSet attrs) { 
  2. //获取自定义属性 
  3. TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HoopView); 
  4. mThemeColor = typedArray.getColor(R.styleable.HoopView_theme_color, Color.YELLOW); 
  5. mText = typedArray.getString(R.styleable.HoopView_text); 
  6. mCount = typedArray.getString(R.styleable.HoopView_count); 
  7.  
  8. mBgPaint = new Paint(); 
  9. mBgPaint.setAntiAlias(true); 
  10. mBgPaint.setColor(mThemeColor); 
  11. mBgPaint.setAlpha(190); 
  12. mBgPaint.setStyle(Paint.Style.FILL); 
  13.  
  14. mPopPaint = new Paint(); 
  15. mPopPaint.setAntiAlias(true); 
  16. mPopPaint.setColor(Color.LTGRAY); 
  17. mPopPaint.setAlpha(190); 
  18. mPopPaint.setStyle(Paint.Style.FILL_AND_STROKE); 
  19.  
  20. mTextPaint = new TextPaint(); 
  21. mTextPaint.setAntiAlias(true); 
  22. mTextPaint.setColor(mTextColor); 
  23. mTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_text_size)); 
  24.  
  25. mCountTextPaint = new TextPaint(); 
  26. mCountTextPaint.setAntiAlias(true); 
  27. mCountTextPaint.setColor(mThemeColor); 
  28. mCountTextPaint.setTextSize(context.getResources().getDimension(R.dimen.hoop_count_text_size)); 
  29.  
  30. typedArray.recycle(); 
  31.  
  32. mBigRadius = context.getResources().getDimension(R.dimen.hoop_big_circle_radius); 
  33. mSmallRadius = context.getResources().getDimension(R.dimen.hoop_small_circle_radius); 
  34. margin = (int) context.getResources().getDimension(R.dimen.hoop_margin); 
  35. mHeight = (int) context.getResources().getDimension(R.dimen.hoop_view_height); 
  36. countMargin = (int) context.getResources().getDimension(R.dimen.hoop_count_margin); 
  37.  
  38. mDatas = new String[] {"1""10""100"}; 
  39. // 计算背景框改变的长度,默认是三个按钮 
  40. mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);}  

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

  1. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  2.  
  3. int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
  4.  
  5. mWidth = getDefaultSize(widthSize, widthMeasureSpec); 
  6.  
  7. setMeasuredDimension(mWidth, mHeight); 
  8.  
  9.  
  10. // 此时才测出了mWidth值,再计算圆心坐标及相关值 
  11.  
  12. cx = mWidth - mBigRadius; 
  13.  
  14. cy = mHeight - mBigRadius; 
  15.  
  16. // 大圆圆心 
  17.  
  18. circle = new PointF(cx, cy); 
  19.  
  20. // 三个按钮的圆心 
  21.  
  22. circleOne = new PointF(cx - mBigRadius - mSmallRadius - margin, cy); 
  23.  
  24. circleTwo = new PointF(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy); 
  25.  
  26. circleThree = new PointF(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy); 
  27.  
  28. // 初始的背景框的边界即为大圆的四个边界点 
  29.  
  30. top = cy - mBigRadius; 
  31.  
  32. bottom = cy + mBigRadius; 
  33.  
  34.  

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

  1. private int mState = STATE_NORMAL;//当前展开收缩的状态 
  2.  
  3. private boolean mIsRun = false;//是否正在展开或收缩 
  4.  
  5.  
  6. //正常状态 
  7.  
  8. public static final int STATE_NORMAL = 0; 
  9.  
  10. //按钮展开 
  11.  
  12. public static final int STATE_EXPAND = 1; 
  13.  
  14. //按钮收缩 
  15.  
  16. public static final int STATE_SHRINK = 2; 
  17.  
  18. //正在展开 
  19.  
  20. public static final int STATE_EXPANDING = 3; 
  21.  
  22. //正在收缩 
  23.  
  24. public static final int STATE_SHRINKING = 4;  

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

  1. @Override protected void onDraw(Canvas canvas) { 
  2.  
  3. switch (mState) { 
  4.  
  5. case STATE_NORMAL: 
  6.  
  7. drawCircle(canvas); 
  8.  
  9. break; 
  10.  
  11. case STATE_SHRINK: 
  12.  
  13. case STATE_SHRINKING: 
  14.  
  15. drawBackground(canvas); 
  16.  
  17. break; 
  18.  
  19. case STATE_EXPAND: 
  20.  
  21. case STATE_EXPANDING: 
  22.  
  23. drawBackground(canvas); 
  24.  
  25. break; 
  26.  
  27.  
  28. drawCircleText(canvas); 
  29.  
  30. drawCountText(canvas); 
  31.  
  32.  

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

上面的绘制方法:

  1. /** 
  2.  
  3.  * 画背景大圆 
  4.  
  5.  * @param canvas 
  6.  
  7.  */ 
  8.  
  9. private void drawCircle(Canvas canvas) { 
  10.  
  11. left = cx - mBigRadius; 
  12.  
  13. right = cx + mBigRadius; 
  14.  
  15. canvas.drawCircle(cx, cy, mBigRadius, mBgPaint); 
  16.  
  17.  
  18.  
  19.  
  20. /** 
  21.  
  22.  * 画大圆上面表示金币数的文字 
  23.  
  24.  * @param canvas 
  25.  
  26.  */ 
  27.  
  28. private void drawCountText(Canvas canvas) { 
  29.  
  30. canvas.translate(0, -countMargin); 
  31.  
  32. //计算文字的宽度 
  33.  
  34. float textWidth = mCountTextPaint.measureText(mCount, 0, mCount.length()); 
  35.  
  36. canvas.drawText(mCount, 0, mCount.length(), (2 * mBigRadius - textWidth - 35) / 2, 0.2f, mCountTextPaint); 
  37.  
  38.  
  39.  
  40.  
  41. /** 
  42.  
  43.  * 画大圆内的文字 
  44.  
  45.  * @param canvas 
  46.  
  47.  */ 
  48.  
  49. private void drawCircleText(Canvas canvas) { 
  50.  
  51. StaticLayout layout = new StaticLayout(mText, mTextPaint, (int) (mBigRadius * Math.sqrt(2)), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, true); 
  52.  
  53. canvas.translate(mWidth - mBigRadius * 1.707f, mHeight - mBigRadius * 1.707f); 
  54.  
  55. layout.draw(canvas); 
  56.  
  57. canvas.save(); 
  58.  
  59.  
  60.  
  61.  
  62. /** 
  63.  
  64.  * 画背景框展开和收缩 
  65.  
  66.  * @param canvas 
  67.  
  68.  */ 
  69.  
  70. private void drawBackground(Canvas canvas) { 
  71.  
  72. left = cx - mBigRadius - mChange; 
  73.  
  74. right = cx + mBigRadius; 
  75.  
  76. canvas.drawRoundRect(lefttopright, bottom, mBigRadius, mBigRadius, mPopPaint); 
  77.  
  78. if ((mChange > 0) && (mChange <= 2 * mSmallRadius + margin)) { 
  79.  
  80. // 绘制***个按钮 
  81.  
  82. canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); 
  83.  
  84. // 绘制***个按钮内的文字 
  85.  
  86. canvas.drawText(mDatas[0], cx - (mBigRadius - mSmallRadius) - mChange, cy + 15, mTextPaint); 
  87.  
  88. else if ((mChange > 2 * mSmallRadius + margin) && (mChange <= 4 * mSmallRadius + 2 * margin)) { 
  89.  
  90. // 绘制***个按钮 
  91.  
  92. canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); 
  93.  
  94. // 绘制***个按钮内的文字 
  95.  
  96. canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint); 
  97.  
  98.  
  99. // 绘制第二个按钮 
  100.  
  101. canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); 
  102.  
  103. // 绘制第二个按钮内的文字 
  104.  
  105. canvas.drawText(mDatas[1], cx - mChange - 20, cy + 15, mTextPaint); 
  106.  
  107. else if ((mChange > 4 * mSmallRadius + 2 * margin) && (mChange <= 6 * mSmallRadius + 3 * margin)) { 
  108.  
  109. // 绘制***个按钮 
  110.  
  111. canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); 
  112.  
  113. // 绘制***个按钮内的文字 
  114.  
  115. canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint); 
  116.  
  117.  
  118. // 绘制第二个按钮 
  119.  
  120. canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint); 
  121.  
  122. // 绘制第二个按钮内的文字 
  123.  
  124. canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint); 
  125.  
  126.  
  127. // 绘制第三个按钮 
  128.  
  129. canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); 
  130.  
  131. // 绘制第三个按钮内的文字 
  132.  
  133. canvas.drawText(mDatas[2], cx - mChange - 34, cy + 15, mTextPaint); 
  134.  
  135. else  if (mChange > 6 * mSmallRadius + 3 * margin) { 
  136.  
  137. // 绘制***个按钮 
  138.  
  139. canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); 
  140.  
  141. // 绘制***个按钮内的文字 
  142.  
  143. canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint); 
  144.  
  145.  
  146. // 绘制第二个按钮 
  147.  
  148. canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint); 
  149.  
  150. // 绘制第二个按钮内的文字 
  151.  
  152. canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint); 
  153.  
  154.  
  155. // 绘制第三个按钮 
  156.  
  157. canvas.drawCircle(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy, mSmallRadius, mBgPaint); 
  158.  
  159. // 绘制第三个按钮内的文字 
  160.  
  161. canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint); 
  162.  
  163.  
  164. drawCircle(canvas); 
  165.  
  166.  
  167.  

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

  1. @Override public boolean onTouchEvent(MotionEvent event) { 
  2.  
  3. int action = event.getAction(); 
  4.  
  5. switch (action) { 
  6.  
  7. case MotionEvent.ACTION_DOWN: 
  8.  
  9. //如果点击的时候动画在进行,不处理 
  10.  
  11. if (mIsRun) return true
  12.  
  13. PointF pointF = new PointF(event.getX(), event.getY()); 
  14.  
  15. if (isPointInCircle(pointF, circle, mBigRadius)) { //如果触摸点在大圆内,根据弹出方向弹出或者收缩按钮 
  16.  
  17. if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) { 
  18.  
  19. //展开 
  20.  
  21. mIsRun = true;//这是必须先设置true,因为onAnimationStart在onAnimationUpdate之后才调用 
  22.  
  23. showPopMenu(); 
  24.  
  25. else { 
  26.  
  27. //收缩 
  28.  
  29. mIsRun = true
  30.  
  31. hidePopMenu(); 
  32.  
  33.  
  34. else { //触摸点不在大圆内 
  35.  
  36. if (mState == STATE_EXPAND) { //如果是展开状态 
  37.  
  38. if (isPointInCircle(pointF, circleOne, mSmallRadius)) { 
  39.  
  40. listener.clickButton(this, Integer.parseInt(mDatas[0])); 
  41.  
  42. else if (isPointInCircle(pointF, circleTwo, mSmallRadius)) { 
  43.  
  44. listener.clickButton(this, Integer.parseInt(mDatas[1])); 
  45.  
  46. else if (isPointInCircle(pointF, circleThree, mSmallRadius)) { 
  47.  
  48. listener.clickButton(this, Integer.parseInt(mDatas[2])); 
  49.  
  50.  
  51. mIsRun = true
  52.  
  53. hidePopMenu(); 
  54.  
  55.  
  56.  
  57. break; 
  58.  
  59.  
  60. return super.onTouchEvent(event); 
  61.  
  62.  

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

  1. mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);  
  1. /** 
  2.  
  3.  * 弹出背景框 
  4.  
  5.  */ 
  6.  
  7. private void showPopMenu() { 
  8.  
  9. if (mState == STATE_SHRINK || mState == STATE_NORMAL) { 
  10.  
  11. ValueAnimator animator = ValueAnimator.ofInt(0, mChangeWidth); 
  12.  
  13. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
  14.  
  15. @Override public void onAnimationUpdate(ValueAnimator animation) { 
  16.  
  17. if (mIsRun) { 
  18.  
  19. mChange = (int) animation.getAnimatedValue(); 
  20.  
  21. invalidate(); 
  22.  
  23. else { 
  24.  
  25. animation.cancel(); 
  26.  
  27. mState = STATE_NORMAL; 
  28.  
  29.  
  30.  
  31. }); 
  32.  
  33. animator.addListener(new AnimatorListenerAdapter() { 
  34.  
  35. @Override public void onAnimationStart(Animator animation) { 
  36.  
  37. super.onAnimationStart(animation); 
  38.  
  39. mIsRun = true
  40.  
  41. mState = STATE_EXPANDING; 
  42.  
  43.  
  44.  
  45.  
  46. @Override public void onAnimationCancel(Animator animation) { 
  47.  
  48. super.onAnimationCancel(animation); 
  49.  
  50. mIsRun = false
  51.  
  52. mState = STATE_NORMAL; 
  53.  
  54.  
  55.  
  56.  
  57. @Override public void onAnimationEnd(Animator animation) { 
  58.  
  59. super.onAnimationEnd(animation); 
  60.  
  61. mIsRun = false
  62.  
  63. //动画结束后设置状态为展开 
  64.  
  65. mState = STATE_EXPAND; 
  66.  
  67.  
  68. }); 
  69.  
  70. animator.setDuration(500); 
  71.  
  72. animator.start(); 
  73.  
  74.  
  75.   
  1. /** 
  2.  
  3.  * 隐藏弹出框 
  4.  
  5.  */ 
  6.  
  7. private void hidePopMenu() { 
  8.  
  9. if (mState == STATE_EXPAND) { 
  10.  
  11. ValueAnimator animator = ValueAnimator.ofInt(mChangeWidth, 0); 
  12.  
  13. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
  14.  
  15. @Override public void onAnimationUpdate(ValueAnimator animation) { 
  16.  
  17. if (mIsRun) { 
  18.  
  19. mChange = (int) animation.getAnimatedValue(); 
  20.  
  21. invalidate(); 
  22.  
  23. else { 
  24.  
  25. animation.cancel(); 
  26.  
  27.  
  28.  
  29. }); 
  30.  
  31. animator.addListener(new AnimatorListenerAdapter() { 
  32.  
  33. @Override public void onAnimationStart(Animator animation) { 
  34.  
  35. super.onAnimationStart(animation); 
  36.  
  37. mIsRun = true
  38.  
  39. mState = STATE_SHRINKING; 
  40.  
  41.  
  42.  
  43.  
  44. @Override public void onAnimationCancel(Animator animation) { 
  45.  
  46. super.onAnimationCancel(animation); 
  47.  
  48. mIsRun = false
  49.  
  50. mState = STATE_EXPAND; 
  51.  
  52.  
  53.  
  54.  
  55. @Override public void onAnimationEnd(Animator animation) { 
  56.  
  57. super.onAnimationEnd(animation); 
  58.  
  59. mIsRun = false
  60.  
  61. //动画结束后设置状态为收缩 
  62.  
  63. mState = STATE_SHRINK; 
  64.  
  65.  
  66. }); 
  67.  
  68. animator.setDuration(500); 
  69.  
  70. animator.start(); 
  71.  
  72.  
  73.  

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

在xml中的使用: 

  1. <LinearLayout 
  2.  
  3. android:layout_width="match_parent" 
  4.  
  5. android:layout_height="wrap_content" 
  6.  
  7. android:layout_alignParentBottom="true" 
  8.  
  9. android:layout_marginBottom="20dp" 
  10.  
  11. android:layout_alignParentRight="true" 
  12.  
  13. android:orientation="vertical"
  14.  
  15.  
  16. <com.xx.hoopcustomview.HoopView 
  17.  
  18. android:id="@+id/hoopview1" 
  19.  
  20. android:layout_width="match_parent" 
  21.  
  22. android:layout_height="wrap_content" 
  23.  
  24. android:layout_marginRight="10dp" 
  25.  
  26. app:text="支持火箭" 
  27.  
  28. app:count="1358" 
  29.  
  30. app:theme_color="#31A129"/> 
  31.  
  32.  
  33. <com.xx.hoopcustomview.HoopView 
  34.  
  35. android:id="@+id/hoopview2" 
  36.  
  37. android:layout_width="match_parent" 
  38.  
  39. android:layout_height="wrap_content" 
  40.  
  41. android:layout_marginRight="10dp" 
  42.  
  43. app:text="热火无敌" 
  44.  
  45. app:count="251" 
  46.  
  47. app:theme_color="#F49C11"/> 
  48.  
  49. </LinearLayout>  

activity中使用: 

  1. hoopview1 = (HoopView) findViewById(R.id.hoopview1); 
  2.  
  3. hoopview1.setOnClickButtonListener(new HoopView.OnClickButtonListener() { 
  4.  
  5. @Override public void clickButton(View viewint num) { 
  6.  
  7. Toast.makeText(MainActivity.this, "hoopview1增加了" + num, Toast.LENGTH_SHORT).show(); 
  8.  
  9.  
  10. });  

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

责任编辑:庞桂玉 来源: 安卓巴士Android开发者门户
相关推荐

2016-12-26 15:25:59

Android自定义View

2016-11-16 21:55:55

源码分析自定义view androi

2016-04-12 10:07:55

AndroidViewList

2021-10-26 10:07:02

鸿蒙HarmonyOS应用

2011-08-02 11:17:13

iOS开发 View

2010-02-07 14:02:16

Android 界面

2017-10-25 14:07:54

APPiOSxcode

2017-03-02 13:33:19

Android自定义View

2024-09-11 14:46:48

C#旋转按钮

2017-03-14 15:09:18

AndroidView圆形进度条

2009-08-05 17:15:27

C#自定义按钮

2013-05-20 17:33:44

Android游戏开发自定义View

2012-05-18 10:52:20

TitaniumAndroid模块自定义View模块

2013-01-06 10:43:54

Android开发View特效

2010-04-28 11:14:20

Windows 7桌面

2011-08-12 18:18:03

iPhone开发UIPageContr按钮

2022-10-25 15:12:24

自定义组件鸿蒙

2015-02-12 15:33:43

微信SDK

2021-11-04 09:55:50

鸿蒙HarmonyOS应用

2013-03-28 10:58:30

自定义Android界android
点赞
收藏

51CTO技术栈公众号