欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android仿知乎悬浮功能按钮FloatingActionButton效果

程序员文章站 2022-08-02 16:18:49
前段时间在看属性动画,恰巧这个按钮的效果可以用属性动画实现,所以就来实践实践。效果基本出来了,大家可以自己去完善。 首先看一下效果图: 我们看到点击floating...

前段时间在看属性动画,恰巧这个按钮的效果可以用属性动画实现,所以就来实践实践。效果基本出来了,大家可以自己去完善。

首先看一下效果图:

Android仿知乎悬浮功能按钮FloatingActionButton效果

我们看到点击floatingactionbutton后会展开一些item,然后会有一个蒙板效果,这都是这个view的功能。那么这整个view肯定是个viewgroup,我们一部分一部分来看。

Android仿知乎悬浮功能按钮FloatingActionButton效果

首先是这个最小的tag:

Android仿知乎悬浮功能按钮FloatingActionButton效果

这个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的组合:

Android仿知乎悬浮功能按钮FloatingActionButton效果

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。

Android仿知乎悬浮功能按钮FloatingActionButton效果

思路也很清楚,蒙板是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。

Android仿知乎悬浮功能按钮FloatingActionButton效果

那么我们需要在onintercepttouchevent中处理事件的拦截,这里判断的方法是:如果menu是打开的,我们在down事件中判断x,y是否落在了a或b区域,如下图

Android仿知乎悬浮功能按钮FloatingActionButton效果

如果是的话,该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);
 }

再看一下,效果不错。

Android仿知乎悬浮功能按钮FloatingActionButton效果

由于我做的小app中涉及到切换夜间模式,这个viewgroup的背景色应该随着主题改变,设置该view的背景色为

app:backgroundcolor="?attr/mybackground"

重写viewgroup的 setbackgroundcolor方法,这里所谓的背景色其实就是蒙板的颜色。

public void setbackgroundcolor(int color){
 mbackgroundcolor = color;
 mbackgroundview.setbackgroundcolor(color);
}

Android仿知乎悬浮功能按钮FloatingActionButton效果

基本功能到这里全部完成了,问题还有很多,比如没有提供根据不同的position进行布局、没有提供根据不同mode设置menu开闭的效果,但是后续我还会继续改进和完善^ ^。欢迎交流。如果大家需要源码,可以去我源码里的customview里面自取。在这里

以上所述是小编给大家介绍的android仿知乎悬浮功能按钮floatingactionbutton效果,希望对大家有所帮助