前言
作為一個資深籃球愛好者,我經常會用虎撲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中的使用:
<LinearLayoutandroid: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.HoopViewandroid: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.HoopViewandroid: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開發者們能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。
新聞熱點
疑難解答