Android仿知乎懸浮功能按鈕FloatingActionButton效果
前段時(shí)間在看屬性動(dòng)畫,恰巧這個(gè)按鈕的效果可以用屬性動(dòng)畫實(shí)現(xiàn),所以就來(lái)實(shí)踐實(shí)踐。效果基本出來(lái)了,大家可以自己去完善。
首先看一下效果圖:

我們看到點(diǎn)擊FloatingActionButton后會(huì)展開(kāi)一些item,然后會(huì)有一個(gè)蒙板效果,這都是這個(gè)View的功能。那么這整個(gè)View肯定是個(gè)ViewGroup,我們一部分一部分來(lái)看。

首先是這個(gè)最小的Tag:

這個(gè)Tag帶文字,可以是一個(gè)TextView,但為了美觀,我們使用CardView,CardView是一個(gè)FrameLayout,我們要讓它具有顯示文字的功能,就繼承CardView自定義一個(gè)ViewGroup。
public class TagView extends CardView
內(nèi)部維護(hù)一個(gè)TextView,在其構(gòu)造函數(shù)中我們實(shí)例化一個(gè)TextView用來(lái)顯示文字,并在外部調(diào)用setTagText的時(shí)候把TextView添加到這個(gè)CardView中。
public class TagView extends CardView {
private TextView mTextView;
public TagView(Context context) {
this(context, null);
}
public TagView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TagView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTextView = new TextView(context);
mTextView.setSingleLine(true);
}
protected void setTextSize(float size){
mTextView.setTextSize(size);
}
protected void setTextColor(int color){
mTextView.setTextColor(color);
}
//給內(nèi)部的TextView添加文字
protected void setTagText(String text){
mTextView.setText(text);
addTag();
}
//添加進(jìn)這個(gè)layout中
private void addTag(){
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT
, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER);
int l = dp2px(8);
int t = dp2px(8);
int r = dp2px(8);
int b = dp2px(8);
layoutParams.setMargins(l, t, r, b);
//addView會(huì)引起所有View的layout
addView(mTextView, layoutParams);
}
private int dp2px(int value){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP
, value, getResources().getDisplayMetrics());
}
}
接下來(lái)我們看這個(gè)item,它是一個(gè)tag和一個(gè)fab的組合:

tag使用剛才我們自定義的TagView,fab就用系統(tǒng)的FloatingActionButton,這里顯然需要一個(gè)ViewGroup來(lái)組合這兩個(gè)子View,可以使用LinearLayout,這里我們就直接使用ViewGroup。
public class TagFabLayout extends ViewGroup
我們?yōu)檫@個(gè)ViewGroup設(shè)置自定義屬性,是為了給tag設(shè)置text:
<declare-styleable name="FabTagLayout"> <attr name="tagText" format="string" /> </declare-styleable>
在構(gòu)造器中獲取自定義屬性,初始化TagView并添加到該ViewGroup中:
public TagFabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttributes(context, attrs);
settingTagView(context);
}
private void getAttributes(Context context, AttributeSet attributeSet){
TypedArray typedArray = context.obtainStyledAttributes(attributeSet
, R.styleable.FabTagLayout);
mTagText = typedArray.getString(R.styleable.FabTagLayout_tagText);
typedArray.recycle();
}
private void settingTagView(Context context){
mTagView = new TagView(context);
mTagView.setTagText(mTagText);
addView(mTagView);
}
在onMeasure對(duì)該ViewGroup進(jìn)行測(cè)量,這里我直接把寬高設(shè)置成wrap_content的了,match_parent和精確值感覺(jué)沒(méi)有必要。TagView和FloatingActionButton橫向排列,中間和兩邊留一點(diǎn)空隙。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int count = getChildCount();
for(int i=0; i<count; i++){
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
width += view.getMeasuredWidth();
height = Math.max(height, view.getMeasuredHeight());
}
width += dp2px(8 + 8 + 8);
height += dp2px(8 + 8);
//直接將該ViewGroup設(shè)定為wrap_content的
setMeasuredDimension(width, height);
}
在onLayout中橫向布局,tag在左,fab在右。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//為子View布局
View tagView = getChildAt(0);
View fabView = getChildAt(1);
int tagWidth = tagView.getMeasuredWidth();
int tagHeight = tagView.getMeasuredHeight();
int fabWidth = fabView.getMeasuredWidth();
int fabHeight = fabView.getMeasuredHeight();
int tl = dp2px(8);
int tt = (getMeasuredHeight() - tagHeight) / 2;
int tr = tl + tagWidth;
int tb = tt + tagHeight;
int fl = tr + dp2px(8);
int ft = (getMeasuredHeight() - fabHeight) / 2;
int fr = fl + fabWidth;
int fb = ft + fabHeight;
fabView.layout(fl, ft, fr, fb);
tagView.layout(tl, tt, tr, tb);
bindEvents(tagView, fabView);
}
還要為這兩個(gè)子View注冊(cè)O(shè)nClickListener,這是點(diǎn)擊事件傳遞的源頭。
private void bindEvents(View tagView, View fabView){
tagView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mOnTagClickListener != null){
mOnTagClickListener.onTagClick();
}
}
});
fabView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnFabClickListener != null){
mOnFabClickListener.onFabClick();
}
}
});
}
現(xiàn)在item的ViewGroup有了,我們還需要一個(gè)蒙板,一個(gè)主fab,那么我們來(lái)看最終的ViewGroup。

