35

I am using a custom view to create a FloatingActionMenu with FloatingActionButtons. I have modified this class to make it work almost perfectly, when trying to add a shadow recently I ran into an issue with it, the shadow is cut by an invisible square due to the class hardcoding it into a square, I believe.

See the below image:

FloatingActionButton

The class I am using is so I can have multiple FloatingActionButtons(FABs) in a menu.

Here is the class:

package terranovaproductions.newcomicreader;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.ArrayList;

/**
 * Created by charry on 2015/6/11. https://gist.github.com/douo/dfde289778a9b3b6918f and modified by Tristan Wiley
 */
public class FloatingActionMenu extends ViewGroup {

    static final TimeInterpolator DEFAULT_OPEN_INTERPOLATOR = new OvershootInterpolator();
    static final TimeInterpolator DEFAULT_CLOSE_INTERPOLATOR = new AnticipateInterpolator();
    private static final long ANIMATION_DURATION = 300;
    private static final int DEFAULT_CHILD_GRAVITY = Gravity.END | Gravity.BOTTOM;
    Animator animator = new Animator() {
        @Override
        public long getStartDelay() {
            return 0;
        }

        @Override
        public void setStartDelay(long startDelay) {

        }

        @Override
        public Animator setDuration(long duration) {
            duration = 2;
            return null;
        }

        @Override
        public long getDuration() {
            return 0;
        }

        @Override
        public void setInterpolator(TimeInterpolator value) {

        }

        @Override
        public boolean isRunning() {
            return true;
        }
    };
    private FloatingActionButton mMenuButton;
    private ArrayList<FloatingActionButton> mMenuItems;
    private ArrayList<TextView> mMenuItemLabels;
    private ArrayList<ItemAnimator> mMenuItemAnimators;
    private int mItemMargin;
    private AnimatorSet mOpenAnimatorSet = new AnimatorSet();
    private AnimatorSet mCloseAnimatorSet = new AnimatorSet();
    private ImageView mIcon;
    private boolean mOpen;
    private boolean animating;
    private boolean mIsSetClosedOnTouchOutside = true;
    private OnMenuItemClickListener onMenuItemClickListener;
    private OnMenuToggleListener onMenuToggleListener;
    GestureDetector mGestureDetector = new GestureDetector(getContext(),
            new GestureDetector.SimpleOnGestureListener() {

                @Override
                public boolean onDown(MotionEvent e) {
                    return mIsSetClosedOnTouchOutside && isOpened();
                }

                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    close();
                    return true;
                }
            });
    private OnClickListener mOnItemClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (v instanceof FloatingActionButton) {
                int i = mMenuItems.indexOf(v);
                if (onMenuItemClickListener != null) {
                    onMenuItemClickListener.onMenuItemClick(FloatingActionMenu.this, i, (FloatingActionButton) v);
                }
            } else if (v instanceof TextView) {
                int i = mMenuItemLabels.indexOf(v);
                if (onMenuItemClickListener != null) {
                    onMenuItemClickListener.onMenuItemClick(FloatingActionMenu.this, i, mMenuItems.get(i));
                }
            }
            close();
        }
    };


    public FloatingActionMenu(Context context) {
        this(context, null, 0);
    }

    public FloatingActionMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FloatingActionMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mMenuItems = new ArrayList<>(5);
        mMenuItemAnimators = new ArrayList<>(5);

        mMenuItemLabels = new ArrayList<>(5);
        mIcon = new ImageView(context);
    }

    @Override
    protected void onFinishInflate() {
        bringChildToFront(mMenuButton);
        bringChildToFront(mIcon);
        super.onFinishInflate();
    }

    @Override
    public void addView(@NonNull View child, int index, LayoutParams params) {
        super.addView(child, index, params);
        if (getChildCount() > 1) {
            if (child instanceof FloatingActionButton) {
                addMenuItem((FloatingActionButton) child);
            }
        } else {
            mMenuButton = (FloatingActionButton) child;
            mIcon.setImageDrawable(mMenuButton.getDrawable());
            addView(mIcon);
            mMenuButton.setImageDrawable(mMenuButton.getDrawable());
            createDefaultIconAnimation();
            mMenuButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    toggle();
                }
            });
        }
    }

    public void toggle() {
        if (!mOpen) {
            open();
        } else {
            close();
        }
    }

    public void open() {
        d("open");
        startOpenAnimator();
        mOpen = true;
        if (onMenuToggleListener != null) {
            onMenuToggleListener.onMenuToggle(true);
        }
    }

    public void close() {
        startCloseAnimator();
        mOpen = false;
        if (onMenuToggleListener != null) {
            onMenuToggleListener.onMenuToggle(true);
        }
    }

    protected void startCloseAnimator() {
        mCloseAnimatorSet.start();
        for (ItemAnimator anim : mMenuItemAnimators) {
            anim.startCloseAnimator();
        }
    }

