Android仿知乎悬浮功能按钮FloatingActionButton效果
前段时间在看属性动画,恰巧这个按钮的效果可以用属性动画实现,所以就来实践实践。效果基本出来了,大家可以自己去完善。
首先看一下效果图:
我们看到点击floatingactionbutton后会展开一些item,然后会有一个蒙板效果,这都是这个view的功能。那么这整个view肯定是个viewgroup,我们一部分一部分来看。
首先是这个最小的tag:
这个tag带文字,可以是一个textview,但为了美观,我们使用cardview,cardview是一个framelayout,我们要让它具有显示文字的功能,就继承cardview自定义一个viewgroup。
public class tagview extends cardview
内部维护一个textview,在其构造函数中我们实例化一个textview用来显示文字,并在外部调用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); } //给内部的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就用系统的floatingactionbutton,这里显然需要一个viewgroup来组合这两个子view,可以使用linearlayout,这里我们就直接使用viewgroup。
public class tagfablayout extends viewgroup
我们为这个viewgroup设置自定义属性,是为了给tag设置text:
<declare-styleable name="fabtaglayout"> <attr name="tagtext" format="string" /> </declare-styleable>
在构造器中获取自定义属性,初始化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进行测量,这里我直接把宽高设置成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设定为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注册onclicklistener,这是点击事件传递的源头。
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(); } } }); }
现在item的viewgroup有了,我们还需要一个蒙板,一个主fab,那么我们来看最终的viewgroup。
思路也很清楚,蒙板是match_parent的,主fab在右下角(当然我们可以自己设置,也可以对外提供接口来设置位置),三个item(也就是tagfablayout)在主fab的上面。至于动画效果,在点击事件中触发。
public class multifloatingactionbutton extends viewgroup
这里我们还需要自定义一些属性,比如蒙板的颜色、主fab的颜色、主fab的图案(当然,你把主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>
在构造器中我们同样是获取并初始化属性:
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应该是在顶层的。我们看下onlayout方法,在这个方法中,我们对所有子view进行布局。
@override protected void onlayout(boolean changed, int l, int t, int r, int b) { if(changed){ //布局背景和主fab layoutfloatingactionbutton(); layoutbackgroundview(); layoutitems(); } }
首先布局主fab,它在右下角,然后添加点击事件,点击这个主fab后,会涉及到旋转主fab,改变蒙板透明度,打开或关闭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,以便我们点击这两块区域的时候都要能响应,并且我们让这两个回调函数中做同样的事情:旋转主fab、改变背景、关闭items(因为能点击一定是展开状态)。此时还要在这个viewgroup中设置一个接口onfabitemclicklistener,用于将点击的位置传递出去,例如activity实现了这个接口,就可以在ontagclick和onfabclick方法中调用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); } } }); }
现在所有的布局和点击事件都已经绑定好了,我们来看下rotatefloatingbutton()、 changebackground() 、 openmenu() 、closemenu()这几个和属性动画相关的函数。
其实也很简单,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() 中根据不同的模式来实现打开的效果,看一下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以外的区域,即蒙板上的区域,此时viewgroup是不会拦截任何touch事件,如果在这个floatingactionbutton下面有可以被点击响应的view,比如listview,就会在蒙板显示的情况下进行响应,正确的逻辑应该是关闭menu。
那么我们需要在onintercepttouchevent中处理事件的拦截,这里判断的方法是:如果menu是打开的,我们在down事件中判断x,y是否落在了a或b区域,如下图
如果是的话,该viewgroup应该拦截这个事件,交由自身的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中做关闭menu等操作。
@override public boolean ontouchevent(motionevent event) { if(ismenuopen){ closemenu(); changebackground(); rotatefloatingbutton(); changestatus(); return true; } return super.ontouchevent(event); }
再看一下,效果不错。
由于我做的小app中涉及到切换夜间模式,这个viewgroup的背景色应该随着主题改变,设置该view的背景色为
app:backgroundcolor="?attr/mybackground"
重写viewgroup的 setbackgroundcolor方法,这里所谓的背景色其实就是蒙板的颜色。
public void setbackgroundcolor(int color){ mbackgroundcolor = color; mbackgroundview.setbackgroundcolor(color); }
基本功能到这里全部完成了,问题还有很多,比如没有提供根据不同的position进行布局、没有提供根据不同mode设置menu开闭的效果,但是后续我还会继续改进和完善^ ^。欢迎交流。如果大家需要源码,可以去我源码里的customview里面自取。在这里
以上所述是小编给大家介绍的android仿知乎悬浮功能按钮floatingactionbutton效果,希望对大家有所帮助