Android自定義View模仿虎撲直播界面的打賞按鈕功能
前言
作為一個(gè)資深籃球愛好者,我經(jīng)常會(huì)用虎撲app看比賽直播,后來(lái)注意到文字直播界面右下角加了兩個(gè)按鈕,可以在直播過(guò)程中送虎撲幣,為自己支持的球隊(duì)加油。
具體的效果如下圖所示:
我個(gè)人覺(jué)得挺好玩的,所以決定自己實(shí)現(xiàn)下這個(gè)按鈕,廢話不多說(shuō),先看實(shí)現(xiàn)的效果吧:
這個(gè)效果看起來(lái)和popupwindow差不多,但我是采用自定義view的方式來(lái)實(shí)現(xiàn),下面說(shuō)說(shuō)過(guò)程。
實(shí)現(xiàn)過(guò)程
首先從虎撲的效果可以看到,它這兩個(gè)按鈕時(shí)浮在整個(gè)界面之上的,所以它需要和FrameLayout結(jié)合使用,因此我讓它的寬度跟隨屏幕大小,高度根據(jù)dpi固定,它的實(shí)際尺寸時(shí)這樣的:
另外這個(gè)view初始化出來(lái)我們看到可以分為三塊,背景圓、圓內(nèi)文字、圓上方數(shù)字,所以正常狀態(tài)下,只需要在onDraw方法中畫出這三塊內(nèi)容即可。先在初始化方法中將自定義的屬性和畫筆以及初始化數(shù)據(jù)準(zhǔn)備好:
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"}; // 計(jì)算背景框改變的長(zhǎng)度,默認(rèn)是三個(gè)按鈕 mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);}
在onMeasure中測(cè)出view的寬度后,根據(jù)寬度計(jì)算出背景圓的圓心坐標(biāo)和一些相關(guān)的數(shù)據(jù)值。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); mWidth = getDefaultSize(widthSize, widthMeasureSpec); setMeasuredDimension(mWidth, mHeight); // 此時(shí)才測(cè)出了mWidth值,再計(jì)算圓心坐標(biāo)及相關(guān)值 cx = mWidth - mBigRadius; cy = mHeight - mBigRadius; // 大圓圓心 circle = new PointF(cx, cy); // 三個(gè)按鈕的圓心 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); // 初始的背景框的邊界即為大圓的四個(gè)邊界點(diǎn) top = cy - mBigRadius; bottom = cy + mBigRadius; }
因?yàn)檫@里面涉及到點(diǎn)擊按鈕展開和收縮的過(guò)程,所以我定義了如下幾種狀態(tài),只有在特定的狀態(tài)下才能進(jìn)行某些操作。
private int mState = STATE_NORMAL;//當(dāng)前展開收縮的狀態(tài) private boolean mIsRun = false;//是否正在展開或收縮 //正常狀態(tài) 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;
接下來(lái)就執(zhí)行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); }
圓上方的數(shù)字和圓內(nèi)的文字是整個(gè)過(guò)程中一直存在的,所以我將這兩個(gè)操作放在switch之外,正常狀態(tài)下繪制圓和之前兩部分文字,點(diǎn)擊展開時(shí)繪制背景框展開過(guò)程和文字,展開狀態(tài)下再次點(diǎn)擊繪制收縮過(guò)程和文字,當(dāng)然在繪制背景框的方法中也需要不斷繪制大圓,大圓也是一直存在的。
上面的繪制方法:
/** * 畫背景大圓 * @param canvas */ private void drawCircle(Canvas canvas) { left = cx - mBigRadius; right = cx + mBigRadius; canvas.drawCircle(cx, cy, mBigRadius, mBgPaint); } /** * 畫大圓上面表示金幣數(shù)的文字 * @param canvas */ private void drawCountText(Canvas canvas) { canvas.translate(0, -countMargin); //計(jì)算文字的寬度 float textWidth = mCountTextPaint.measureText(mCount, 0, mCount.length()); canvas.drawText(mCount, 0, mCount.length(), (2 * mBigRadius - textWidth - 35) / 2, 0.2f, mCountTextPaint); } /** * 畫大圓內(nèi)的文字 * @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)) { // 繪制第一個(gè)按鈕 canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); // 繪制第一個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[0], cx - (mBigRadius - mSmallRadius) - mChange, cy + 15, mTextPaint); } else if ((mChange > 2 * mSmallRadius + margin) && (mChange <= 4 * mSmallRadius + 2 * margin)) { // 繪制第一個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); // 繪制第一個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint); // 繪制第二個(gè)按鈕 canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); // 繪制第二個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[1], cx - mChange - 20, cy + 15, mTextPaint); } else if ((mChange > 4 * mSmallRadius + 2 * margin) && (mChange <= 6 * mSmallRadius + 3 * margin)) { // 繪制第一個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); // 繪制第一個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint); // 繪制第二個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint); // 繪制第二個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint); // 繪制第三個(gè)按鈕 canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint); // 繪制第三個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[2], cx - mChange - 34, cy + 15, mTextPaint); } else if (mChange > 6 * mSmallRadius + 3 * margin) { // 繪制第一個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - mSmallRadius - margin, cy, mSmallRadius, mBgPaint); // 繪制第一個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint); // 繪制第二個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint); // 繪制第二個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint); // 繪制第三個(gè)按鈕 canvas.drawCircle(cx - mBigRadius - 5 * mSmallRadius - 3 * margin, cy, mSmallRadius, mBgPaint); // 繪制第三個(gè)按鈕內(nèi)的文字 canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint); } drawCircle(canvas); }
然后是點(diǎn)擊事件的處理,只有觸摸點(diǎn)在大圓內(nèi)時(shí)才會(huì)觸發(fā)展開或收縮的操作,點(diǎn)擊小圓時(shí)提供了一個(gè)接口給外部調(diào)用。
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //如果點(diǎn)擊的時(shí)候動(dòng)畫在進(jìn)行,不處理 if (mIsRun) return true; PointF pointF = new PointF(event.getX(), event.getY()); if (isPointInCircle(pointF, circle, mBigRadius)) { //如果觸摸點(diǎn)在大圓內(nèi),根據(jù)彈出方向彈出或者收縮按鈕 if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) { //展開 mIsRun = true;//這是必須先設(shè)置true,因?yàn)閛nAnimationStart在onAnimationUpdate之后才調(diào)用 showPopMenu(); } else { //收縮 mIsRun = true; hidePopMenu(); } } else { //觸摸點(diǎn)不在大圓內(nèi) if (mState == STATE_EXPAND) { //如果是展開狀態(tài) 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); }
展開和收縮的動(dòng)畫是改變背景框的寬度屬性的動(dòng)畫,并監(jiān)聽這個(gè)屬性動(dòng)畫,在寬度值改變的過(guò)程中去重新繪制整個(gè)view。因?yàn)橐婚_始我就確定了大圓小圓的半徑和小圓與背景框之間的間距,所以初始化時(shí)已經(jīng)計(jì)算好了背景框的寬度:
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; //動(dòng)畫結(jié)束后設(shè)置狀態(tài)為展開 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; //動(dòng)畫結(jié)束后設(shè)置狀態(tài)為收縮 mState = STATE_SHRINK; } }); animator.setDuration(500); animator.start(); } }
這個(gè)過(guò)程看起來(lái)是彈出或收縮,實(shí)際上寬度值每改變一點(diǎn),就將所有的組件重繪一次,只是文字和大圓等內(nèi)容的尺寸及位置都沒(méi)有變化,只有背景框的寬度值在變,所以才有這種效果。
在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="熱火無(wú)敵" 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(); } });
大致實(shí)現(xiàn)過(guò)程就是這樣,與原始效果還是有點(diǎn)區(qū)別,我這個(gè)還有很多瑕疵,比如文字的位置居中問(wèn)題,彈出或收縮時(shí),小圓內(nèi)的文字的旋轉(zhuǎn)動(dòng)畫我沒(méi)有實(shí)現(xiàn)。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)各位Android開發(fā)者們能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android直播app送禮物連擊動(dòng)畫效果(實(shí)例代碼)
- Android 實(shí)現(xiàn)仿網(wǎng)絡(luò)直播彈幕功能詳解及實(shí)例
- Android實(shí)現(xiàn)炫酷的網(wǎng)絡(luò)直播彈幕功能
- Android仿斗魚直播的彈幕效果
- Android高級(jí)UI特效仿直播點(diǎn)贊動(dòng)畫效果
- android實(shí)現(xiàn)直播點(diǎn)贊飄心動(dòng)畫效果
- Android仿直播特效之點(diǎn)贊飄心效果
- Android控件實(shí)現(xiàn)直播App點(diǎn)贊飄心動(dòng)畫
- Android貝塞爾曲線實(shí)現(xiàn)直播點(diǎn)贊效果
- Android實(shí)現(xiàn)直播聊天區(qū)域中頂部的漸變效果
相關(guān)文章
Android實(shí)現(xiàn)簡(jiǎn)易記事本
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)簡(jiǎn)易記事本,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07android利用service完成計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了android利用service完成計(jì)時(shí)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Android自定義Gallery控件實(shí)現(xiàn)3D圖片瀏覽器
這篇文章主要介紹了Android自定義Gallery控件實(shí)現(xiàn)3D圖片瀏覽器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04Android Studio使用recyclerview實(shí)現(xiàn)展開和折疊功能(在之前的微信頁(yè)面基礎(chǔ)之上)
這篇文章主要介紹了Android Studio使用recyclerview實(shí)現(xiàn)展開和折疊(在之前的微信頁(yè)面基礎(chǔ)之上),本文通過(guò)截圖實(shí)例代碼給大家講解的非常詳細(xì),需要的朋友可以參考下2020-03-03Android開發(fā)簡(jiǎn)單實(shí)現(xiàn)搖動(dòng)動(dòng)畫的方法
這篇文章主要介紹了Android開發(fā)簡(jiǎn)單實(shí)現(xiàn)搖動(dòng)動(dòng)畫的方法,結(jié)合實(shí)例形式分析了Android搖動(dòng)動(dòng)畫的布局與功能簡(jiǎn)單實(shí)現(xiàn)方法,需要的朋友可以參考下2017-10-10Android布局之絕對(duì)布局AbsoluteLayout詳解
這篇文章主要為大家詳細(xì)介紹了Android布局之絕對(duì)布局AbsoluteLayout的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10Android?Studio支持安卓手機(jī)投屏功能詳解
這篇文章主要給大家介紹了關(guān)于Android?Studio支持安卓手機(jī)投屏功能的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)有需要的朋友具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-01-01Android實(shí)現(xiàn)未讀消息小紅點(diǎn)顯示實(shí)例
大家好,本篇文章主要講的是Android實(shí)現(xiàn)未讀消息小紅點(diǎn)顯示實(shí)例,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-02-02