//    Rect rect = new Rect();
//    Paint paint = new Paint();
//
//    @Override
//    protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
//        boolean b = super.drawChild(canvas, child, drawingTime);
//        paint.setColor(0xFFFF0000);
//        paint.setStyle(Paint.Style.STROKE);
//        rect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
//        canvas.drawRect(rect, paint);
//        return b;
//    }

    protected void startOpenAnimator() {
        mOpenAnimatorSet.start();
        for (ItemAnimator anim : mMenuItemAnimators) {
            anim.startOpenAnimator();
        }
    }

    public void addMenuItem(FloatingActionButton item) {
        mMenuItems.add(item);
        mMenuItemAnimators.add(new ItemAnimator(item));

        TextView label = new TextView(getContext());


        label.setBackgroundResource(R.drawable.rounded_corners);

        label.setTextColor(Color.WHITE);
        label.setText(item.getContentDescription());

        Integer paddingSize = (int)label.getTextSize() / 3;

        float scale = getResources().getDisplayMetrics().density;
        int pxtodp = (int) (6*scale + 0.5f);

        label.setPadding(paddingSize, paddingSize, paddingSize + pxtodp, paddingSize);

        addView(label);
        mMenuItemLabels.add(label);
        item.setTag(label);
        item.setOnClickListener(mOnItemClickListener);
        label.setOnClickListener(mOnItemClickListener);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width;
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height;
        final int count = getChildCount();
        int maxChildWidth = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
        for (int i = 0; i < mMenuItems.size(); i++) {
            FloatingActionButton fab = mMenuItems.get(i);
            TextView label = mMenuItemLabels.get(i);
            maxChildWidth = Math.max(maxChildWidth, label.getMeasuredWidth() + fab.getMeasuredWidth() + mItemMargin);

        }

        maxChildWidth = Math.max(mMenuButton.getMeasuredWidth(), maxChildWidth);

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = maxChildWidth + 30;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            int heightSum = 0;
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                heightSum += child.getMeasuredHeight();
            }
            height = heightSum + 20;
        }

        setMeasuredDimension(resolveSize(width, widthMeasureSpec),
                resolveSize(height, heightMeasureSpec));
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (mIsSetClosedOnTouchOutside) {
            return mGestureDetector.onTouchEvent(event);
        } else {
            return super.onTouchEvent(event);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        System.out.println("onLayout:" + changed);
        if (changed) {
            int right = r - getPaddingRight();
            int bottom = b - getPaddingBottom();
            int top = bottom - mMenuButton.getMeasuredHeight();

            mMenuButton.layout(right - mMenuButton.getMeasuredWidth(), top, right, bottom);
            int dw = (mMenuButton.getMeasuredWidth() - mIcon.getMeasuredWidth()) / 2;
            int dh = (mMenuButton.getMeasuredHeight() - mIcon.getMeasuredHeight()) / 2;
            mIcon.layout(right - mIcon.getMeasuredWidth() - dw, bottom - mIcon.getMeasuredHeight() - dh, right - dw, bottom - dh);
            for (int i = 0; i < mMenuItems.size(); i++) {
                FloatingActionButton item = mMenuItems.get(i);
                TextView label = mMenuItemLabels.get(i);


                bottom = top -= mMenuItems.get(i).getPaddingBottom(); //Add 10px padding

                top -= item.getMeasuredHeight();
                int width = item.getMeasuredWidth();
                int d = (mMenuButton.getMeasuredWidth() - width) / 2;
                item.layout(right - width - d, top, right - d, bottom);
                d = (item.getMeasuredHeight() - label.getMeasuredHeight()) / 2;

                label.layout(item.getLeft() - mItemMargin - label.getMeasuredWidth(), item.getTop() + d, item.getLeft() - mItemMargin, item.getTop() + d + label.getMeasuredHeight());
                label.setBackgroundResource(R.drawable.rounded_corners);

                if (!animating) {
                    if (!mOpen) {
                        item.setTranslationY(mMenuButton.getTop() - item.getTop());
                        item.setVisibility(GONE);
                        label.setVisibility(GONE);
                    } else {
                        item.setTranslationY(0);
                        item.setVisibility(VISIBLE);
                        label.setVisibility(VISIBLE);
                    }
                }
            }
            if (!animating && getBackground() != null) {
                if (!mOpen) {
                    getBackground().setAlpha(0);
                } else {
                    getBackground().setAlpha(0xff);
                }
            }
        }
    }

    private void createDefaultIconAnimation() {
        Animator.AnimatorListener listener = new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                animating = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                animating = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                animating = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        };
        ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(
                mIcon,
                "rotation",
                135f,
                0f
        );

        ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(
                mIcon,
                "rotation",
                0f,
                135f
        );

        if (getBackground() != null) {


            ValueAnimator hideBackgroundAnimator = ObjectAnimator.ofInt(0xff, 0);
            hideBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    Integer alpha = (Integer) animation.getAnimatedValue();
                    //System.out.println(alpha);
                    getBackground().setAlpha(alpha > 0xff ? 0xff : alpha);
                }
            });
            ValueAnimator showBackgroundAnimator = ObjectAnimator.ofInt(0, 0xff);
            showBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {

                    Integer alpha = (Integer) animation.getAnimatedValue();
                    //System.out.println(alpha);
                    getBackground().setAlpha(alpha > 0xff ? 0xff : alpha);
                }
            });

            mOpenAnimatorSet.playTogether(expandAnimator, showBackgroundAnimator);
            mCloseAnimatorSet.playTogether(collapseAnimator, hideBackgroundAnimator);
        } else {
            mOpenAnimatorSet.playTogether(expandAnimator);
            mCloseAnimatorSet.playTogether(collapseAnimator);
        }

        mOpenAnimatorSet.setInterpolator(DEFAULT_OPEN_INTERPOLATOR);
        mCloseAnimatorSet.setInterpolator(DEFAULT_CLOSE_INTERPOLATOR);

        mOpenAnimatorSet.setDuration(ANIMATION_DURATION);
        mCloseAnimatorSet.setDuration(ANIMATION_DURATION);

        mOpenAnimatorSet.addListener(listener);
        mCloseAnimatorSet.addListener(listener);
    }

    public boolean isOpened() {
        return mOpen;
    }

    @Override
    public Parcelable onSaveInstanceState() {
        d("onSaveInstanceState");
        Bundle bundle = new Bundle();
        bundle.putParcelable("instanceState", super.onSaveInstanceState());
        bundle.putBoolean("mOpen", mOpen);
        // ... save everything
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        d("onRestoreInstanceState");
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            mOpen = bundle.getBoolean("mOpen");
            // ... load everything
            state = bundle.getParcelable("instanceState");
        }
        super.onRestoreInstanceState(state);
    }

    @Override
    protected void onDetachedFromWindow() {
        d("onDetachedFromWindow");
        //getBackground().setAlpha(bgAlpha);//reset default alpha
        super.onDetachedFromWindow();
    }

    @Override
    public void setBackground(Drawable background) {
        if (background instanceof ColorDrawable) {
            // after activity finish and relaucher , background drawable state still remain?
            int bgAlpha = Color.alpha(((ColorDrawable) background).getColor());
            d("bg:" + Integer.toHexString(bgAlpha));
            super.setBackground(background);
        } else {
            throw new IllegalArgumentException("floating only support color background");
        }
    }

    public OnMenuToggleListener getOnMenuToggleListener() {
        return onMenuToggleListener;
    }

    public void setOnMenuToggleListener(OnMenuToggleListener onMenuToggleListener) {
        this.onMenuToggleListener = onMenuToggleListener;
    }

    public OnMenuItemClickListener getOnMenuItemClickListener() {
        return onMenuItemClickListener;
    }

    public void setOnMenuItemClickListener(OnMenuItemClickListener onMenuItemClickListener) {
        this.onMenuItemClickListener = onMenuItemClickListener;
    }

    protected void d(String msg) {
        Log.d("FAM", msg == null ? null : msg);
    }

    public interface OnMenuToggleListener {
        void onMenuToggle(boolean opened);
    }


    public interface OnMenuItemClickListener {
        void onMenuItemClick(FloatingActionMenu fam, int index, FloatingActionButton item);
    }

    private class ItemAnimator implements Animator.AnimatorListener {
        private View mView;
        private boolean playingOpenAnimator;

        public ItemAnimator(View v) {
            v.animate().setListener(this);
            mView = v;
        }

        public void startOpenAnimator() {
            mView.animate().cancel();
            playingOpenAnimator = true;
            mView.animate().translationY(0).setInterpolator(DEFAULT_OPEN_INTERPOLATOR).start();
            mMenuButton.animate().rotation(135f).setInterpolator(DEFAULT_OPEN_INTERPOLATOR).start();
        }

        public void startCloseAnimator() {
            mView.animate().cancel();
            playingOpenAnimator = false;
            mView.animate().translationY((mMenuButton.getTop() - mView.getTop())).setInterpolator(DEFAULT_CLOSE_INTERPOLATOR).start();
            mMenuButton.animate().rotation(0f).setInterpolator(DEFAULT_CLOSE_INTERPOLATOR).start();
        }

        @Override
        public void onAnimationStart(Animator animation) {
            if (playingOpenAnimator) {
                mView.setVisibility(VISIBLE);
            } else {
                ((TextView) mView.getTag()).setVisibility(GONE);
            }
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (!playingOpenAnimator) {
                mView.setVisibility(GONE);
            } else {
                ((TextView) mView.getTag()).setVisibility(VISIBLE);
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    }
}

My Layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:fab="http://schemas.android.com/apk/res-auto"
    android:id="@+id/comicView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background_main"
    android:orientation="vertical">

    <terranovaproductions.newcomicreader.FloatingActionMenu
        android:id="@+id/fab_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        >

        <!--First button as menu button-->
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab_main"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_add_white_24dp"
            fab:fabSize="normal"
            fab:backgroundTint="@color/material_orange"
            fab:borderWidth="0dp"
            fab:elevation="6dp"/>

        <!-- Other button as menu items-->
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab_random"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/default_random"
            android:paddingBottom="@dimen/menu_button_margin"
            android:src="@drawable/ic_random"
            fab:fabSize="mini"
            fab:backgroundTint="@color/material_orange" />


        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab_download"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/download"
            android:paddingBottom="@dimen/menu_button_margin"
            android:src="@drawable/ic_download"
            fab:fabSize="mini"
            fab:backgroundTint="@color/material_orange"/>

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab_browser"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/default_browser"
            android:paddingBottom="@dimen/menu_button_margin"
            android:src="@drawable/ic_open_browser"
            fab:fabSize="mini"
            fab:backgroundTint="@color/material_orange"/>

    </terranovaproductions.newcomicreader.FloatingActionMenu>


</RelativeLayout>

I understand there are better ways to do a FloatingActionMenu but I chose this way because I put a lot of work into it.

I have tried remove padding, adding a margin. I am unsure on how to make the shadow extend. I am pretty sure in the onLayout I need to change something.

Ask for any other information if needed.

Community
  • 1
  • 1
Tristan
  • 3,530
  • 3
  • 30
  • 39
  • Have you taken stuff out of your XML, or is it really just the FAM inside of the RL? I'm sure you've shortened this for an MCVE, but I'd try to post the actual XML – AdamMc331 Sep 05 '15 at 23:15
  • I shortened it since the other stuff was irrelevant. Just two TextViews and an ImageView – Tristan Sep 06 '15 at 02:28
  • It might not be irrelevant. Why is the width and height of the FAM match_parent, as opposed to wrap_content? Also, how is it getting on the bottom right without using layout_gravity? – AdamMc331 Sep 06 '15 at 02:30
  • Found why? I have the same problem here. – Rodrigo Manguinho Oct 27 '16 at 13:55

4 Answers4

76

You need to add

android:clipChildren="false"
android:clipToPadding="false"

to the parent view in your xml layout.

Justin
  • 6,564
  • 6
  • 37
  • 34
10

You can try to remove 16dp padding from FloatingActionMenu and add 16dp margin to each FloatingActionButton.

Orgazm pionerki
  • 336
  • 4
  • 11
4

Just add a margin to the button. Like Orgazm pionerki suggested. I added 5dp, and this is OK.

Tsunaze
  • 3,204
  • 7
  • 44
  • 81
-6

Override the default elevation of the FAB by adding:

android:elevation="0dp"

Or in code call View.setElevation(float)

For more detail visit here.

How to remove FloatingActionButton's surrounding shadow?

Community
  • 1
  • 1
Harshad
  • 1,344
  • 1
  • 10
  • 25