自定義view-仿虎撲直播比賽界面的打賞按鈕

作為一個資深籃球愛好者,我經(jīng)常會用虎撲app看比賽直播招刨,后來注意到文字直播界面右下角加了兩個按鈕霎俩,可以在直播過程中送虎撲幣,為自己支持的球隊加油沉眶,具體的效果如下圖所示:

1701061

我個人覺得挺好玩的打却,所以決定自己實現(xiàn)下這個按鈕,廢話不多說谎倔,先看實現(xiàn)的效果吧:

hoopview

這個效果看起來和popupwindow差不多柳击,但我是采用自定義view的方式來實現(xiàn),下面說說過程片习。

首先從虎撲的效果可以看到捌肴,它這兩個按鈕時浮在整個界面之上的,所以它需要和FrameLayout結(jié)合使用藕咏,因此我讓它的寬度跟隨屏幕大小状知,高度根據(jù)dpi固定,它的實際尺寸時這樣的:

1701062

另外這個view初始化出來我們看到可以分為三塊孽查,背景圓饥悴、圓內(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"};
    // 計算背景框改變的長度洲胖,默認(rèn)是三個按鈕
    mChangeWidth = (int) (2 * mSmallRadius * 3 + 4 * margin);

}

在onMeasure中測出view的寬度后济榨,根據(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);

    // 此時才測出了mWidth值绿映,再計算圓心坐標(biāo)及相關(guān)值
    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;
}

因為這里面涉及到點擊按鈕展開和收縮的過程擒滑,所以我定義了如下幾種狀態(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;

接下來就執(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)的文字是整個過程中一直存在的藻糖,所以我將這兩個操作放在switch之外,正常狀態(tài)下繪制圓和之前兩部分文字库车,點擊展開時繪制背景框展開過程和文字巨柒,展開狀態(tài)下再次點擊繪制收縮過程和文字,當(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);
    //計算文字的寬度
    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)) {
        // 繪制第一個按鈕
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 繪制第一個按鈕內(nèi)的文字
        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);
        // 繪制第一個按鈕內(nèi)的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 20, cy + 15, mTextPaint);

        // 繪制第二個按鈕
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 繪制第二個按鈕內(nèi)的文字
        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);
        // 繪制第一個按鈕內(nèi)的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

        // 繪制第二個按鈕
        canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
        // 繪制第二個按鈕內(nèi)的文字
        canvas.drawText(mDatas[1], cx - mBigRadius - 3 * mSmallRadius - 2 * margin - 25, cy + 15, mTextPaint);

        // 繪制第三個按鈕
        canvas.drawCircle(cx - mChange, cy, mSmallRadius, mBgPaint);
        // 繪制第三個按鈕內(nèi)的文字
        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);
        // 繪制第一個按鈕內(nèi)的文字
        canvas.drawText(mDatas[0], cx - mBigRadius - mSmallRadius - margin - 16, cy + 15, mTextPaint);

        // 繪制第二個按鈕
        canvas.drawCircle(cx - mBigRadius - 3 * mSmallRadius - 2 * margin, cy, mSmallRadius, mBgPaint);
        // 繪制第二個按鈕內(nèi)的文字
        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);
        // 繪制第三個按鈕內(nèi)的文字
        canvas.drawText(mDatas[2], cx - mBigRadius - 5 * mSmallRadius - 3 * margin - 34, cy + 15, mTextPaint);
    }
    drawCircle(canvas);

}

然后是點擊事件的處理,只有觸摸點在大圓內(nèi)時才會觸發(fā)展開或收縮的操作珍坊,點擊小圓時提供了一個接口給外部調(diào)用牺勾。

@Override public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            //如果點擊的時候動畫在進(jìn)行,不處理
            if (mIsRun) return true;
            PointF pointF = new PointF(event.getX(), event.getY());
            if (isPointInCircle(pointF, circle, mBigRadius)) { //如果觸摸點在大圓內(nèi)阵漏,根據(jù)彈出方向彈出或者收縮按鈕
                if ((mState == STATE_SHRINK || mState == STATE_NORMAL) && !mIsRun) {
                    //展開
                    mIsRun = true;//這是必須先設(shè)置true驻民,因為onAnimationStart在onAnimationUpdate之后才調(diào)用
                    showPopMenu();
                } else {
                    //收縮
                    mIsRun = true;
                    hidePopMenu();
                }
            } else { //觸摸點不在大圓內(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);
}