思路也很清楚,蒙板是match_parent的,主fab在右下角(當(dāng)然我們可以自己設(shè)置,也可以對(duì)外提供接口來(lái)設(shè)置位置),三個(gè)item(也就是TagFabLayout)在主fab的上面。至于動(dòng)畫效果,在點(diǎn)擊事件中觸發(fā)。
public class MultiFloatingActionButton extends ViewGroup
這里我們還需要自定義一些屬性,比如蒙板的顏色、主Fab的顏色、主Fab的圖案(當(dāng)然,你把主Fab直接寫在xml中就可以直接定義這些屬性)、動(dòng)畫的duaration、動(dòng)畫的模式等。
<attr name="animationMode"> <enum name="fade" value="0"/> <enum name="scale" value="1"/> <enum name="bounce" value="2"/> </attr> <attr name="position"> <enum name="left_bottom" value="0"/> <enum name="right_bottom" value="1"/> </attr> <declare-styleable name="MultiFloatingActionButton"> <attr name="backgroundColor" format="color"/> <attr name="switchFabIcon" format="reference"/> <attr name="switchFabColor" format="color"/> <attr name="animationDuration" format="integer"/> <attr name="animationMode"/> <attr name="position"/> </declare-styleable>
在構(gòu)造器中我們同樣是獲取并初始化屬性:
public MultiFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取屬性值
getAttributes(context, attrs);
//添加一個(gè)背景View和一個(gè)FloatingActionButton
setBaseViews(context);
}
private void getAttributes(Context context, AttributeSet attrs){
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MultiFloatingActionButton);
mBackgroundColor = typedArray.getColor( R.styleable.MultiFloatingActionButton_backgroundColor, Color.TRANSPARENT);
mFabIcon = typedArray.getDrawable(R.styleable.MultiFloatingActionButton_switchFabIcon);
mFabColor = typedArray.getColorStateList(R.styleable.MultiFloatingActionButton_switchFabColor);
mAnimationDuration = typedArray.getInt(R.styleable.MultiFloatingActionButton_animationDuration, 150);
mAnimationMode = typedArray.getInt(R.styleable.MultiFloatingActionButton_animationMode, ANIM_SCALE);
mPosition = typedArray.getInt(R.styleable.MultiFloatingActionButton_position, POS_RIGHT_BOTTOM);
typedArray.recycle();
}
接著我們初始化、添加蒙板和主fab。
private void setBaseViews(Context context){
mBackgroundView = new View(context);
mBackgroundView.setBackgroundColor(mBackgroundColor);
mBackgroundView.setAlpha(0);
addView(mBackgroundView);
mFloatingActionButton = new FloatingActionButton(context);
mFloatingActionButton.setBackgroundTintList(mFabColor);
mFloatingActionButton.setImageDrawable(mFabIcon);
addView(mFloatingActionButton);
}
在onMeasure中,我們并不會(huì)對(duì)這個(gè)ViewGroup進(jìn)行wrap_content的支持,因?yàn)榛旧隙际莔atch_parent的吧,也不會(huì)有精確值,而且這個(gè)ViewGroup應(yīng)該是在頂層的。我們看下onLayout方法,在這個(gè)方法中,我們對(duì)所有子View進(jìn)行布局。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
//布局背景和主Fab
layoutFloatingActionButton();
layoutBackgroundView();
layoutItems();
}
}
首先布局主Fab,它在右下角,然后添加點(diǎn)擊事件,點(diǎn)擊這個(gè)主Fab后,會(huì)涉及到旋轉(zhuǎn)主Fab,改變蒙板透明度,打開(kāi)或關(guān)閉items等操作,這些等下再說(shuō)。
private void layoutFloatingActionButton(){
int width = mFloatingActionButton.getMeasuredWidth();
int height = mFloatingActionButton.getMeasuredHeight();
int fl = 0;
int ft = 0;
int fr = 0;
int fb = 0;
switch (mPosition){
case POS_LEFT_BOTTOM:
case POS_RIGHT_BOTTOM:
fl = getMeasuredWidth() - width - dp2px(8);
ft = getMeasuredHeight() - height - dp2px(8);
fr = fl + width;
fb = ft + height;
break;
}
mFloatingActionButton.layout(fl, ft, fr, fb);
bindFloatingEvent();
}
private void bindFloatingEvent(){
mFloatingActionButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rotateFloatingButton();
changeBackground();
changeStatus();
if (isMenuOpen) {
openMenu();
} else {
closeMenu();
}
}
});
}
然后布局背景:
private void layoutBackgroundView(){
mBackgroundView.layout(0, 0
, getMeasuredWidth(), getMeasuredHeight());
}
接著布局items,并為items添加點(diǎn)擊事件。每個(gè)item都是TagFabLayout,可以為它setOnTagClickListener和setOnFabClickListener,以便我們點(diǎn)擊這兩塊區(qū)域的時(shí)候都要能響應(yīng),并且我們讓這兩個(gè)回調(diào)函數(shù)中做同樣的事情:旋轉(zhuǎn)主Fab、改變背景、關(guān)閉items(因?yàn)槟茳c(diǎn)擊一定是展開(kāi)狀態(tài))。此時(shí)還要在這個(gè)ViewGroup中設(shè)置一個(gè)接口OnFabItemClickListener,用于將點(diǎn)擊的位置傳遞出去,例如Activity實(shí)現(xiàn)了這個(gè)接口,就可以在onTagClick和onFabClick方法中調(diào)用mOnFabItemClickListener.onFabItemClick()方法。說(shuō)一下這里的布局,是累積向上的,注意坐標(biāo)的計(jì)算。
private void layoutItems(){
int count = getChildCount();
for(int i=2; i<count; i++) {
TagFabLayout child = (TagFabLayout) getChildAt(i);
child.setVisibility(INVISIBLE);
//獲取自身測(cè)量寬高,這里說(shuō)一下,由于TagFabLayout我們默認(rèn)形成wrap_content,所以這里測(cè)量到的是wrap_content的最終大小
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
// 獲取主Fab測(cè)量寬高
int fabHeight = mFloatingActionButton.getMeasuredHeight();
int cl = 0;
int ct = 0;
switch (mPosition) {
case POS_LEFT_BOTTOM:
case POS_RIGHT_BOTTOM:
cl = getMeasuredWidth() - width - dp2px(8);
ct = getMeasuredHeight() - fabHeight - (i - 1) * height - dp2px(8);
}
child.layout(cl, ct, cl + width, ct + height);
bindMenuEvents(child, i);
prepareAnim(child);
}
}
private void bindMenuEvents(final TagFabLayout child, final int pos){
child.setOnTagClickListener(new TagFabLayout.OnTagClickListener() {
@Override
public void onTagClick() {
rotateFloatingButton();
changeBackground();
changeStatus();
closeMenu();
if(mOnFabItemClickListener != null){
mOnFabItemClickListener.onFabItemClick(child, pos);
}
}
});
child.setOnFabClickListener(new TagFabLayout.OnFabClickListener() {
@Override
public void onFabClick() {
rotateFloatingButton();
changeBackground();
changeStatus();
closeMenu();
if (mOnFabItemClickListener != null){
mOnFabItemClickListener.onFabItemClick(child, pos);
}
}
});
}
現(xiàn)在所有的布局和點(diǎn)擊事件都已經(jīng)綁定好了,我們來(lái)看下rotateFloatingButton()、 changeBackground() 、 openMenu() 、closeMenu()這幾個(gè)和屬性動(dòng)畫相關(guān)的函數(shù)。
其實(shí)也很簡(jiǎn)單,rotateFloatingButton()對(duì)mFloatingActionButton的rotation這個(gè)屬性進(jìn)行改變,以菜單是否打開(kāi)為判斷條件。
private void rotateFloatingButton(){
ObjectAnimator animator = isMenuOpen ? ObjectAnimator.ofFloat(mFloatingActionButton
, "rotation", 45F, 0f) : ObjectAnimator.ofFloat(mFloatingActionButton, "rotation", 0f, 45f);
animator.setDuration(150);
animator.setInterpolator(new LinearInterpolator());
animator.start();
}
changeBackground()改變mBackgroundView的alpha這個(gè)屬性,也是以菜單是否打開(kāi)為判斷條件。
private void changeBackground(){
ObjectAnimator animator = isMenuOpen ? ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0.9f, 0f) :
ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 0.9f);
animator.setDuration(150);
animator.setInterpolator(new LinearInterpolator());
animator.start();
}
openMenu() 中根據(jù)不同的模式來(lái)實(shí)現(xiàn)打開(kāi)的效果,看一下scaleToShow(),這里同時(shí)對(duì)scaleX、scaleY、alpha這3個(gè)屬性進(jìn)行動(dòng)畫,來(lái)達(dá)到放大顯示的效果。
private void openMenu(){
switch (mAnimationMode){
case ANIM_BOUNCE:
bounceToShow();
break;
case ANIM_SCALE:
scaleToShow();
}
}
private void scaleToShow(){
for(int i = 2; i<getChildCount(); i++){
View view = getChildAt(i);
view.setVisibility(VISIBLE);
view.setAlpha(0);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f);
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
AnimatorSet set = new AnimatorSet();
set.playTogether(scaleX, scaleY, alpha);
set.setDuration(mAnimationDuration);
set.start();
}
}
差不多達(dá)到我們要求的效果了,但是還有一個(gè)小地方需要注意一下,在menu展開(kāi)的時(shí)候,如果我們點(diǎn)擊menu以外的區(qū)域,即蒙板上的區(qū)域,此時(shí)ViewGroup是不會(huì)攔截任何Touch事件,如果在這個(gè)FloatingActionButton下面有可以被點(diǎn)擊響應(yīng)的View,比如ListView,就會(huì)在蒙板顯示的情況下進(jìn)行響應(yīng),正確的邏輯應(yīng)該是關(guān)閉menu。

