Android仿知乎懸浮功能按鈕FloatingActionButton效果
前段時間在看屬性動畫,恰巧這個按鈕的效果可以用屬性動畫實現(xiàn),所以就來實踐實踐。效果基本出來了,大家可以自己去完善。
首先看一下效果圖:
我們看到點擊FloatingActionButton后會展開一些item,然后會有一個蒙板效果,這都是這個View的功能。那么這整個View肯定是個ViewGroup,我們一部分一部分來看。
首先是這個最小的Tag:
這個Tag帶文字,可以是一個TextView,但為了美觀,我們使用CardView,CardView是一個FrameLayout,我們要讓它具有顯示文字的功能,就繼承CardView自定義一個ViewGroup。
public class TagView extends CardView
內(nèi)部維護一個TextView,在其構(gòu)造函數(shù)中我們實例化一個TextView用來顯示文字,并在外部調(diào)用setTagText的時候把TextView添加到這個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(); } //添加進這個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會引起所有View的layout addView(mTextView, layoutParams); } private int dp2px(int value){ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP , value, getResources().getDisplayMetrics()); } }
接下來我們看這個item,它是一個tag和一個fab的組合:
tag使用剛才我們自定義的TagView,fab就用系統(tǒng)的FloatingActionButton,這里顯然需要一個ViewGroup來組合這兩個子View,可以使用LinearLayout,這里我們就直接使用ViewGroup。
public class TagFabLayout extends ViewGroup
我們?yōu)檫@個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對該ViewGroup進行測量,這里我直接把寬高設(shè)置成wrap_content的了,match_parent和精確值感覺沒有必要。TagView和FloatingActionButton橫向排列,中間和兩邊留一點空隙。
@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); }
還要為這兩個子View注冊O(shè)nClickListener,這是點擊事件傳遞的源頭。
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有了,我們還需要一個蒙板,一個主fab,那么我們來看最終的ViewGroup。
思路也很清楚,蒙板是match_parent的,主fab在右下角(當(dāng)然我們可以自己設(shè)置,也可以對外提供接口來設(shè)置位置),三個item(也就是TagFabLayout)在主fab的上面。至于動畫效果,在點擊事件中觸發(fā)。
public class MultiFloatingActionButton extends ViewGroup
這里我們還需要自定義一些屬性,比如蒙板的顏色、主Fab的顏色、主Fab的圖案(當(dāng)然,你把主Fab直接寫在xml中就可以直接定義這些屬性)、動畫的duaration、動畫的模式等。
<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); //添加一個背景View和一個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中,我們并不會對這個ViewGroup進行wrap_content的支持,因為基本上都是match_parent的吧,也不會有精確值,而且這個ViewGroup應(yīng)該是在頂層的。我們看下onLayout方法,在這個方法中,我們對所有子View進行布局。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if(changed){ //布局背景和主Fab layoutFloatingActionButton(); layoutBackgroundView(); layoutItems(); } }
首先布局主Fab,它在右下角,然后添加點擊事件,點擊這個主Fab后,會涉及到旋轉(zhuǎn)主Fab,改變蒙板透明度,打開或關(guān)閉items等操作,這些等下再說。
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添加點擊事件。每個item都是TagFabLayout,可以為它setOnTagClickListener和setOnFabClickListener,以便我們點擊這兩塊區(qū)域的時候都要能響應(yīng),并且我們讓這兩個回調(diào)函數(shù)中做同樣的事情:旋轉(zhuǎn)主Fab、改變背景、關(guān)閉items(因為能點擊一定是展開狀態(tài))。此時還要在這個ViewGroup中設(shè)置一個接口OnFabItemClickListener,用于將點擊的位置傳遞出去,例如Activity實現(xiàn)了這個接口,就可以在onTagClick和onFabClick方法中調(diào)用mOnFabItemClickListener.onFabItemClick()方法。說一下這里的布局,是累積向上的,注意坐標的計算。
private void layoutItems(){ int count = getChildCount(); for(int i=2; i<count; i++) { TagFabLayout child = (TagFabLayout) getChildAt(i); child.setVisibility(INVISIBLE); //獲取自身測量寬高,這里說一下,由于TagFabLayout我們默認形成wrap_content,所以這里測量到的是wrap_content的最終大小 int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); // 獲取主Fab測量寬高 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)在所有的布局和點擊事件都已經(jīng)綁定好了,我們來看下rotateFloatingButton()、 changeBackground() 、 openMenu() 、closeMenu()這幾個和屬性動畫相關(guān)的函數(shù)。
其實也很簡單,rotateFloatingButton()對mFloatingActionButton的rotation這個屬性進行改變,以菜單是否打開為判斷條件。
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這個屬性,也是以菜單是否打開為判斷條件。
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ù)不同的模式來實現(xiàn)打開的效果,看一下scaleToShow(),這里同時對scaleX、scaleY、alpha這3個屬性進行動畫,來達到放大顯示的效果。
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(); } }
差不多達到我們要求的效果了,但是還有一個小地方需要注意一下,在menu展開的時候,如果我們點擊menu以外的區(qū)域,即蒙板上的區(qū)域,此時ViewGroup是不會攔截任何Touch事件,如果在這個FloatingActionButton下面有可以被點擊響應(yīng)的View,比如ListView,就會在蒙板顯示的情況下進行響應(yīng),正確的邏輯應(yīng)該是關(guān)閉menu。
那么我們需要在onInterceptTouchEvent中處理事件的攔截,這里判斷的方法是:如果menu是打開的,我們在DOWN事件中判斷x,y是否落在了a或b區(qū)域,如下圖
如果是的話,該ViewGroup應(yīng)該攔截這個事件,交由自身的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); }
再看一下,效果不錯。
由于我做的小app中涉及到切換夜間模式,這個ViewGroup的背景色應(yīng)該隨著主題改變,設(shè)置該View的背景色為
app:backgroundColor="?attr/myBackground"
重寫ViewGroup的 setBackgroundColor方法,這里所謂的背景色其實就是蒙板的顏色。
public void setBackgroundColor(int color){ mBackgroundColor = color; mBackgroundView.setBackgroundColor(color); }
基本功能到這里全部完成了,問題還有很多,比如沒有提供根據(jù)不同的position進行布局、沒有提供根據(jù)不同mode設(shè)置menu開閉的效果,但是后續(xù)我還會繼續(xù)改進和完善^ ^。歡迎交流。如果大家需要源碼,可以去我源碼里的customview里面自取。在這里
以上所述是小編給大家介紹的Android仿知乎懸浮功能按鈕FloatingActionButton效果,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
- Android開發(fā)懸浮按鈕 Floating ActionButton的實現(xiàn)方法
- Android自定義可拖拽的懸浮按鈕DragFloatingActionButton
- Android開發(fā)之FloatingActionButton懸浮按鈕基本使用、字體、顏色用法示例
- android 應(yīng)用內(nèi)部懸浮可拖動按鈕簡單實現(xiàn)代碼
- Android實現(xiàn)系統(tǒng)級懸浮按鈕
- Android懸浮按鈕的使用方法
- Android懸浮窗按鈕實現(xiàn)點擊并顯示/隱藏多功能列表
- Android自定義APP全局懸浮按鈕
- Android利用WindowManager生成懸浮按鈕及懸浮菜單
- Android自定義懸浮按鈕效果
相關(guān)文章
Android studio實現(xiàn)PopupWindow彈出框效果
這篇文章主要為大家詳細介紹了Android studio實現(xiàn)PopupWindow彈出框效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-10-10Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化
這篇文章主要為大家介紹了Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02Android的HTTP類庫Volley入門學(xué)習(xí)教程
這篇文章主要介紹了Android應(yīng)用開發(fā)框架Volley的入門學(xué)習(xí)教程,Volley適合于輕量級的通信功能開發(fā),善于處理JSON對象,需要的朋友可以參考下2016-02-02android實現(xiàn)主動連接和被動連接的藍牙聊天功能
這篇文章主要為大家詳細介紹了android實現(xiàn)主動連接和被動連接的藍牙聊天功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06Android ListView自動顯示隱藏布局的實現(xiàn)方法
這篇文章主要介紹了Android ListView自動顯示隱藏布局的實現(xiàn)方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09Android RxJava創(chuàng)建操作符Interval
這篇文章主要為大家詳細介紹了Android RxJava創(chuàng)建操作符Interval的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12