展開和收縮的動畫是改變背景框的寬度屬性的動畫,并監(jiān)聽這個屬性動畫履怯,在寬度值改變的過程中去重新繪制整個view回还。因為一開始我就確定了大圓小圓的半徑和小圓與背景框之間的間距,所以初始化時已經(jīng)計算好了背景框的寬度:

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;
                //動畫結(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;
                //動畫結(jié)束后設(shè)置狀態(tài)為收縮
                mState = STATE_SHRINK;
            }
        });
        animator.setDuration(500);
        animator.start();
    }
}

這個過程看起來是彈出或收縮叹洲,實際上寬度值每改變一點懦趋,就將所有的組件重繪一次,只是文字和大圓等內(nè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="熱火無敵"
        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();
    }
});

大致實現(xiàn)過程就是這樣糙捺,與原始效果還是有點區(qū)別诫咱,我這個還有很多瑕疵,比如文字的位置居中問題洪灯,彈出或收縮時坎缭,小圓內(nèi)的文字的旋轉(zhuǎn)動畫我沒有實現(xiàn)。

代碼地址:
https://github.com/shenhuniurou/HoopCustomView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末签钩,一起剝皮案震驚了整個濱河市掏呼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铅檩,老刑警劉巖憎夷,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異昧旨,居然都是意外死亡拾给,警方通過查閱死者的電腦和手機(jī)祥得,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒋得,“玉大人级及,你說我怎么就攤上這事《钛茫” “怎么了饮焦?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵氧卧,是天一觀的道長宴卖。 經(jīng)常有香客問我,道長恶复,這世上最難降的妖魔是什么疏之? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮暇咆,結(jié)果婚禮上锋爪,老公的妹妹穿的比我還像新娘。我一直安慰自己爸业,他們只是感情好其骄,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扯旷,像睡著了一般拯爽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钧忽,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天毯炮,我揣著相機(jī)與錄音,去河邊找鬼耸黑。 笑死桃煎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的大刊。 我是一名探鬼主播为迈,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缺菌!你這毒婦竟也來了葫辐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤伴郁,失蹤者是張志新(化名)和其女友劉穎耿战,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焊傅,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡昆箕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年鸦列,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹏倘。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡薯嗤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纤泵,到底是詐尸還是另有隱情骆姐,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布捏题,位于F島的核電站玻褪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏公荧。R本人自食惡果不足惜带射,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望循狰。 院中可真熱鬧窟社,春花似錦、人聲如沸绪钥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽程腹。三九已至匣吊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寸潦,已是汗流浹背色鸳。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留见转,地道東北人缕碎。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像池户,于是被迫代替她去往敵國和親咏雌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 NBA全明星周末校焦!重要事情說一遍赊抖!作為一個經(jīng)常在虎撲上看NBA直播的小球迷,老早就留意到了打賞加油的小控件寨典,...
    dengzq閱讀 1,001評論 0 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,133評論 25 707
  • 主題一:關(guān)于婚姻的幾個核心主題--1氛雪、經(jīng)濟(jì)。雙方收入高低耸成、金錢的支配權(quán)(各花各的报亩、一方主導(dǎo)浴鸿、共同賬戶及商議)、...
    湯蔚閱讀 425評論 0 3
  • 以前看到過一句話弦追,一直都在腦中岳链,難以忘懷,越是長大劲件,越是能深刻感覺:“越是深夜掸哑,越是孤獨”。我想大概越是夜深人靜就...
    朝露2閱讀 203評論 1 1
  • 網(wǎng)絡(luò)時代零远,越來越多的人適應(yīng)苗分、甚至喜歡上了“快餐式”消費。隨之而來的影響牵辣,我們沒有了“英雄”摔癣,滿文具店的是一次性水筆...
    浩哥隨思錄閱讀 305評論 0 0