那么我們需要在onInterceptTouchEvent中處理事件的攔截,這里判斷的方法是:如果menu是打開(kāi)的,我們?cè)贒OWN事件中判斷x,y是否落在了a或b區(qū)域,如下圖

如果是的話,該ViewGroup應(yīng)該攔截這個(gè)事件,交由自身的onTouchEvent處理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
if(isMenuOpen){
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if(judgeIfTouchBackground(x, y)){
intercepted = true;
}
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
intercepted = false;
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
}
return intercepted;
}
private boolean judgeIfTouchBackground(int x, int y){
Rect a = new Rect();
Rect b = new Rect();
a.set(0, 0, getWidth(), getHeight() - getChildAt(getChildCount() - 1).getTop());
b.set(0, getChildAt(getChildCount() - 1).getTop(), getChildAt(getChildCount() - 1).getLeft(), getHeight());
if(a.contains(x, y) || b.contains(x, y)){
return true;
}
return false;
}
在onTouchEvent中做關(guān)閉menu等操作。
@Override
public boolean onTouchEvent(MotionEvent event) {
if(isMenuOpen){
closeMenu();
changeBackground();
rotateFloatingButton();
changeStatus();
return true;
}
return super.onTouchEvent(event);
}
再看一下,效果不錯(cuò)。

