Android 自定義組件衛(wèi)星菜單的實現(xiàn)
衛(wèi)星菜單 ArcMenu
相信大家接觸安卓,從新手到入門的過渡,就應(yīng)該會了解到衛(wèi)星菜單、抽屜、Xutils、Coolmenu、一些大神封裝好的一些組件。這些組件在 Github 上面很容易搜得到,但是有時候打開會發(fā)現(xiàn)看不懂里面的代碼,包括一些方法和函數(shù) 。。。。。
首先先上效果圖:
實現(xiàn)效果

首先如果要想自定義組件
1.那么第一件事就是賦予自定義組件的屬性,從效果圖上看出,該組件可以存在屏幕的各個角落點,那么位置是其屬性之一。
2.既然是衛(wèi)星菜單,那么主按鈕和其附屬的小按鈕之間的圍繞半徑也應(yīng)該作為其參數(shù)之一。
3.右圖得出,該組件包含很多按鈕,主按鈕和附屬按鈕,那么這個組件應(yīng)該繼承 ViewGroup。
一、定義衛(wèi)星菜單的屬性在 values 包下建立 attr 的 XML 文件,賦予組件位置屬性,和半徑屬性。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 位置屬性-->
<attr name="position">
<enum name="left_top" value="0" />
<enum name="left_bottom" value="1" />
<enum name="right_top" value="2" />
<enum name="right_bottom" value="3" />
</attr>
<!-- 尺寸屬性dp如果使用px可能會造成屏幕適配問題-->
<attr name="radius" format="dimension" />
<!-- 自定義屬性-->
<declare-styleable name="ArcMenu">
<attr name="position" />
<attr name="radius" />
</declare-styleable>
</resources>
二、編寫自定義組件
package com.lanou.dllo.arcmenudemo.arcmenu;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.RotateAnimation;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import com.lanou.dllo.arcmenudemo.R;
/**
* Created by dllo on 16/3/25.
* 1.首先ArcMenu是繼承ViewGroup,那么一個衛(wèi)星菜單包括一個大按鈕和其他的子按鈕群.
*/
public class ArcMenu extends ViewGroup implements View.OnClickListener {
//設(shè)置常量,標(biāo)識成枚舉
private static final int POS_LEFT_TOP = 0;
private static final int POS_LEFT_BOTTOM = 1;
private static final int POS_RIGHT_TOP = 2;
private static final int POS_RIGHT_BOTTOM = 3;
//以下5個成員變量是所需要的.
//聲明兩個屬性 位置 還有半徑
private Position mPosition = Position.RIGHT_BOTTOM;
private int mRadius;
/**
* 菜單的狀態(tài)
*/
private Status mCurrentStatus = Status.CLOSE;
/**
* 菜單的主按鈕
*/
private View mCButton;
//子菜單的回調(diào)按鈕
private OnMenuItemClickListener mMenuItemClickListener;
/**
* 菜單的位置枚舉類,4個位置
*/
public enum Position {
LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM
}
public enum Status {
OPEN, CLOSE
}
/**
* 點擊子菜單項,順便把位置傳遞過去
*/
public interface OnMenuItemClickListener {
void onClick(View view, int pos);
}
//3個構(gòu)造方法,相互傳遞.
//注意別寫錯誤.
public ArcMenu(Context context) {
this(context, null);
}
public ArcMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ArcMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//TypedValue.applyDimension是轉(zhuǎn)變標(biāo)準(zhǔn)尺寸的方法 參數(shù)一:單位 參數(shù)二:默認(rèn)值 參數(shù)三:可以獲取當(dāng)前屏幕的分辨率信息.
mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP
, 100, getResources().getDisplayMetrics());
//獲取自定義屬性的值
//參數(shù)1:attrs AttributeSet是節(jié)點的屬性集合
//參數(shù)2:attrs的一個數(shù)組集
//參數(shù)3:指向當(dāng)前theme 某個item 描述的style 該style指定了一些默認(rèn)值為這個TypedArray
//參數(shù)4;當(dāng)defStyleAttr 找不到或者為0, 可以直接指定某個style
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.ArcMenu, defStyleAttr, 0);
int pos = a.getInt(R.styleable.ArcMenu_position, POS_RIGHT_BOTTOM);
switch (pos) {
case POS_LEFT_TOP:
mPosition = Position.LEFT_TOP;
break;
case POS_LEFT_BOTTOM:
mPosition = Position.LEFT_BOTTOM;
break;
case POS_RIGHT_TOP:
mPosition = Position.RIGHT_TOP;
break;
case POS_RIGHT_BOTTOM:
mPosition = Position.RIGHT_BOTTOM;
break;
}
mRadius = (int) a.getDimension(R.styleable.ArcMenu_radius,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP
, 100, getResources().getDisplayMetrics()));
Log.d("TAG", "Position = " + mPosition + ", radius" + mRadius);
//使用完必須回收.
a.recycle();
}
public void setOnMenuItemClickListener(OnMenuItemClickListener mMenuItemClickListener) {
this.mMenuItemClickListener = mMenuItemClickListener;
}
/**
* 測量方法
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
//測量child的各個屬性.
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
layoutCButton();
//獲得容器內(nèi)組件的個數(shù),并且包括這個主的組件(大按鈕)
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
//這里直接獲取第一個,是因為getChildAt(0)是紅色的按鈕.
View child = getChildAt(i + 1);
//正常來說,如果設(shè)置按鈕動畫,移動出去后,是不能點擊的,這里給按鈕設(shè)置一個隱藏的屬性.等衛(wèi)星菜單飛過去,在讓它們顯示出來.
child.setVisibility(View.GONE);
/**
* 根據(jù)畫圖分析,得出每個子衛(wèi)星按鈕的夾角 a = 90°/(菜單的個數(shù)-1)
* 假設(shè)menu總數(shù)為4,那么從左側(cè)數(shù)menu1的坐標(biāo)為(0,R);
* menu2的坐標(biāo)為(R*sin(a),R*cos(a));
* menu3的坐標(biāo)為(R*sin(2a),R*cos(2a));
* ...
* menuN的坐標(biāo)為(R,0);
* 另:PI為π
* */
//測量每個子衛(wèi)星組件的在屏幕上面的坐標(biāo)距離
//這里count-2,是因為count包含了主按鈕
//每個組件的坐標(biāo)為(cl,ct);
int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
int cWidth = child.getMeasuredWidth();
int cHeight = child.getMeasuredHeight();
//如果衛(wèi)星菜單存在于底部,那么坐標(biāo)位置的計算方法,就完全相反.
/**
* 如果菜單位置在底部 左下 ,右下.坐標(biāo)會發(fā)生變化
* */
if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
ct = getMeasuredHeight() - cHeight - ct;
}
/**
* 右上,右下
* */
if (mPosition == Position.RIGHT_TOP || mPosition == Position.RIGHT_BOTTOM) {
cl = getMeasuredWidth() - cWidth - cl;
}
//子布局的測量坐標(biāo);
child.layout(cl, ct, cl + cWidth, ct + cHeight);
}
}
}
/**
* 定位主菜單按鈕
*/
private void layoutCButton() {
// 給主按鈕設(shè)置監(jiān)聽
mCButton = getChildAt(0);
mCButton.setOnClickListener(this);
//分別代表控件所處離左側(cè)和上側(cè)得距離
int l = 0;
int t = 0;
int width = mCButton.getMeasuredWidth();
int height = mCButton.getMeasuredHeight();
/**
* getMeasuredHeight()如果前面沒有對象調(diào)用,那么這個控件繼承ViewGroup,就意味著這是獲取容器的總高度.
* getMeasuredWidth()也是同理.
* 那么就可以判斷出控件在四個位置(根據(jù)坐標(biāo)系判斷.)
* */
switch (mPosition) {
case LEFT_TOP:
l = 0;
t = 0;
break;
case LEFT_BOTTOM:
l = 0;
t = getMeasuredHeight() - height;
break;
case RIGHT_TOP:
l = getMeasuredWidth() - width;
t = 0;
break;
case RIGHT_BOTTOM:
l = getMeasuredWidth() - width;
t = getMeasuredHeight() - height;
break;
}
//layout的四個屬性.分別代表主按鈕在不同位置距離屏幕左側(cè)和上側(cè)
mCButton.layout(l, t, l + width, t + height);
}
@Override
public void onClick(View v) {
//主要確定mCButton的值
mCButton = findViewById(R.id.id_button);
if (mCButton == null) {
mCButton = getChildAt(0);
}
//旋轉(zhuǎn)動畫
rotateCButton(v, 0f, 360f, 300);
//判斷菜單是否關(guān)閉,如果菜單關(guān)閉需要給菜單展開,如果菜單是展開的需要給菜單關(guān)閉.
toggleMenu(500);
}
/**
* 切換菜單
* 參數(shù):切換菜單的時間是可控的.
*/
public void toggleMenu(int duration) {
//為所有子菜單添加動畫. :平移動畫丶旋轉(zhuǎn)動畫
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
/**
* 默認(rèn)位置左上的話,子菜單起始坐標(biāo)點為(-cl,-ct);
* 位置右上的話,子菜單起始坐標(biāo)點為(+cl,-ct);
* 位置左下的話,子菜單起始坐標(biāo)點為(-cl,+ct);
* 位置右下的話,子菜單起始坐標(biāo)點為(+cl,+ct);**
* */
final View childView = getChildAt(i + 1);
//不管按鈕是開還是關(guān),子菜單必須顯示才能出現(xiàn)動畫效果.
childView.setVisibility(View.VISIBLE);
//平移 結(jié)束為止 0,0(以子菜單按鈕當(dāng)前位置,為坐標(biāo)系.)
int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
//創(chuàng)建兩個判斷變量,判別起始位置.
int xflag = 1;
int yflag = 1;
if (mPosition == Position.LEFT_TOP
|| mPosition == Position.LEFT_BOTTOM) {
xflag = -1;
}
if (mPosition == Position.LEFT_TOP
|| mPosition == Position.RIGHT_TOP) {
yflag = -1;
}
//多個動畫同時使用使用,用到AnimationSet
AnimationSet animset = new AnimationSet(true);
Animation tranAnim = null;
//to open 打開的情況下
if (mCurrentStatus == Status.CLOSE) {
tranAnim = new TranslateAnimation(xflag * cl, 0, yflag * ct, 0);
//當(dāng)衛(wèi)星菜單打開的時候,按鈕就可以進行點擊.
childView.setClickable(true);
childView.setFocusable(true);
} else {//to close
tranAnim = new TranslateAnimation(0, xflag * cl, 0, yflag * ct);
//當(dāng)衛(wèi)星菜單關(guān)閉的時候,按鈕也不能隨之點擊.
childView.setClickable(false);
childView.setFocusable(false);
}
tranAnim.setFillAfter(true);
tranAnim.setDuration(duration);
//設(shè)置彈出速度.
tranAnim.setStartOffset((i * 100) / count);
//為動畫設(shè)置監(jiān)聽 如果需要關(guān)閉的話,在動畫結(jié)束的同時,需要將子菜單的按鈕全部隱藏.
tranAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
//在動畫結(jié)束時,進行設(shè)置.
@Override
public void onAnimationEnd(Animation animation) {
if (mCurrentStatus == Status.CLOSE) {
// Log.d("動畫結(jié)束狀態(tài)",mCurrentStatus +"");
childView.setVisibility(View.GONE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//設(shè)置旋轉(zhuǎn)動畫(轉(zhuǎn)兩圈)
RotateAnimation rotateAnim = new RotateAnimation(0, 720,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnim.setDuration(duration);
rotateAnim.setFillAfter(true);
//把兩個動畫放到動畫集里面
//注意動畫順序.先增加旋轉(zhuǎn)/在增加移動./
animset.addAnimation(rotateAnim);
animset.addAnimation(tranAnim);
childView.startAnimation(animset);
final int pos = i + 1;
//設(shè)置子菜單的點擊事件
childView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mMenuItemClickListener != null) {
mMenuItemClickListener.onClick(childView, pos);
}
menuItemAnim(pos - 1);
//切換菜單狀態(tài)
changeStatus();
}
});
}
/**
* 當(dāng)所有子菜單切換完成后,那么菜單的狀態(tài)也發(fā)生了改變.
* 所以changeStatus()必須放在循環(huán)外,
* */
//切換菜單狀態(tài)
changeStatus();
}
/**
* 切換菜單狀態(tài)
*/
private void changeStatus() {
//在執(zhí)行一個操作之后,如果按鈕是打開的在次點擊就會切換狀態(tài).
mCurrentStatus = (mCurrentStatus == Status.CLOSE ? Status.OPEN :
Status.CLOSE);
Log.d("動畫結(jié)束狀態(tài)", mCurrentStatus + "");
}
public boolean isOpen(){
return mCurrentStatus ==Status.OPEN;
}
//設(shè)置旋轉(zhuǎn)動畫繞自身旋轉(zhuǎn)一圈 然后持續(xù)時間為300
private void rotateCButton(View v, float start, float end, int duration) {
RotateAnimation anim = new RotateAnimation(start, end, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
anim.setDuration(duration);
//保持動畫旋轉(zhuǎn)后的狀態(tài).
anim.setFillAfter(true);
v.startAnimation(anim);
}
/**
* 添加menuItem的點擊動畫
* */
private void menuItemAnim(int pos) {
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i + 1);
//在判斷條件下,寫入動畫
//當(dāng)其中一個子菜單被點擊后,自身變大并且消失
//其他子菜單則變小消失.
if (i == pos) {
childView.startAnimation(scaleBigAnim(300));
} else {
childView.startAnimation(scaleSmallAnim(300));
}
//當(dāng)子菜單被點擊之后,其他子菜單就要變成不可被點擊和獲得焦點的狀態(tài),
childView.setClickable(false);
childView.setFocusable(false);
}
}
/**
* 為當(dāng)前點擊的Item設(shè)置變大和透明度降低的動畫
*
* @param duration
* @return
*/
private Animation scaleBigAnim(int duration) {
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 4.0f, 1.0f, 4.0f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation alphaAnimation=new AlphaAnimation(1f,0.0f);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(alphaAnimation);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
private Animation scaleSmallAnim(int duration) {
AnimationSet animationSet = new AnimationSet(true);
ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation alphaAnimation=new AlphaAnimation(1f,0.0f);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(alphaAnimation);
animationSet.setDuration(duration);
animationSet.setFillAfter(true);
return animationSet;
}
}
以上就是 衛(wèi)星菜單的編寫,上面的注釋量比較大。
這里需要注意的一點。衛(wèi)星菜單在屏幕不同位置,他的動畫平移值是不一樣的。
如果實在不理解可以畫圖試試。
三、使用時注意賦予命名空間
<?xml version="1.0" encoding="utf-8"?>
<com.lanou.dllo.arcmenudemo.arcmenu.ArcMenu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:arcmenu="http://schemas.android.com/apk/res/com.lanou.dllo.arcmenudemo"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
arcmenu:position="left_top"
arcmenu:radius="140dp"
>
<!-- 主按鈕-->
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/composer_button">
<ImageView
android:id="@+id/id_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/composer_icn_plus" />
</RelativeLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_camera"
android:tag="Camera"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_music"
android:tag="Music"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_place"
android:tag="Place"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_sleep"
android:tag="Sleep"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_thought"
android:tag="Sun"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/composer_with"
android:tag="People"/>
</com.lanou.dllo.arcmenudemo.arcmenu.ArcMenu>
其他的大家可以自行探索研究。
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
- Android衛(wèi)星菜單效果的實現(xiàn)方法
- Android自定義VIew實現(xiàn)衛(wèi)星菜單效果淺析
- Android實現(xiàn)自定義的衛(wèi)星式菜單(弧形菜單)詳解
- Android編程實現(xiàn)仿優(yōu)酷圓盤旋轉(zhuǎn)菜單效果的方法詳解【附demo源碼下載】
- Android學(xué)習(xí)教程之圓形Menu菜單制作方法(1)
- Android自定義view實現(xiàn)圓形與半圓形菜單
- Android圓形旋轉(zhuǎn)菜單開發(fā)實例
- Android自定義ViewGroup實現(xiàn)帶箭頭的圓角矩形菜單
- Android仿優(yōu)酷圓形菜單學(xué)習(xí)筆記分享
- Adapter模式實戰(zhàn)之重構(gòu)鴻洋集團的Android圓形菜單建行
- Android實現(xiàn)衛(wèi)星菜單效果
相關(guān)文章
Android短信備份及數(shù)據(jù)插入實現(xiàn)代碼解析
這篇文章主要介紹了Android短信備份及數(shù)據(jù)插入實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11
Android自定義View實現(xiàn)游戲搖桿鍵盤的方法示例
Android進階過程中有一個繞不開的話題——自定義View。最近在做項目中又遇到了,所以下面這篇文章主要給大家介紹了利用Android自定義View實現(xiàn)游戲搖桿鍵盤的相關(guān)資料,操作方式類似王者榮耀的搖桿操作,文中通過示例代碼介紹的非常詳細,需要的朋友們下面來一起看看吧。2017-07-07
android開發(fā)教程之獲取使用當(dāng)前api的應(yīng)用程序名稱
開發(fā)手機安全管家的時候,比如要打電話,或者照相需要知道是哪個應(yīng)用程序在調(diào)用,就可以在API接口中調(diào)用下面的代碼2014-02-02
android studio節(jié)省C盤空間的配置方法
這篇文章主要介紹了android studio節(jié)省C盤空間的配置方法,本文給大家介紹的非常詳細,具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-07-07
Android開發(fā)之獲取LayoutInflater對象的方法總結(jié)
這篇文章主要介紹了Android開發(fā)之獲取LayoutInflater對象的方法,結(jié)合實例形式總結(jié)分析了Android獲取LayoutInflater對象的常用技巧,需要的朋友可以參考下2016-02-02

