android SectorMenuView底部導(dǎo)航扇形菜單的實現(xiàn)代碼
這次分析一個扇形菜單展開的自定義View, 也是我實習(xí)期間做的一個印象比較深刻的自定義View, 前后切換了很多種實現(xiàn)思路, 先看看效果展示
效果展示

效果分析
- 點擊圓形的FloatActionBar, 自身旋轉(zhuǎn)一定的角度
- 菜單像波紋一樣擴散開來
- 顯示我們添加的item
實現(xiàn)分析
使用adapter適配器去設(shè)置View, 用戶可自定義性強, 不過每次使用需要去設(shè)置Adapter, 較為繁瑣
直接調(diào)用ItemView, 將ImageView和TextView寫死, 用戶操作簡單, 但是缺乏可定制性(利他)
本次功能實現(xiàn)采用了方案 2
實現(xiàn)步驟
- 與氣泡拖拽類似, 新開啟一個Window進行自定義View的繪制
- 初始化時調(diào)用setWillNotDraw(false)方法, 強行啟動ViewGroup的繪制
- onMeasure中將寬高寫死
- 繪制背景
- 錨點為View的底部中心點
- 半徑為屏幕寬度一半的平方和的開方(注意這里不是屏幕的一半)
- 添加itemView, 在onLayout中去確定其位置
- 添加動畫效果
- 將相關(guān)接口暴露給外界
使用方式
BottomSectorMenuView.Converter(mFab)
.setToggleDuration(500, 800)
.setAnchorRotationAngle(135f)
.addMenuItem(R.drawable.icon_camera, "拍照") { Toast.makeText(this@MainActivity, "拍照", Toast.LENGTH_SHORT).show() }
.addMenuItem(R.drawable.icon_photo, "圖片") { Toast.makeText(this@MainActivity, "圖片", Toast.LENGTH_SHORT).show() }
.addMenuItem(R.drawable.icon_text, "文字") { Toast.makeText(this@MainActivity, "文字", Toast.LENGTH_SHORT).show() }
.addMenuItem(R.drawable.icon_video, "視頻") { Toast.makeText(this@MainActivity, "視頻", Toast.LENGTH_SHORT).show() }
.addMenuItem(R.drawable.icon_camera_shooting, "攝像") { Toast.makeText(this@MainActivity, "攝像", Toast.LENGTH_SHORT).show() }
.apply()
源碼實現(xiàn)
/**
* Email: frankchoochina@gmail.com
* Created by FrankChoo on 2017/10/9.
* Description: 底部扇形菜單, 通過Adapter添加Item
* 1. 調(diào)用openMenu打開菜單
* 2. 調(diào)用closeMenu關(guān)閉菜單
*/
public class SectorMenuView extends FrameLayout {
// 每個ItemView之間的角度差
private double mAngle;
// 圓心坐標
private Point mCenterPoint;
// ItemView到圓心的半徑
private float mMaxItemRadius;
private float mCurItemRadius;
// 背景圓的半徑
private float mMaxBkgRadius;
private float mCurBkgRadius;
private Paint mPaint;
private SectorMenuAdapter mAdapter;
private OnMenuOpenedListener mMenuOpenedListener;
private OnMenuClosedListener mMenuClosedListener;
public SectorMenuView(Context context) {
this(context, null);
}
public SectorMenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SectorMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 初始化畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setDither(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
// 設(shè)置背景圓繪制的半徑
int displayWidth = getResources().getDisplayMetrics().widthPixels;
mMaxBkgRadius = (int) Math.sqrt(Math.pow(displayWidth/2, 2.0) + Math.pow(displayWidth/2, 2.0));
// 開啟ViewGroup的繪制
setWillNotDraw(false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 這里直接將寬高寫死, 不支持Margin
int width = getResources().getDisplayMetrics().widthPixels;
int height = (int) Math.sqrt(Math.pow(width / 2, 2.0) + Math.pow(width / 2, 2.0));
setMeasuredDimension(width, height);
// 計算半徑
int realWidth = width - getPaddingRight() - getPaddingLeft();
int realHeight = height - getPaddingTop() - getPaddingBottom();
mMaxItemRadius = realWidth / 2;
// 計算圓心
int centerX = getPaddingLeft() + realWidth / 2;
int centerY = getPaddingTop() + realHeight;
mCenterPoint = new Point(centerX, centerY);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
double curAngle = Math.PI - mAngle * (i + 1);
int childCenterX = (int) (mCenterPoint.x + mCurItemRadius * Math.cos(curAngle));
int childCenterY = (int) (mCenterPoint.y - mCurItemRadius * Math.sin(curAngle));
child.layout(
childCenterX - child.getMeasuredWidth() / 2,
childCenterY - child.getMeasuredHeight() / 2,
childCenterX + child.getMeasuredWidth() / 2,
childCenterY + child.getMeasuredHeight() / 2
);
// 這里動態(tài)的去設(shè)置子View的透明度
child.setAlpha(mCurItemRadius / mMaxItemRadius);
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCurBkgRadius, mPaint);
super.onDraw(canvas);
}
public void setAdapter(SectorMenuAdapter adapter) {
mAdapter = adapter;
for (int i = 0; i < mAdapter.getCount(); i++) {
View child = mAdapter.getView(i, null, this);
addView(child);
}
mAngle = Math.PI / (mAdapter.getCount() + 1);
}
public void setBackgroudColor(@ColorInt int color) {
mPaint.setColor(color);
}
public void setBackgroundResource(@ColorRes int colorResId) {
mPaint.setColor(ContextCompat.getColor(getContext(), colorResId));
}
/**
* 打開菜單
*/
public void openMenu() {
if (mMaxItemRadius == 0) {
mMaxItemRadius = getResources().getDisplayMetrics().widthPixels / 2
- getPaddingRight() - getPaddingLeft();
}
// 背景動畫
ValueAnimator bkgAnim = ValueAnimator.ofFloat(0f, mMaxBkgRadius).setDuration(300);
bkgAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurBkgRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
// item的位置動畫
ValueAnimator itemTranslationAnim = ValueAnimator.ofFloat(0f, mMaxItemRadius).setDuration(300);
itemTranslationAnim.setInterpolator(new OvershootInterpolator(2f));
itemTranslationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurItemRadius = (float) animation.getAnimatedValue();
requestLayout();
}
});
// 動畫集合
final AnimatorSet set = new AnimatorSet();
set.playSequentially(bkgAnim, itemTranslationAnim);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
setAlpha(1f);
setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
if (mMenuOpenedListener != null) {
mMenuOpenedListener.opened();
}
}
});
set.start();
}
/**
* 關(guān)閉菜單
*/
public void closeMenu() {
// Item動畫
ValueAnimator itemViewAnim = ValueAnimator.ofFloat(mMaxItemRadius, 0f).setDuration(300);
itemViewAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurItemRadius = (float) animation.getAnimatedValue();
requestLayout();
}
});
itemViewAnim.setInterpolator(new AnticipateInterpolator(2f));
// 背景動畫
ValueAnimator backgroundAnim = ValueAnimator.ofFloat(mMaxBkgRadius, 0f).setDuration(300);
backgroundAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurBkgRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
// 這里設(shè)置了該View整體透明度的變化, 防止消失的背景不在錨點處, 顯示效果突兀
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).setDuration(250);
// 動畫集合
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(itemViewAnim).before(backgroundAnim);
animatorSet.play(backgroundAnim).with(alphaAnim);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mMenuClosedListener != null) {
mMenuClosedListener.closed();
}
setVisibility(View.INVISIBLE);
}
});
animatorSet.start();
}
public void setOnMenuOpenedListener(OnMenuOpenedListener listener) {
mMenuOpenedListener = listener;
}
public void setOnMenuClosedListener(OnMenuClosedListener listener) {
mMenuClosedListener = listener;
}
/**
* 供外界調(diào)用的Adapter
*/
public abstract static class SectorMenuAdapter extends BaseAdapter {
@Override
public long getItemId(int position) {
return 0;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return createView(position, parent);
}
protected abstract View createView(int position, ViewGroup parent);
@Override
public abstract int getCount();
}
public interface OnMenuOpenedListener {
void opened();
}
public interface OnMenuClosedListener {
void closed();
}
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android CoordinatorLayout高級用法之自定義Behavior
這篇文章主要介紹了Android CoordinatorLayout高級用法之自定義Behavior,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-02-02
Android App將數(shù)據(jù)寫入內(nèi)部存儲和外部存儲的示例
這篇文章主要介紹了Android App將數(shù)據(jù)寫入內(nèi)部存儲和外部存儲的示例,使用外部存儲即訪問并寫入SD卡,需要的朋友可以參考下2016-03-03
關(guān)于Android發(fā)送短信獲取送達報告的問題(推薦)
最近公司開發(fā)一個項目,要求app能夠發(fā)送短信并獲取送達報告。實現(xiàn)代碼非常簡單的,下面小編給大家分享關(guān)于Android發(fā)送短信獲取送達報告的問題,感興趣的朋友一起看看吧2017-03-03
Android自定義控件實現(xiàn)可多選課程日歷CalendarView
這篇文章主要為大家詳細介紹了Android自定義控件實現(xiàn)可多選課程日歷CalendarView,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Android開發(fā)實現(xiàn)應(yīng)用層面屏蔽狀態(tài)欄的方法小結(jié)
這篇文章主要介紹了Android開發(fā)實現(xiàn)應(yīng)用層面屏蔽狀態(tài)欄的方法,結(jié)合實例形式分析了Android屏蔽狀態(tài)欄的相關(guān)函數(shù)調(diào)用、權(quán)限控制及函數(shù)重寫等相關(guān)操作技巧,需要的朋友可以參考下2017-08-08
Android Studio 3.0 Gradle 配置變更
這篇文章主要介紹了Android Studio 3.0 Gradle 配置變更的相關(guān)知識,即多渠道打包變更和更改打包命名及路徑的代碼,感興趣的朋友跟隨腳本之家小編一起看看吧2018-03-03

