仿知乎悬浮功能按钮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里面自取。在这里

时间: 2024-08-04 03:19:43

仿知乎悬浮功能按钮FloatingActionButton的相关文章

悬浮按钮FloatingActionButton

FloatingActionButton也是Design Support 提供的一个控件,FloatingActionButton不属于主界面的一部分,而是位于另外一个维度. 别忘了添加闭包 compile 'com.android.support:design:25.0.0' 代码运用如下, <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width

微信小程序开发日记——高仿知乎日报(上)

本人对知乎日报是情有独钟,看我的博客和github就知道了,写了几个不同技术类型的知乎日报APP 要做微信小程序首先要对html,css,js有一定的基础,还有对微信小程序的API也要非常熟悉 我将该教程分为以下三篇 微信小程序开发日记--高仿知乎日报(上) 微信小程序开发日记--高仿知乎日报(中) 微信小程序开发日记--高仿知乎日报(下) 三篇分别讲不同的组件和功能块 这篇要讲 API分析 启动页 轮播图 日报列表 浮动按钮 侧滑菜单 API分析 以下是使用到的具体API,更加详细参数和返回结

仿知乎日报学习笔记与优化

仿知乎日报学习笔记与优化项目原地址(有些小BUG):单页网页应用:https://github.com/pomelo-chuan/Zhihu-Daily-Vue.js知乎日报api查询:https://github.com/izzyleung/ZhihuDailyPurify/wiki/%E7%9F%A5%E4%B9%8E%E6%97%A5%E6%8A%A5-API-%E5%88%86%E6%9E%90如果想看完整的API的json数据的话,推荐使用sublime,网上搜索sublime jso

iOS开发&gt;学无止境 - 新浪微博iOS底部功能按钮简单实现

以上效果主要涉及点 九宫格布局 形变动画 UIView动画 首先,考虑图片的效果,初步采用使用modal控制器来实现,但是考虑到modal最后会移除modal他的控制器的view,所以,还是使用自定义UIView来实现这个功能. 做这种功能,首先实现的是按钮出现位置,后面在实现动画就容易了. 首先进行九宫格布局,创建模型传入指定数量的按钮,并且使用形变,将所有的按钮移动到看不见的坐标 点击底部按钮后,设置形变至屏幕区域(这里直接设置成CGAffineTransformIdentity) 对功能按

Office2007中简繁体转换功能按钮消失解决

Office2007 Excel简转繁功能按钮消失 运行环境: 系统:Win7旗舰版32位  Office 2007 解决方法: 针对于我遇到的情况 1.点击左上角的Office图标,选择Excel选项,进入 2.选择加载项,在最下面的管理中下拉选项中,选择禁用项目 3.启用ChineseTranslation Addin PS:对于简繁体功能按钮消失的情况,重新安装居然不能解决

在工具栏上新增功能按钮

在工具栏上新增功能按钮,该按钮定义保存在系统结构体SSCRFIELDS中,N为一个整数类型的序号例如当N等于1时,按钮描述保存在字段SSCRFIELDS-functxt_01中,按钮的功能代码将默认赋值为'FC01',保存在字段SSCRFIELDS-UCOMM中,其使用方法如下例所示.注意:本例中共有两个按钮由于按钮属于屏幕元素的一部分,多以必须定义好屏幕再对按钮字段进行赋值. 1 REPORT ztest_sum. 2 TABLES:mara. 3 TABLES sscrfields. "引用

Drawer Arrow Drawable(meun-icon-to-back-arrow)使用,仿知乎菜单栏界面

Drawer Arrow Drawable(meun-icon-to-back-arrow)使用,仿知乎菜单栏界面 一.什么是Drawer Arrow Drawable Drawer Arrow Drawable 其实就是一个抽屉侧滑菜单栏,只不过加入了很酷炫的meun-icon-to-back-arrow动画效果,如下图所示 二.Drawer Arrow Drawable的实现原理 设计方法: 我的想法是:如果我能生成每条线末端的移动曲线,我就能随抽屉(drawer)的滑动,根据参数t简单地计

[Swift通天遁地]二、表格表单-(7)电子邮件Mail:实现单元格左右滑动调出功能按钮

本文将演示对单元格进行扩展,当手指在单元格左右滑动时,弹出不同的功能菜单. Github项目:[MGSwipeTableCell] 下载该项目的源代码.文件夹[demo]->文件夹[MailAppDemoSwift]->文件夹[MailAppDemoSwift] ->双击文件[MailAppDemoSwift.xcodeproj]打开示例工程. 选择该项目中的几个文件,拖动到自己的开发项目中.按住[Shift],选择 [MGSwipeButton.h] [MGSwipeButton.m]

为Excel电子表格添加快速打印功能按钮的方法

对于需要频繁切换打印机来完成打印工作的人来说,切换打印机的重复操作非常繁琐且没有效率.其实微软早就为大家准备了各种解决重复劳动的各种工具,比如“快速访问工具栏”和“宏”.下面笔者就跟大家来讲讲如何利用这两个工具来为Excel电子表格添加快速打印的功能按钮,只要轻轻点击一下这个按钮,内容就会发送至指定的打印机进行打印,而不用管当前默认的打印机是哪一台. 一.添加“快速打印”功能按钮 首先打开Excel电子表格,点开窗口左上角下拉箭头的“自定义快速访问工具栏”,然后选择相应的功能即可.比如我们选择“