由于我做的小app中涉及到切換夜間模式,這個(gè)ViewGroup的背景色應(yīng)該隨著主題改變,設(shè)置該View的背景色為
app:backgroundColor="?attr/myBackground"
重寫ViewGroup的 setBackgroundColor方法,這里所謂的背景色其實(shí)就是蒙板的顏色。
public void setBackgroundColor(int color){
mBackgroundColor = color;
mBackgroundView.setBackgroundColor(color);
}

基本功能到這里全部完成了,問(wèn)題還有很多,比如沒(méi)有提供根據(jù)不同的position進(jìn)行布局、沒(méi)有提供根據(jù)不同mode設(shè)置menu開(kāi)閉的效果,但是后續(xù)我還會(huì)繼續(xù)改進(jìn)和完善^ ^。歡迎交流。如果大家需要源碼,可以去我源碼里的customview里面自取。在這里
以上所述是小編給大家介紹的Android仿知乎懸浮功能按鈕FloatingActionButton效果,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
- Android開(kāi)發(fā)懸浮按鈕 Floating ActionButton的實(shí)現(xiàn)方法
- Android自定義可拖拽的懸浮按鈕DragFloatingActionButton
- Android開(kāi)發(fā)之FloatingActionButton懸浮按鈕基本使用、字體、顏色用法示例
- android 應(yīng)用內(nèi)部懸浮可拖動(dòng)按鈕簡(jiǎn)單實(shí)現(xiàn)代碼
- Android實(shí)現(xiàn)系統(tǒng)級(jí)懸浮按鈕
- Android懸浮按鈕的使用方法
- Android懸浮窗按鈕實(shí)現(xiàn)點(diǎn)擊并顯示/隱藏多功能列表
- Android自定義APP全局懸浮按鈕
- Android利用WindowManager生成懸浮按鈕及懸浮菜單
- Android自定義懸浮按鈕效果
相關(guān)文章
Android應(yīng)用中通過(guò)Layout_weight屬性用ListView實(shí)現(xiàn)表格
這篇文章主要介紹了Android應(yīng)用中通過(guò)Layout_weight屬性用ListView實(shí)現(xiàn)表格的方法,文中對(duì)Layout_weight屬性先有一個(gè)較為詳細(xì)的解釋,需要的朋友可以參考下2016-04-04
Android仿微信實(shí)現(xiàn)首字母導(dǎo)航條
這篇文章主要為大家詳細(xì)介紹了Android仿微信實(shí)現(xiàn)首字母導(dǎo)航條的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
Android實(shí)現(xiàn)圖片自動(dòng)輪播并且支持手勢(shì)左右無(wú)限滑動(dòng)
這篇文章給大家介紹android實(shí)現(xiàn)圖片自動(dòng)輪播并且支持手勢(shì)左右無(wú)限滑動(dòng),代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2016-10-10
Android使用SAX解析XML格式數(shù)據(jù)的操作步驟
SAX是一種基于事件驅(qū)動(dòng)的 XML 解析方式,適用于處理大規(guī)模 XML 文檔,本文給大家介紹了Android使用SAX解析XML格式數(shù)據(jù)的操作步驟,并通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04
使用Android WebSocket實(shí)現(xiàn)即時(shí)通訊功能
即時(shí)通訊(Instant Messaging)最重要的毫無(wú)疑問(wèn)就是即時(shí),不能有明顯的延遲,要實(shí)現(xiàn)IM的功能其實(shí)并不難,目前有很多第三方,比如極光的JMessage,都比較容易實(shí)現(xiàn)。本文通過(guò)實(shí)例代碼給大家分享Android WebSocket實(shí)現(xiàn)即時(shí)通訊功能,一起看看吧2019-10-10
Android實(shí)現(xiàn)客戶端語(yǔ)音動(dòng)彈界面實(shí)例代碼
這篇文章主要介紹了Android實(shí)現(xiàn)客戶端語(yǔ)音動(dòng)彈界面實(shí)例代碼,文章只給大家介紹了控件布局的方法,需要的朋友可以參考下2017-11-11
融會(huì)貫通Android?Jetpack?Compose中的Snackbar
這篇文章主要為大家介紹了融會(huì)貫通Android?Jetpack?Compose中的Snackbar方法及使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
android新建草稿刪除后下次開(kāi)機(jī)還會(huì)顯示保存的草稿
android 新建一個(gè)草稿,保存,然后全部刪除會(huì)話,關(guān)機(jī)再開(kāi)機(jī)后還會(huì)顯示保存的草稿,下面與大家分享下具體的解決方法2013-06-06

