From 003912976e775c2eb7ca24e008f75f209a030c79 Mon Sep 17 00:00:00 2001 From: Joaquim Ley Date: Sat, 3 Mar 2018 13:13:19 +0100 Subject: [PATCH] Feature/community requests (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add the ability to open/close the component, set color through resolved color Exposed two new methods open/close and renamed the internal implementation toexpande/collapse respectivily, when the consumer opens or closes the component there is an exposed listener which'll trigger the respective callback to when the animation ends. Fix: #35 See also: #41 (thanks @qijaz221 for your contribution) * Update README with changelog Remove blog comming soon post since no artcile is going to be written 🔖 * Bump versions, set latest targetSdkVersion to 27 * update to implementation --- README.MD | 43 +- library/build.gradle | 8 +- .../com/joaquimley/faboptions/FabOptions.java | 753 ++++++++++-------- .../FabOptionsAnimationStateListener.java | 11 + sample/build.gradle | 10 +- .../joaquimley/sample/JavaSampleActivity.java | 139 ++-- .../joaquimley/sample/XmlSampleActivity.java | 152 ++-- .../main/res/layout/activity_sample_xml.xml | 2 +- 8 files changed, 597 insertions(+), 521 deletions(-) create mode 100644 library/src/main/java/com/joaquimley/faboptions/FabOptionsAnimationStateListener.java diff --git a/README.MD b/README.MD index 062b21c..a362803 100644 --- a/README.MD +++ b/README.MD @@ -1,18 +1,13 @@ -![AppIcon](../master/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png) +![AppIcon](../develop/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png) # FabOptions [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-FabOptions-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/4734) [![MaterialUp](https://img.shields.io/badge/MaterialUp-FabOptions-blue.svg?style=flat)](https://material.uplabs.com/posts/faboptions) -[![Bintray](https://img.shields.io/badge/Bintray-v1.1.2-brightgreen.svg?style=flat)](https://bintray.com/leyopensource/FabOptions/com.github.joaquimley%3Afaboptions/1.1.2) +[![Bintray](https://img.shields.io/badge/Bintray-v1.2.0-brightgreen.svg?style=flat)](https://bintray.com/leyopensource/FabOptions/com.github.joaquimley%3Afaboptions/1.1.2) ![minSdkVersion](https://img.shields.io/badge/minSdkVersion-14-green.svg?style=true) -![compileSdkVersion](https://img.shields.io/badge/compileSdkVersion-25-green.svg?style=true) - -### A multi-functional FAB component with customizable options. -### Read more on the blog post: < WIP-Soon > +![compileSdkVersion](https://img.shields.io/badge/compileSdkVersion-27-green.svg?style=true) **Special thanks to [André Mion](https://github.com/andremion)** for the help provided on building this component. - - Original concept by **Praveen Bisht** posted on [MaterialUp](https://www.uplabs.com/posts/options-floating-interaction), turned into code into open source library. @@ -27,7 +22,7 @@ Android implementation - Import gradle dependency: dependencies { - compile 'com.github.joaquimley:faboptions:1.1.2' + compile 'com.github.joaquimley:faboptions:1.2.0' } - Add the component to your layout: @@ -40,7 +35,6 @@ Android implementation android:layout_gravity="bottom" /> ``` - - Define a `menu.xml` file with your buttons information **e.g.** ```xml @@ -111,7 +105,7 @@ fabOptions.setFabColor(R.color.fabOptionsFabColor); fabOptions.setBackgroundColor(R.color.fabOptionsBackgroundColor); ``` -*Note: One is not depedent on the other, you can costomize individualy.* +*Note: One is not dependent on the other, you can set individually.* **Changing button color** ```java @@ -128,6 +122,32 @@ This will return a boolean value if it's able to change the color. **Issues:** Fell free to open a new issue. Follow the [ISSUE_TEMPLATE.MD](../development/ISSUE_TEMPLATE.MD) +## [Changelog](https://github.com/JoaquimLey/faboptions/releases) + +**1.2.0** +- Ability to open and close the component with new exposed `open()`/`close()` methods. - #35 +- Change the background color `setBackgroundColor()` through `@ColorInt` - #41 + +**1.1.2** +- Fix a bug where buttons were clickable even when hidden - #25 + +**1.1.1** +- Fix a resurfaced issue with related to Snackbar behaviour - #8 + +**1.1.0** +- Backport to API 14 - #21 +- Change button color at runtime with the new #setButtonColor(int) - #22 +- Bug fix on Menu not displayed correctly - #17 +- Customize both background + fab colors. - #16 + +**1.0.2** +- Fix layout measure +- The component now reacts when a snackbar dismissed by user - #8 + + +**1.0.1** +- Fix slight vertical offset on the button's icon - #2 + ## Contributing Contributions are always welcome! @@ -143,7 +163,6 @@ Follow the "fork-and-pull" Git workflow. **Prevent** code-style related changes (at least run Ctrl+⌥+O, ⌥+⌘+L) before commiting. - ### License Copyright © 2016 Joaquim Ley diff --git a/library/build.gradle b/library/build.gradle index 0f79206..5d7cd27 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -25,8 +25,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 27 - versionCode 11 - versionName "1.1.2" + versionCode 12 + versionName "1.2.0" vectorDrawables.useSupportLibrary = true } @@ -39,8 +39,8 @@ android { } dependencies { - final DESIGN_LIBRARY_VERSION = '27.0.2' + final DESIGN_LIBRARY_VERSION = '27.1.0' compile "com.android.support:design:$DESIGN_LIBRARY_VERSION" } -//apply from: 'https://raw.githubusercontent.com/JoaquimLey/jcenter-config/master/deploy.gradle' +apply from: 'https://raw.githubusercontent.com/JoaquimLey/jcenter-config/master/deploy.gradle' diff --git a/library/src/main/java/com/joaquimley/faboptions/FabOptions.java b/library/src/main/java/com/joaquimley/faboptions/FabOptions.java index b7575c7..c59812c 100644 --- a/library/src/main/java/com/joaquimley/faboptions/FabOptions.java +++ b/library/src/main/java/com/joaquimley/faboptions/FabOptions.java @@ -17,6 +17,7 @@ package com.joaquimley.faboptions; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; @@ -28,6 +29,7 @@ import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.MenuRes; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; @@ -57,358 +59,401 @@ @CoordinatorLayout.DefaultBehavior(FabOptionsBehavior.class) public class FabOptions extends FrameLayout implements View.OnClickListener { - private static final String TAG = "FabOptions"; - private static final int NO_DIMENSION = 0; - private static final long CLOSE_MORPH_TRANSFORM_DURATION = 70; - - private boolean mIsAnimating; - private boolean mIsOpen; - private View.OnClickListener mListener; - - private Menu mMenu; // TODO: 22/11/2016 add items in runtime - private FloatingActionButton mFab; - - private View mBackground; - private View mSeparator; - private FabOptionsButtonContainer mButtonContainer; - - public FabOptions(Context context) { - this(context, null, 0); - } - - public FabOptions(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public FabOptions(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - mIsOpen = false; - initViews(context); - setInitialFabIcon(); - - TypedArray fabOptionsAttributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FabOptions, 0, 0); - styleComponent(context, fabOptionsAttributes); - inflateButtonsFromAttrs(context, fabOptionsAttributes); - } - - private void initViews(Context context) { - inflate(context, R.layout.faboptions_layout, this); - mBackground = findViewById(R.id.faboptions_background); - mButtonContainer = (FabOptionsButtonContainer) findViewById(R.id.faboptions_button_container); - mFab = (FloatingActionButton) findViewById(R.id.faboptions_fab); - mFab.setOnClickListener(this); - } - - private void setInitialFabIcon() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - VectorDrawable drawable = (VectorDrawable) getResources().getDrawable(faboptions_ic_overflow, null); - mFab.setImageDrawable(drawable); - } else { - mFab.setImageResource(R.drawable.faboptions_ic_overflow); - } - } - - private void styleComponent(Context context, TypedArray attributes) { - // If not set, the background same colour as the FAB, which if not set - // it will use the default accent color - int fabColor = attributes.getColor(R.styleable.FabOptions_fab_color, getThemeAccentColor(context)); - int backgroundColor = attributes.getColor(R.styleable.FabOptions_background_color, fabColor); - - setBackgroundColor(context, backgroundColor); - mFab.setBackgroundTintList(ColorStateList.valueOf(fabColor)); - } - - public void setFabColor(@ColorRes int fabColor) { - Context context = getContext(); - if (context != null) { - @ColorInt int colorId = ContextCompat.getColor(context, fabColor); - mFab.setBackgroundTintList(ColorStateList.valueOf(colorId)); - } - } - - public void setBackgroundColor(@ColorRes int backgroundColor) { - Context context = getContext(); - if (context != null) { - @ColorInt int color = ContextCompat.getColor(context, backgroundColor); - setBackgroundColor(context, color); - } else { - Log.w(TAG, "Couldn't set background color, context is null"); - } - } - - - private void setBackgroundColor(Context context, @ColorInt int backgroundColor) { - Drawable backgroundShape = ContextCompat.getDrawable(context, R.drawable.faboptions_background); - backgroundShape.setColorFilter(backgroundColor, PorterDuff.Mode.ADD); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - mBackground.setBackground(backgroundShape); - } else { - mBackground.setBackgroundDrawable(backgroundShape); - } - } - - @ColorInt - private int getThemeAccentColor(final Context context) { - final TypedValue value = new TypedValue(); - context.getTheme().resolveAttribute(R.attr.colorAccent, value, true); - return value.data; - } - - private void inflateButtonsFromAttrs(Context context, TypedArray attributes) { - if (attributes.hasValue(R.styleable.FabOptions_button_menu)) { - setButtonsMenu(context, attributes.getResourceId(R.styleable.FabOptions_button_menu, 0)); - } - } - - public boolean setButtonColor(int buttonId, @ColorRes int color) { - for (int i = 0; i < mButtonContainer.getChildCount(); i++) { - if (mMenu.getItem(i).getItemId() == buttonId) { - return styleButton(i, color); - } - } - Log.d(TAG, "setButtonColor(): Couldn't find button with id " + buttonId); - return false; - } - - public void setButtonsMenu(@MenuRes int menuId) { - Context context = getContext(); - if (context != null) { - setButtonsMenu(context, menuId); - } else { - Log.w(TAG, "Couldn't set buttons, context is null"); - } - } - - /** - * Deprecated. Use {@link #setButtonsMenu(int)} instead. - */ - @Deprecated - public void setButtonsMenu(Context context, @MenuRes int menuId) { - mMenu = new MenuBuilder(context); - SupportMenuInflater menuInf = new SupportMenuInflater(context); - menuInf.inflate(menuId, mMenu); - addButtonsFromMenu(context, mMenu); - mSeparator = mButtonContainer.addSeparator(context); - animateButtons(false); - } - - private void addButtonsFromMenu(Context context, Menu menu) { - for (int i = 0; i < menu.size(); i++) { - addButton(context, menu.getItem(i)); - } - } - - private void addButton(Context context, MenuItem menuItem) { - AppCompatImageView button = mButtonContainer.addButton(context, menuItem.getItemId(), - menuItem.getTitle(), menuItem.getIcon()); - button.setOnClickListener(this); - } - - private boolean styleButton(int buttonIndex, @ColorRes int color) { - if (buttonIndex >= (mButtonContainer.getChildCount() / 2)) { - // Hacky way to deal with the separator view index - buttonIndex++; - } - - if (buttonIndex >= mButtonContainer.getChildCount()) { - Log.e(TAG, "Button at " + buttonIndex + " is null (index out of bounds)"); - return false; - } - - AppCompatImageView imageView = (AppCompatImageView) mButtonContainer.getChildAt(buttonIndex); - imageView.setColorFilter(ContextCompat.getColor(getContext(), color)); - return true; - } - - @Override - public void onClick(View v) { - if (!mIsAnimating) { - mIsAnimating = true; - - if (v.getId() == R.id.faboptions_fab) { - if (mIsOpen) { - close(); - } else { - open(); - } - } else { - if (mListener != null && mIsOpen) { - mListener.onClick(v); - close(); - } - } - } - } - - @Override - public void setOnClickListener(View.OnClickListener listener) { - mListener = listener; - } - - private void open() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources() - .getDrawable(R.drawable.faboptions_ic_menu_animatable, null); - mFab.setImageDrawable(drawable); - drawable.start(); - } else { - mFab.setImageResource(R.drawable.faboptions_ic_close); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - final TransitionSet transitionSet = new OpenMorphTransition(mButtonContainer); - transitionSet.addListener(new Transition.TransitionListener() { - @Override - public void onTransitionStart(final Transition transition) { - } - - @Override - public void onTransitionEnd(final Transition transition) { - mIsAnimating = false; - } - - @Override - public void onTransitionCancel(final Transition transition) { - } - - @Override - public void onTransitionPause(final Transition transition) { - } - - @Override - public void onTransitionResume(final Transition transition) { - } - }); - - TransitionManager.beginDelayedTransition(this, transitionSet); - } - animateBackground(true); - animateButtons(true); - - mIsOpen = true; - } - - private void close() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources().getDrawable(R.drawable.faboptions_ic_close_animatable, null); - mFab.setImageDrawable(drawable); - drawable.start(); - } else { - mFab.setImageResource(R.drawable.faboptions_ic_overflow); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - final TransitionSet transitionSet = new CloseMorphTransition(mButtonContainer); - transitionSet.addListener(new Transition.TransitionListener() { - @Override - public void onTransitionStart(final Transition transition) { - } - - @Override - public void onTransitionEnd(final Transition transition) { - mIsAnimating = false; - } - - @Override - public void onTransitionCancel(final Transition transition) { - } - - @Override - public void onTransitionPause(final Transition transition) { - } - - @Override - public void onTransitionResume(final Transition transition) { - } - }); - - TransitionManager.beginDelayedTransition(this, transitionSet); - } - animateButtons(false); - animateBackground(false); - mIsOpen = false; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mSeparator != null) { - ViewGroup.LayoutParams separatorLayoutParams = mSeparator.getLayoutParams(); - separatorLayoutParams.width = mFab.getMeasuredWidth(); - separatorLayoutParams.height = mFab.getMeasuredHeight(); - mSeparator.setLayoutParams(separatorLayoutParams); - } - } - - private void animateBackground(final boolean isOpen) { - ViewGroup.LayoutParams backgroundLayoutParams = mBackground.getLayoutParams(); - backgroundLayoutParams.width = isOpen ? mButtonContainer.getMeasuredWidth() : NO_DIMENSION; - mBackground.setLayoutParams(backgroundLayoutParams); - } - - private void openCompatAnimation() { - ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 1.0f); - anim.setDuration(30000); // duration 3 seconds - anim.start(); - } - - - private void closeCompatAnimation() { - ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 0.0f); - anim.setDuration(3000); - anim.start(); - animateButtons(false); - } - - private void animateButtons(boolean isOpen) { - for (int i = 0; i < mButtonContainer.getChildCount(); i++) { - mButtonContainer.getChildAt(i).setScaleX(isOpen ? 1 : 0); - mButtonContainer.getChildAt(i).setScaleY(isOpen ? 1 : 0); - } - } - - public boolean isOpen() { - return mIsOpen; - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private class OpenMorphTransition extends TransitionSet { - OpenMorphTransition(ViewGroup viewGroup) { - - ChangeBounds changeBound = new ChangeBounds(); - changeBound.excludeChildren(R.id.faboptions_button_container, true); - addTransition(changeBound); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - ChangeTransform changeTransform = new ChangeTransform(); - for (int i = 0; i < viewGroup.getChildCount(); i++) { - changeTransform.addTarget(viewGroup.getChildAt(i)); - } - addTransition(changeTransform); - } - - setOrdering(TransitionSet.ORDERING_SEQUENTIAL); - } - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private class CloseMorphTransition extends TransitionSet { - CloseMorphTransition(ViewGroup viewGroup) { - - ChangeBounds changeBound = new ChangeBounds(); - changeBound.excludeChildren(R.id.faboptions_button_container, true); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - ChangeTransform changeTransform = new ChangeTransform(); - for (int i = 0; i < viewGroup.getChildCount(); i++) { - changeTransform.addTarget(viewGroup.getChildAt(i)); - } - changeTransform.setDuration(CLOSE_MORPH_TRANSFORM_DURATION); - addTransition(changeTransform); - } - - addTransition(changeBound); - setOrdering(TransitionSet.ORDERING_TOGETHER); - } - } + private static final String TAG = "FabOptions"; + + private static final String SUPER_INSTANCE_STATE = "superInstanceState"; + private static final String FAB_OPTIONS_IS_OPEN = "fabOptionsIsOpen"; + + private static final int NO_DIMENSION = 0; + private static final long CLOSE_MORPH_TRANSFORM_DURATION = 70; + + private boolean mIsAnimating; + private boolean mIsOpen; + private View.OnClickListener mClickListener; + + private Menu mMenu; + private FloatingActionButton mFab; + + private View mBackground; + private View mSeparator; + private FabOptionsButtonContainer mButtonContainer; + + public FabOptions(Context context) { + this(context, null); + } + + public FabOptions(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FabOptions(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initViews(context); + setInitialFabIcon(); + + TypedArray fabOptionsAttributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FabOptions, 0, 0); + styleComponent(context, fabOptionsAttributes); + inflateButtonsFromAttrs(context, fabOptionsAttributes); + } + + public boolean isOpen() { + return mIsOpen; + } + + public void open(@Nullable final FabOptionsAnimationStateListener listener) { + expand(listener); + } + + public void close(@Nullable final FabOptionsAnimationStateListener listener) { + collapse(listener); + } + +// @Nullable +// @Override +// protected Parcelable onSaveInstanceState() { +// Bundle bundle = new Bundle(); +// bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState()); +// bundle.putBoolean(FAB_OPTIONS_IS_OPEN, mIsOpen); +// return bundle; +// } +// +// @Override +// protected void onRestoreInstanceState(Parcelable state) { +// if (state instanceof Bundle) { +// Bundle bundle = (Bundle) state; +// mIsOpen = bundle.getBoolean(FAB_OPTIONS_IS_OPEN, false); +// state = bundle.getParcelable(SUPER_INSTANCE_STATE); +// } +// super.onRestoreInstanceState(state); +// } + + public void setFabColor(@ColorRes int fabColor) { + Context context = getContext(); + if (context != null) { + @ColorInt int colorId = ContextCompat.getColor(context, fabColor); + mFab.setBackgroundTintList(ColorStateList.valueOf(colorId)); + } + } + + public void setBackgroundColor(Context context, @ColorInt int backgroundColor) { + Drawable backgroundShape = ContextCompat.getDrawable(context, R.drawable.faboptions_background); + if (backgroundShape != null) { + backgroundShape.setColorFilter(backgroundColor, PorterDuff.Mode.ADD); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mBackground.setBackground(backgroundShape); + } else { + mBackground.setBackgroundDrawable(backgroundShape); + } + } + + /** + * @deprecated Prefer passing resolved color {@link #setBackgroundColor(Context, int)} for safe context + */ + @Deprecated + public void setBackgroundColor(@ColorRes int backgroundColor) { + Context context = getContext(); + if (context != null) { + setBackgroundColor(context, ContextCompat.getColor(context, backgroundColor)); + } else { + Log.w(TAG, "Couldn't set background color, context is null"); + } + } + + public boolean setButtonColor(int buttonId, @ColorRes int color) { + for (int i = 0; i < mButtonContainer.getChildCount(); i++) { + if (mMenu.getItem(i).getItemId() == buttonId) { + return styleButton(i, color); + } + } + Log.d(TAG, "setButtonColor(): Couldn't find button with id " + buttonId); + return false; + } + + public void setButtonsMenu(@MenuRes int menuId) { + Context context = getContext(); + if (context != null) { + setButtonsMenu(context, menuId); + } else { + Log.w(TAG, "Couldn't set buttons, context is null"); + } + } + + /** + * Deprecated use {@link #setButtonsMenu(int)}. + */ + @Deprecated + @SuppressLint("RestrictedApi") + public void setButtonsMenu(Context context, @MenuRes int menuId) { + mMenu = new MenuBuilder(context); + SupportMenuInflater menuInf = new SupportMenuInflater(context); + menuInf.inflate(menuId, mMenu); + addButtonsFromMenu(context, mMenu); + mSeparator = mButtonContainer.addSeparator(context); + animateButtons(false); + } + + private void initViews(Context context) { + inflate(context, R.layout.faboptions_layout, this); + mBackground = findViewById(R.id.faboptions_background); + mButtonContainer = (FabOptionsButtonContainer) findViewById(R.id.faboptions_button_container); + mFab = (FloatingActionButton) findViewById(R.id.faboptions_fab); + mFab.setOnClickListener(this); + } + + private void setInitialFabIcon() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + VectorDrawable drawable = (VectorDrawable) getResources().getDrawable(faboptions_ic_overflow, null); + mFab.setImageDrawable(drawable); + } else { + mFab.setImageResource(R.drawable.faboptions_ic_overflow); + } + } + + /** + * Styles the component via attributes R.styleable.FabOptions_fab_color + * If not set, the background same colour as the FAB, which in turn if the later + * is not not set the default accent color will be used + */ + private void styleComponent(Context context, TypedArray attributes) { + int fabColor = attributes.getColor(R.styleable.FabOptions_fab_color, getThemeAccentColor(context)); + int backgroundColor = attributes.getColor(R.styleable.FabOptions_background_color, fabColor); + + setBackgroundColor(context, backgroundColor); + mFab.setBackgroundTintList(ColorStateList.valueOf(fabColor)); + } + + @ColorInt + private int getThemeAccentColor(final Context context) { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorAccent, value, true); + return value.data; + } + + private void inflateButtonsFromAttrs(Context context, TypedArray attributes) { + if (attributes.hasValue(R.styleable.FabOptions_button_menu)) { + setButtonsMenu(context, attributes.getResourceId(R.styleable.FabOptions_button_menu, 0)); + } + } + + private void addButtonsFromMenu(Context context, Menu menu) { + for (int i = 0; i < menu.size(); i++) { + addButton(context, menu.getItem(i)); + } + } + + private void addButton(Context context, MenuItem menuItem) { + AppCompatImageView button = mButtonContainer.addButton(context, menuItem.getItemId(), + menuItem.getTitle(), menuItem.getIcon()); + button.setOnClickListener(this); + } + + private boolean styleButton(int buttonIndex, @ColorRes int color) { + if (buttonIndex >= (mButtonContainer.getChildCount() / 2)) { + // Hacky way to deal with the separator view index + buttonIndex++; + } + + if (buttonIndex >= mButtonContainer.getChildCount()) { + Log.e(TAG, "Button at " + buttonIndex + " is null (index out of bounds)"); + return false; + } + + AppCompatImageView imageView = (AppCompatImageView) mButtonContainer.getChildAt(buttonIndex); + imageView.setColorFilter(ContextCompat.getColor(getContext(), color)); + return true; + } + + @Override + public void onClick(View v) { + if (!mIsAnimating) { + mIsAnimating = true; + if (v.getId() == R.id.faboptions_fab) { + if (mIsOpen) { + collapse(null); + } else { + expand(null); + } + } else { + if (mClickListener != null && mIsOpen) { + mClickListener.onClick(v); + collapse(null); + } + } + } + } + + @Override + public void setOnClickListener(View.OnClickListener listener) { + mClickListener = listener; + } + + private void expand(@Nullable final FabOptionsAnimationStateListener listener) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources() + .getDrawable(R.drawable.faboptions_ic_menu_animatable, null); + mFab.setImageDrawable(drawable); + drawable.start(); + } else { + mFab.setImageResource(R.drawable.faboptions_ic_close); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + final TransitionSet transitionSet = new OpenMorphTransition(mButtonContainer); + transitionSet.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(final Transition transition) { + } + + @Override + public void onTransitionEnd(final Transition transition) { + if (listener != null) { + listener.onOpenAnimationEnd(); + } + mIsAnimating = false; + } + + @Override + public void onTransitionCancel(final Transition transition) { + } + + @Override + public void onTransitionPause(final Transition transition) { + } + + @Override + public void onTransitionResume(final Transition transition) { + } + }); + + TransitionManager.beginDelayedTransition(this, transitionSet); + } + animateBackground(true); + animateButtons(true); + + mIsOpen = true; + } + + private void collapse(@Nullable final FabOptionsAnimationStateListener listener) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getResources().getDrawable(R.drawable.faboptions_ic_close_animatable, null); + mFab.setImageDrawable(drawable); + drawable.start(); + } else { + mFab.setImageResource(R.drawable.faboptions_ic_overflow); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + final TransitionSet transitionSet = new CloseMorphTransition(mButtonContainer); + transitionSet.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(final Transition transition) { + } + + @Override + public void onTransitionEnd(final Transition transition) { + if (listener != null) { + listener.onCloseAnimationEnd(); + } + mIsAnimating = false; + } + + @Override + public void onTransitionCancel(final Transition transition) { + } + + @Override + public void onTransitionPause(final Transition transition) { + } + + @Override + public void onTransitionResume(final Transition transition) { + } + }); + + TransitionManager.beginDelayedTransition(this, transitionSet); + } + animateButtons(false); + animateBackground(false); + mIsOpen = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mSeparator != null) { + ViewGroup.LayoutParams separatorLayoutParams = mSeparator.getLayoutParams(); + separatorLayoutParams.width = mFab.getMeasuredWidth(); + separatorLayoutParams.height = mFab.getMeasuredHeight(); + mSeparator.setLayoutParams(separatorLayoutParams); + } + } + + private void animateBackground(final boolean isOpen) { + ViewGroup.LayoutParams backgroundLayoutParams = mBackground.getLayoutParams(); + backgroundLayoutParams.width = isOpen ? mButtonContainer.getMeasuredWidth() : NO_DIMENSION; + mBackground.setLayoutParams(backgroundLayoutParams); + } + + private void openCompatAnimation() { + ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 1.0f); + anim.setDuration(30000); // duration 3 seconds + anim.start(); + } + + + private void closeCompatAnimation() { + ObjectAnimator anim = ObjectAnimator.ofFloat(mBackground, "scaleX", 0.0f); + anim.setDuration(3000); + anim.start(); + animateButtons(false); + } + + private void animateButtons(boolean isOpen) { + for (int i = 0; i < mButtonContainer.getChildCount(); i++) { + mButtonContainer.getChildAt(i).setScaleX(isOpen ? 1 : 0); + mButtonContainer.getChildAt(i).setScaleY(isOpen ? 1 : 0); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private class OpenMorphTransition extends TransitionSet { + OpenMorphTransition(ViewGroup viewGroup) { + + ChangeBounds changeBound = new ChangeBounds(); + changeBound.excludeChildren(R.id.faboptions_button_container, true); + addTransition(changeBound); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + ChangeTransform changeTransform = new ChangeTransform(); + for (int i = 0; i < viewGroup.getChildCount(); i++) { + changeTransform.addTarget(viewGroup.getChildAt(i)); + } + addTransition(changeTransform); + } + + setOrdering(TransitionSet.ORDERING_SEQUENTIAL); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private class CloseMorphTransition extends TransitionSet { + CloseMorphTransition(ViewGroup viewGroup) { + + ChangeBounds changeBound = new ChangeBounds(); + changeBound.excludeChildren(R.id.faboptions_button_container, true); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + ChangeTransform changeTransform = new ChangeTransform(); + for (int i = 0; i < viewGroup.getChildCount(); i++) { + changeTransform.addTarget(viewGroup.getChildAt(i)); + } + changeTransform.setDuration(CLOSE_MORPH_TRANSFORM_DURATION); + addTransition(changeTransform); + } + + addTransition(changeBound); + setOrdering(TransitionSet.ORDERING_TOGETHER); + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/joaquimley/faboptions/FabOptionsAnimationStateListener.java b/library/src/main/java/com/joaquimley/faboptions/FabOptionsAnimationStateListener.java new file mode 100644 index 0000000..d017312 --- /dev/null +++ b/library/src/main/java/com/joaquimley/faboptions/FabOptionsAnimationStateListener.java @@ -0,0 +1,11 @@ +package com.joaquimley.faboptions; + +/** + * FabOptions exposed listener for the expand and collapse animations + */ + +public interface FabOptionsAnimationStateListener { + void onOpenAnimationEnd(); + + void onCloseAnimationEnd(); +} \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 5cccdc7..7fa2010 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -24,8 +24,8 @@ android { applicationId "com.joaquimley.faboptions.sample" minSdkVersion 14 targetSdkVersion 27 - versionCode 6 - versionName "1.0.1" + versionCode 7 + versionName "1.0.2" } buildTypes { @@ -38,9 +38,9 @@ android { dependencies { final DESIGN_LIBRARY_VERSION = '27.0.0' - final FABOPTIONS_VERSION = '1.1.1' + final FABOPTIONS_VERSION = '1.2.0' - compile "com.android.support:design:$DESIGN_LIBRARY_VERSION" + implementation "com.android.support:design:$DESIGN_LIBRARY_VERSION" // compile "com.github.joaquimley:faboptions:$FABOPTIONS_VERSION" - compile project(':library') + implementation project(':library') } diff --git a/sample/src/main/java/com/joaquimley/sample/JavaSampleActivity.java b/sample/src/main/java/com/joaquimley/sample/JavaSampleActivity.java index 938fe72..a5c28d9 100644 --- a/sample/src/main/java/com/joaquimley/sample/JavaSampleActivity.java +++ b/sample/src/main/java/com/joaquimley/sample/JavaSampleActivity.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.os.Bundle; import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; @@ -39,73 +40,73 @@ public class JavaSampleActivity extends AppCompatActivity implements View.OnClickListener { - private Toolbar mToolbar; - - public static Intent newStartIntent(Context context) { - return new Intent(context, JavaSampleActivity.class); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_sample_java); - mToolbar = (Toolbar) findViewById(toolbar); - mToolbar.setTitle(getString(R.string.title_activity_java)); - setSupportActionBar(mToolbar); - - FabOptions fabOptions = (FabOptions) findViewById(R.id.fab_options); - fabOptions.setButtonsMenu(R.menu.menu_faboptions); - fabOptions.setBackgroundColor(R.color.colorPrimaryDark); - fabOptions.setFabColor(R.color.colorAccent); - fabOptions.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - switch (view.getId()) { - case R.id.faboptions_favorite: - Toast.makeText(JavaSampleActivity.this, "Favorite", Toast.LENGTH_SHORT).show(); - break; - - case R.id.faboptions_textsms: - Toast.makeText(JavaSampleActivity.this, "Message", Toast.LENGTH_SHORT).show(); - break; - - - case R.id.faboptions_download: - Toast.makeText(JavaSampleActivity.this, "Download", Toast.LENGTH_SHORT).show(); - break; - - case R.id.faboptions_share: - Toast.makeText(JavaSampleActivity.this, "Share", Toast.LENGTH_SHORT).show(); - break; - - default: - // no-op - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_activity_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_snackbar_test: - Snackbar.make(mToolbar, getString(R.string.action_snackbar_test_message), - Snackbar.LENGTH_LONG).show(); - return true; - - case R.id.action_change_activity: - startActivity(XmlSampleActivity.newStartIntent(JavaSampleActivity.this)); - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } + private Toolbar mToolbar; + + public static void start(Context context) { + context.startActivity(new Intent(context, JavaSampleActivity.class)); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sample_java); + mToolbar = findViewById(toolbar); + mToolbar.setTitle(getString(R.string.title_activity_java)); + setSupportActionBar(mToolbar); + + FabOptions fabOptions = findViewById(R.id.fab_options); + fabOptions.setButtonsMenu(R.menu.menu_faboptions); + fabOptions.setBackgroundColor(this, ContextCompat.getColor(this, R.color.colorPrimaryDark)); + fabOptions.setFabColor(R.color.colorAccent); + fabOptions.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.faboptions_favorite: + Toast.makeText(JavaSampleActivity.this, "Favorite", Toast.LENGTH_SHORT).show(); + break; + + case R.id.faboptions_textsms: + Toast.makeText(JavaSampleActivity.this, "Message", Toast.LENGTH_SHORT).show(); + break; + + + case R.id.faboptions_download: + Toast.makeText(JavaSampleActivity.this, "Download", Toast.LENGTH_SHORT).show(); + break; + + case R.id.faboptions_share: + Toast.makeText(JavaSampleActivity.this, "Share", Toast.LENGTH_SHORT).show(); + break; + + default: + // no-op + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_activity_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_snackbar_test: + Snackbar.make(mToolbar, getString(R.string.action_snackbar_test_message), + Snackbar.LENGTH_LONG).show(); + return true; + + case R.id.action_change_activity: + XmlSampleActivity.start(JavaSampleActivity.this); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } } \ No newline at end of file diff --git a/sample/src/main/java/com/joaquimley/sample/XmlSampleActivity.java b/sample/src/main/java/com/joaquimley/sample/XmlSampleActivity.java index 2449c70..5339398 100644 --- a/sample/src/main/java/com/joaquimley/sample/XmlSampleActivity.java +++ b/sample/src/main/java/com/joaquimley/sample/XmlSampleActivity.java @@ -23,7 +23,6 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDelegate; import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -41,79 +40,80 @@ public class XmlSampleActivity extends AppCompatActivity implements View.OnClickListener { - static { - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); - } - - private Toolbar mToolbar; - private FabOptions mFabOptions; - - public static Intent newStartIntent(Context context) { - return new Intent(context, XmlSampleActivity.class); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_sample_xml); - mToolbar = (Toolbar) findViewById(toolbar); - mToolbar.setTitle(getString(R.string.title_activity_xml)); - setSupportActionBar(mToolbar); - mFabOptions = (FabOptions) findViewById(R.id.fab_options); - mFabOptions.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - switch (view.getId()) { - case R.id.faboptions_favorite: - mFabOptions.setButtonColor(R.id.faboptions_favorite, R.color.colorAccent); - Toast.makeText(XmlSampleActivity.this, "Favorite", Toast.LENGTH_SHORT).show(); - break; - - case R.id.faboptions_textsms: - mFabOptions.setButtonColor(R.id.faboptions_textsms, R.color.colorAccent); - Toast.makeText(XmlSampleActivity.this, "Message", Toast.LENGTH_SHORT).show(); - break; - - - case R.id.faboptions_download: - mFabOptions.setButtonColor(R.id.faboptions_download, R.color.colorAccent); - Toast.makeText(XmlSampleActivity.this, "Download", Toast.LENGTH_SHORT).show(); - break; - - - case R.id.faboptions_share: - mFabOptions.setButtonColor(R.id.faboptions_share, R.color.colorAccent); - Toast.makeText(XmlSampleActivity.this, "Share", Toast.LENGTH_SHORT).show(); - break; - - default: - // no-op - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_activity_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_snackbar_test: - Snackbar.make(mToolbar, getString(R.string.action_snackbar_test_message), - Snackbar.LENGTH_LONG).show(); - return true; - - case R.id.action_change_activity: - startActivity(JavaSampleActivity.newStartIntent(XmlSampleActivity.this)); - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } + static { + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + } + + private Toolbar mToolbar; + private FabOptions mFabOptions; + + public static void start(Context context) { + context.startActivity(new Intent(context, XmlSampleActivity.class)); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sample_xml); + mToolbar = findViewById(toolbar); + mToolbar.setTitle(getString(R.string.title_activity_xml)); + setSupportActionBar(mToolbar); + + mFabOptions = findViewById(R.id.fab_options); + mFabOptions.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.faboptions_favorite: + mFabOptions.setButtonColor(R.id.faboptions_favorite, R.color.colorAccent); + Toast.makeText(XmlSampleActivity.this, "Favorite", Toast.LENGTH_SHORT).show(); + break; + + case R.id.faboptions_textsms: + mFabOptions.setButtonColor(R.id.faboptions_textsms, R.color.colorAccent); + Toast.makeText(XmlSampleActivity.this, "Message", Toast.LENGTH_SHORT).show(); + break; + + + case R.id.faboptions_download: + mFabOptions.setButtonColor(R.id.faboptions_download, R.color.colorAccent); + Toast.makeText(XmlSampleActivity.this, "Download", Toast.LENGTH_SHORT).show(); + break; + + + case R.id.faboptions_share: + mFabOptions.setButtonColor(R.id.faboptions_share, R.color.colorAccent); + Toast.makeText(XmlSampleActivity.this, "Share", Toast.LENGTH_SHORT).show(); + break; + + default: + // no-op + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_activity_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_snackbar_test: + Snackbar.make(mToolbar, getString(R.string.action_snackbar_test_message), + Snackbar.LENGTH_LONG).show(); + return true; + + case R.id.action_change_activity: + JavaSampleActivity.start(XmlSampleActivity.this); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } } \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_sample_xml.xml b/sample/src/main/res/layout/activity_sample_xml.xml index 3611780..d32eb2a 100644 --- a/sample/src/main/res/layout/activity_sample_xml.xml +++ b/sample/src/main/res/layout/activity_sample_xml.xml @@ -48,7 +48,7 @@ android:id="@+id/fab_options" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="bottom" + android:layout_gravity="center_horizontal|bottom" app:button_menu="@menu/menu_faboptions" app:background_color="@color/colorPrimary" app:fab_color="@color/colorPrimaryDark" />