/* * Copyright (C) 2015 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.widget; import android.animation.Animator; import android.content.Context; 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.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import com.hippo.ehviewer.R; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import junit.framework.Assert; public class FabLayout extends ViewGroup implements View.OnClickListener { private static final long ANIMATE_TIME = 300L; private static final String STATE_KEY_SUPER = "super"; private static final String STATE_KEY_AUTO_CANCEL = "auto_cancel"; private static final String STATE_KEY_EXPANDED = "expanded"; private int mFabSize; private int mFabMiniSize; private int mIntervalPrimary; private int mIntervalSecondary; private boolean mExpanded = true; private boolean mAutoCancel = true; private boolean mHidePrimaryFab = false; private float mMainFabCenterY = -1f; private OnExpandListener mOnExpandListener; private OnClickFabListener mOnClickFabListener; public FabLayout(Context context) { super(context); init(context); } public FabLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public FabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { setSoundEffectsEnabled(false); setClipToPadding(false); mFabSize = context.getResources().getDimensionPixelOffset(R.dimen.fab_size); mFabMiniSize = context.getResources().getDimensionPixelOffset(R.dimen.fab_min_size); mIntervalPrimary = context.getResources().getDimensionPixelOffset(R.dimen.fab_layout_primary_margin); mIntervalSecondary = context.getResources().getDimensionPixelOffset(R.dimen.fab_layout_secondary_margin); } @Override public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) { if (!(child instanceof FloatingActionButton)) { throw new IllegalStateException("FloatingActionBarLayout should only " + "contain FloatingActionButton, but try to add "+ child.getClass().getName()); } super.addView(child, index, params); } public FloatingActionButton getPrimaryFab() { View v = getChildAt(getChildCount() - 1); if (v == null) { return null; } else { return (FloatingActionButton) v; } } public int getSecondaryFabCount() { return Math.max(0, getChildCount() - 1); } public FloatingActionButton getSecondaryFabAt(int index) { if (index < 0 || index >= getSecondaryFabCount()) { return null; } return (FloatingActionButton) getChildAt(index); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Assert.assertEquals("Measure mode must be MeasureSpec.EXACTLY", MeasureSpec.getMode(widthMeasureSpec), MeasureSpec.EXACTLY); Assert.assertEquals("Measure mode must be MeasureSpec.EXACTLY", MeasureSpec.getMode(heightMeasureSpec), MeasureSpec.EXACTLY); int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width - getPaddingLeft() - getPaddingRight(), MeasureSpec.AT_MOST); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height - getPaddingTop() - getPaddingBottom(), MeasureSpec.AT_MOST); measureChildren(childWidthMeasureSpec, childHeightMeasureSpec); setMeasuredDimension(width, height); } // For pre-L, FloatActionButton use padding to show shadow, so its position looks wrong. // We use it default size to make it position right @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int centerX = 0; int bottom = getMeasuredHeight() - getPaddingBottom(); int count = getChildCount(); int i = count; while(--i >= 0) { View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); int layoutBottom; int layoutRight; if (i == count - 1) { layoutBottom = bottom + ((childHeight - mFabSize) / 2); layoutRight = getMeasuredWidth() - getPaddingRight() + ((childWidth - mFabSize) / 2); bottom -= mFabSize + mIntervalPrimary; centerX = layoutRight - (childWidth / 2); mMainFabCenterY = layoutBottom - (childHeight / 2f); } else { layoutBottom = bottom + ((childHeight - mFabMiniSize) / 2); layoutRight = centerX + (childWidth / 2); bottom -= mFabMiniSize + mIntervalSecondary; } child.layout(layoutRight - childWidth, layoutBottom - childHeight, layoutRight, layoutBottom); } } public void setOnExpandListener(OnExpandListener listener) { mOnExpandListener = listener; } public void setOnClickFabListener(OnClickFabListener listener) { mOnClickFabListener = listener; if (listener != null) { for (int i = 0, n = getChildCount(); i < n; i++) { getChildAt(i).setOnClickListener(this); } } else { for (int i = 0, n = getChildCount(); i < n; i++) { getChildAt(i).setClickable(false); } } } public void setHidePrimaryFab(boolean hidePrimaryFab) { if (mHidePrimaryFab != hidePrimaryFab) { mHidePrimaryFab = hidePrimaryFab; boolean expanded = mExpanded; int count = getChildCount(); if (!expanded && count > 0) { getChildAt(count - 1).setVisibility(hidePrimaryFab ? INVISIBLE : VISIBLE); } } } public void setAutoCancel(boolean autoCancel) { if (mAutoCancel != autoCancel) { mAutoCancel = autoCancel; if (mExpanded) { if (autoCancel) { setOnClickListener(this); } else { setClickable(false); } } } } public void toggle() { setExpanded(!mExpanded); } public boolean isExpanded() { return mExpanded; } public void setExpanded(boolean expanded) { setExpanded(expanded, true); } public void setExpanded(boolean expanded, boolean animation) { if (mExpanded != expanded) { mExpanded = expanded; if (mAutoCancel) { if (expanded) { setOnClickListener(this); } else { setClickable(false); } } final int count = getChildCount(); if (count > 0) { if (mMainFabCenterY == -1f || !animation) { // It is before first onLayout int checkCount = mHidePrimaryFab ? count : count - 1; for (int i = 0; i < checkCount; i++) { View child = getChildAt(i); child.setVisibility(expanded ? View.VISIBLE : View.INVISIBLE); if (expanded) { child.setAlpha(1f); } } } else { if (mHidePrimaryFab) { setPrimaryFabAnimation(getChildAt(count - 1), expanded, !expanded); } for (int i = 0; i < count - 1; i++) { View child = getChildAt(i); setSecondaryFabAnimation(child, expanded, expanded); } } } if (mOnExpandListener != null) { mOnExpandListener.onExpand(expanded); } } } private void setPrimaryFabAnimation(final View child, final boolean expanded, boolean delay) { float startRotation; float endRotation; float startScale; float endScale; Interpolator interpolator; if (expanded) { startRotation = -45.0f; endRotation = 0.0f; startScale = 0.0f; endScale = 1.0f; interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR; } else { startRotation = 0.0f; endRotation = 0.0f; startScale = 1.0f; endScale = 0.0f; interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR; } child.setScaleX(startScale); child.setScaleY(startScale); child.setRotation(startRotation); child.animate() .scaleX(endScale) .scaleY(endScale) .rotation(endRotation) .setStartDelay(delay ? ANIMATE_TIME : 0L) .setDuration(ANIMATE_TIME) .setInterpolator(interpolator) .setListener(new SimpleAnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (expanded) { child.setVisibility(View.VISIBLE); } } @Override public void onAnimationEnd(Animator animation) { if (!expanded) { child.setVisibility(View.INVISIBLE); } } }).start(); } private void setSecondaryFabAnimation(final View child, final boolean expanded, boolean delay) { float startTranslationY; float endTranslationY; float startAlpha; float endAlpha; Interpolator interpolator; if (expanded) { startTranslationY = mMainFabCenterY - (child.getTop() + (child.getHeight() / 2)); endTranslationY = 0f; startAlpha = 0f; endAlpha = 1f; interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR; } else { startTranslationY = 0f; endTranslationY = mMainFabCenterY - (child.getTop() + (child.getHeight() / 2)); startAlpha = 1f; endAlpha = 0f; interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR; } child.setAlpha(startAlpha); child.setTranslationY(startTranslationY); child.animate() .alpha(endAlpha) .translationY(endTranslationY) .setStartDelay(delay ? ANIMATE_TIME : 0L) .setDuration(ANIMATE_TIME) .setInterpolator(interpolator) .setListener(new SimpleAnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (expanded) { child.setVisibility(View.VISIBLE); } } @Override public void onAnimationEnd(Animator animation) { if (!expanded) { child.setVisibility(View.INVISIBLE); } } }).start(); } @Override public void onClick(View v) { if (this == v) { setExpanded(false); } else if (mOnClickFabListener != null) { int position = indexOfChild(v); if (position == getChildCount() - 1) { mOnClickFabListener.onClickPrimaryFab(this, (FloatingActionButton) v); } else if (position >= 0 && mExpanded) { mOnClickFabListener.onClickSecondaryFab(this, (FloatingActionButton) v, position); } } } @Override protected void dispatchSetPressed(boolean pressed) { // Don't dispatch it to children } @Override public Parcelable onSaveInstanceState() { final Bundle state = new Bundle(); state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState()); state.putBoolean(STATE_KEY_AUTO_CANCEL, mAutoCancel); state.putBoolean(STATE_KEY_EXPANDED, mExpanded); return state; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { final Bundle savedState = (Bundle) state; super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER)); setAutoCancel(savedState.getBoolean(STATE_KEY_AUTO_CANCEL)); setExpanded(savedState.getBoolean(STATE_KEY_EXPANDED), false); } } public interface OnExpandListener { void onExpand(boolean expanded); } public interface OnClickFabListener { void onClickPrimaryFab(FabLayout view, FloatingActionButton fab); void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int position); } }