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

justthink 7年前發布 | 10K 次閱讀 安卓開發 Android開發 移動開發

作為一個資深籃球愛好者,我經常會用虎撲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(); 
 
} 
 
});  

大致實現過程就是這樣,與原始效果還是有點區別,我這個還有很多瑕疵,比如文字的位置居中問題,彈出或收縮時,小圓內的文字的旋轉動畫我沒有實現。

 

來自:http://mobile.51cto.com/android-536120.htm

 

 本文由用戶 justthink 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!