(this,
47 | R.layout.simple_list_item_2,
48 | R.id.text1,
49 | titles) {
50 | @Override
51 | public View getView(int position, View convertView, ViewGroup parent) {
52 | View view = super.getView(position, convertView, parent);
53 | TextView text1 = (TextView) view.findViewById(R.id.text1);
54 | TextView text2 = (TextView) view.findViewById(R.id.text2);
55 |
56 | text1.setText(titles[position]);
57 | text2.setText(descriptions[position]);
58 | return view;
59 | }
60 | };
61 | ListView listView = (ListView) findViewById(R.id.list_view);
62 | listView.setAdapter(adapter);
63 | listView.setOnItemClickListener(this);
64 | }
65 |
66 | @Override
67 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
68 | final Class extends BaseSampleActivity> newActivity;
69 | switch (position) {
70 | default:
71 | case 0 :
72 | newActivity = PositionSampleActivity.class;
73 | break;
74 | case 1:
75 | newActivity = GroupStyleSampleActivity.class;
76 | break;
77 | case 2:
78 | newActivity = IndividualStyleSampleActivity.class;
79 | break;
80 | case 3:
81 | newActivity = EventsSampleActivity.class;
82 | break;
83 | case 4:
84 | newActivity = MenuItemsSampleActivity.class;
85 | break;
86 | }
87 | Intent i = new Intent(HomeActivity.this, newActivity);
88 | i.putExtra(BaseSampleActivity.TITLE, titles[position]);
89 | startActivity(i);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/samples/src/main/res/layout/activity_position_sample.xml:
--------------------------------------------------------------------------------
1 |
16 |
24 |
25 |
29 |
30 |
36 |
37 |
38 |
39 |
44 |
45 |
53 |
54 |
62 |
63 |
71 |
72 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/samples/src/main/res/layout/activity_menu_items_sample.xml:
--------------------------------------------------------------------------------
1 |
16 |
24 |
25 |
29 |
30 |
36 |
37 |
38 |
39 |
48 |
49 |
53 |
54 |
59 |
60 |
65 |
66 |
71 |
72 |
76 |
77 |
82 |
83 |
84 |
85 |
89 |
90 |
94 |
95 |
100 |
101 |
102 |
103 |
107 |
108 |
112 |
113 |
118 |
119 |
120 |
121 |
122 |
123 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/library/src/main/java/io/github/yavski/fabspeeddial/FabSpeedDialBehaviour.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Yavor Ivanov
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.github.yavski.fabspeeddial;
18 |
19 | import android.content.Context;
20 | import android.graphics.Rect;
21 | import android.os.Build;
22 | import android.support.design.widget.AppBarLayout;
23 | import android.support.design.widget.CoordinatorLayout;
24 | import android.support.design.widget.Snackbar;
25 | import android.support.v4.view.ViewCompat;
26 | import android.support.v4.view.ViewPropertyAnimatorCompat;
27 | import android.util.AttributeSet;
28 | import android.view.View;
29 |
30 | import java.lang.reflect.InvocationTargetException;
31 | import java.lang.reflect.Method;
32 | import java.util.List;
33 |
34 |
35 | /**
36 | * Copyright (C) 2015 The Android Open Source Project
37 | *
38 | * Licensed under the Apache License, Version 2.0 (the "License");
39 | * you may not use this file except in compliance with the License.
40 | * You may obtain a copy of the License at
41 | *
42 | * http://www.apache.org/licenses/LICENSE-2.0
43 | *
44 | * Unless required by applicable law or agreed to in writing, software
45 | * distributed under the License is distributed on an "AS IS" BASIS,
46 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
47 | * See the License for the specific language governing permissions and
48 | * limitations under the License.
49 | *
50 | *
51 | * A slightly modified version of the original
52 | * {@link android.support.design.widget.FloatingActionButton.Behavior}.
53 | *
54 | * Created by yavorivanov on 03/01/2016.
55 | */
56 | public class FabSpeedDialBehaviour extends CoordinatorLayout.Behavior {
57 |
58 | // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
59 | // because we can use view translation properties which greatly simplifies the code.
60 | private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;
61 |
62 | private ViewPropertyAnimatorCompat mFabTranslationYAnimator;
63 | private float mFabTranslationY;
64 | private Rect mTmpRect;
65 |
66 | public FabSpeedDialBehaviour() {
67 |
68 | }
69 |
70 | public FabSpeedDialBehaviour(Context context, AttributeSet attrs) {
71 | super(context, attrs);
72 | }
73 |
74 | @Override
75 | public boolean layoutDependsOn(CoordinatorLayout parent, FabSpeedDial child, View dependency) {
76 | // We're dependent on all SnackbarLayouts (if enabled)
77 | return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
78 | }
79 |
80 | @Override
81 | public boolean onDependentViewChanged(CoordinatorLayout parent, FabSpeedDial child,
82 | View dependency) {
83 | if (dependency instanceof Snackbar.SnackbarLayout) {
84 | updateFabTranslationForSnackbar(parent, child, dependency);
85 | } else if (dependency instanceof AppBarLayout) {
86 | // If we're depending on an AppBarLayout we will show/hide it automatically
87 | // if the FAB is anchored to the AppBarLayout
88 | updateFabVisibility(parent, (AppBarLayout) dependency, child);
89 | }
90 | return false;
91 | }
92 |
93 | private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
94 | final FabSpeedDial fab, View snackbar) {
95 | if (fab.getVisibility() != View.VISIBLE) {
96 | return;
97 | }
98 |
99 | final float targetTransY = getFabTranslationYForSnackbar(parent, fab);
100 | if (mFabTranslationY == targetTransY) {
101 | // We're already at (or currently animating to) the target value, return...
102 | return;
103 | }
104 |
105 | final float currentTransY = ViewCompat.getTranslationY(fab);
106 |
107 | // Make sure that any current animation is cancelled
108 | if (mFabTranslationYAnimator != null) {
109 | mFabTranslationYAnimator.cancel();
110 | }
111 |
112 | if (Math.abs(currentTransY - targetTransY) > (fab.getHeight() * 0.667f)) {
113 | mFabTranslationYAnimator = ViewCompat.animate(fab)
114 | .setInterpolator(FabSpeedDial.FAST_OUT_SLOW_IN_INTERPOLATOR)
115 | .translationY(targetTransY);
116 | mFabTranslationYAnimator.start();
117 | } else {
118 | ViewCompat.setTranslationY(fab, targetTransY);
119 | }
120 |
121 | mFabTranslationY = targetTransY;
122 | }
123 |
124 | private float getFabTranslationYForSnackbar(CoordinatorLayout parent,
125 | FabSpeedDial fab) {
126 | float minOffset = 0;
127 | final List dependencies = parent.getDependencies(fab);
128 | for (int i = 0, z = dependencies.size(); i < z; i++) {
129 | final View view = dependencies.get(i);
130 | if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
131 | minOffset = Math.min(minOffset,
132 | ViewCompat.getTranslationY(view) - view.getHeight());
133 | }
134 | }
135 |
136 | return minOffset;
137 | }
138 |
139 | @Override
140 | public boolean onLayoutChild(CoordinatorLayout parent, FabSpeedDial child, int layoutDirection) {
141 | // First, lets make sure that the visibility of the FAB is consistent
142 | final List dependencies = parent.getDependencies(child);
143 | for (int i = 0, count = dependencies.size(); i < count; i++) {
144 | final View dependency = dependencies.get(i);
145 | if (dependency instanceof AppBarLayout
146 | && updateFabVisibility(parent, (AppBarLayout) dependency, child)) {
147 | break;
148 | }
149 | }
150 | // Now let the CoordinatorLayout lay out the FAB
151 | parent.onLayoutChild(child, layoutDirection);
152 | return true;
153 | }
154 |
155 | private boolean updateFabVisibility(CoordinatorLayout parent, AppBarLayout appBarLayout,
156 | FabSpeedDial child) {
157 | final CoordinatorLayout.LayoutParams lp =
158 | (CoordinatorLayout.LayoutParams) child.getLayoutParams();
159 | if (lp.getAnchorId() != appBarLayout.getId()) {
160 | // The anchor ID doesn't match the dependency, so we won't automatically
161 | // show/hide the FAB
162 | return false;
163 | }
164 |
165 | if (mTmpRect == null) {
166 | mTmpRect = new Rect();
167 | }
168 |
169 | // First, let's get the visible rect of the dependency
170 | final Rect rect = mTmpRect;
171 | ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);
172 |
173 | /**
174 | * TODO: Remove reflection and replace with
175 | * AppBarLayout#getMinimumHeightForVisibleOverlappingContent() once made publuc
176 | */
177 | int minimumHeightForVisibleOverlappingContent =
178 | getMinimumHeightForVisibleOverlappingContent(appBarLayout);
179 | if (minimumHeightForVisibleOverlappingContent == -1) { // the api has changed, return
180 | return true;
181 | }
182 | if (rect.bottom <= minimumHeightForVisibleOverlappingContent) {
183 | // If the anchor's bottom is below the seam, we'll animate our FAB out
184 | // child.hide();
185 | } else {
186 | // Else, we'll animate our FAB back in
187 | // child.show();
188 | }
189 | return true;
190 | }
191 |
192 | private int getMinimumHeightForVisibleOverlappingContent(AppBarLayout appBarLayout) {
193 | try {
194 | Method method = appBarLayout.getClass().getDeclaredMethod("getMinimumHeightForVisibleOverlappingContent");
195 | method.setAccessible(true);
196 | Object value = method.invoke(appBarLayout, null);
197 | return (int) value;
198 | } catch (NoSuchMethodException e) {
199 | e.printStackTrace();
200 | } catch (InvocationTargetException e) {
201 | e.printStackTrace();
202 | } catch (IllegalAccessException e) {
203 | e.printStackTrace();
204 | }
205 | return -1;
206 | }
207 |
208 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### fab-speed-dial
2 |
3 | [](https://android-arsenal.com/details/1/3062) [](http://www.android-gems.com/lib/yavski/fab-speed-dial)
4 |
5 | A simple library marrying together [FAB] (http://developer.android.com/reference/android/support/design/widget/FloatingActionButton.html) + [menu resources] (http://developer.android.com/guide/topics/resources/menu-resource.html) + [Speed dial metaphor from Material Design] (https://www.google.com/design/spec/components/buttons-floating-action-button.html#buttons-floating-action-button-transitions).
6 |
7 | Similarly tо [NavigationView] (http://developer.android.com/reference/android/support/design/widget/NavigationView.html?utm_campaign=io15&utm_source=dac&utm_medium=blog) and [ActionBar] (http://developer.android.com/reference/android/app/ActionBar.html), [FabSpeedDial] (https://github.com/yavski/fab-speed-dial/blob/master/library/src/main/java/io/github/yavski/fabspeeddial/FabSpeedDial.java) makes use of [menu resources] (http://developer.android.com/guide/topics/resources/menu-resource.html) in order to present a list of actionable buttons. This makes the library somewhat familiar to use and easy to integrate. The library runs on Android 2.2 (API 8) onwards.
8 |
9 | ### Gettting started
10 |
11 | ##### Add the dependency to gradle.build
12 | ```
13 | dependencies {
14 | compile 'io.github.yavski:fab-speed-dial:1.0.6'
15 | }
16 | ```
17 |
18 | ##### Define a menu resource
19 | ```
20 |
35 | ```
36 |
37 | ##### Add FabSpeedDial to your layout xml
38 | ```
39 |
44 |
45 |
54 |
55 |
56 | ```
57 | ##### Result
58 |
59 |
60 | In order to save the menu open/closed state, **you must define an id to the FabSpeedDial**, otherwise if you rotate the device the state won't be persisted.
61 |
62 | ##### Events
63 |
64 | As with all menus, you have a callback just before the list of actionable items are presented. The callback allows you to update your menu items, or not show the menu altogether.
65 |
66 | ```
67 | FabSpeedDial fabSpeedDial = (FabSpeedDial) findViewById(R.id.fab_speed_dial);
68 | fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() {
69 | @Override
70 | public boolean onPrepareMenu(NavigationMenu navigationMenu) {
71 | // TODO: Do something with yout menu items, or return false if you don't want to show them
72 | return true;
73 | }
74 | });
75 | ```
76 |
77 | Similarly, in order to be notified about a selection:
78 |
79 | ```
80 | FabSpeedDial fabSpeedDial = (FabSpeedDial) findViewById(R.id.fab_speed_dial);
81 | fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() {
82 | @Override
83 | public boolean onMenuItemSelected(MenuItem menuItem) {
84 | //TODO: Start some activity
85 | return false;
86 | }
87 | });
88 | ```
89 |
90 |
91 | ### Customisation
92 |
93 | ##### Multi-colored fabs
94 | Define an integer-array resource that contains the colors you want to use in the order of the items you want colored:
95 |
96 | ```
97 |
98 |
99 | - @android:color/holo_red_dark
100 |
101 | - @android:color/holo_purple
102 |
103 | - @android:color/holo_green_light
104 |
105 | ```
106 |
107 | Make sure you also assign [android:orderInCategory](https://developer.android.com/guide/topics/resources/menu-resource.html) to each menu item in your menu.xml in the same 0-based order.
108 |
109 | To provide the color array to the menu items use the following attributes: ```miniFabTitleTextColorList``` and ```miniFabBackgroundTintList```. Note - if you've used ```miniFabTitleTextColor``` and ```miniFabBackgroundTint``` they will be overriden by the color arrays.
110 |
111 | ##### Result
112 |
113 |
114 | ##### Position
115 | In order to change the position of the view, use the standard android APIs to position *FabSpeedDial* within your *ViewGroup* and be sure to assign ```fabGravity``` a relevant value.
116 |
117 |
118 |
119 |
120 |
121 | ##### Basic styling
122 | As a rule of thumb, attributes prepended with *fab*, i.e. ```fabDrawable```, refer to the normsal-sized FAB view; attribtues prepended with *miniFab* refer to the mini-sized FAB views in the list.
123 |
124 | The following attribtues are supported:
125 |
126 | | FabSpeedDial| Android | Desscription |
127 | | ------------- |-------------|-----|
128 | | app:fabDrawable | [android:src](http://developer.android.com/reference/android/widget/ImageView.html#attr_android:src) | Sets the icon drawable of the main FAB |
129 | | app:fabDrawableTint | [android:tint](http://developer.android.com/reference/android/widget/ImageView.html#attr_android:tint) | Tints the icon drawable of the main FAB |
130 | | app:fabBackgroundTint | [android:backgroundTint](http://developer.android.com/reference/android/view/View.html#attr_android:backgroundTint) | Tints the background colour of the main FAB |
131 | | app:miniFabDrawableTint | [android:tint](http://developer.android.com/reference/android/widget/ImageView.html#attr_android:tint) | Tints the icon drawable of the mini FAB(s) |
132 | | app:miniFabBackgroundTint | [android:backgroundTint](http://developer.android.com/reference/android/view/View.html#attr_android:backgroundTint) | Tints the background colour of the mini FAB(s) |
133 | |app:miniFabBackgroundTintList | | An array containing the background colors for each of the mini FABs.
134 | |app:miniFabTitleTextColor| [android:textColor] (https://developer.android.com/reference/android/widget/TextView.html#attr_android:textColor) | Sets the color of the title of the mini FAB. |
135 | |app:miniFabTitleTextColorList|| An array containing the colors for each of the titles of the mini FABs.
136 | | app:miniFabTitleBackgroundTint | [android:backgroundTint](http://developer.android.com/reference/android/view/View.html#attr_android:backgroundTint) | Tints the background colour of the title(s) of the mini FAB(s) |
137 | | app:miniFabTitlesEnabled | | Convinience for hiding the tilte(s) of the mini FAB(s) |
138 | | app:touchGuard | | Hide FAB when touching out of its bounds |
139 | | app:touchGuardDrawable | [android:background](http://developer.android.com/reference/android/view/View.html#setBackground(android.graphics.drawable.Drawable)) | Sets background to the container of FAB
140 |
141 | ### Caveats
142 |
143 | If you have used FloatingActionButton, CoordinatorLayout, and both combined, you are most probably aware that:
144 | * Internally, FAB has two main implementations: one for SDK >= 21, one for earlier versions; **the one for older versions uses extra padding in order to draw shadows; you don't need to account for the extra padding** as the library takes care of it however do check your layouts/dimensions to avoid mis-positioned views.
145 | * When used in a CoordinatorLayout, FAB is known to have its margin values ignored / misused under certain circumstances; as a workaround the library always adds left or right margin values (depending on gravity), taking into account the SDK version too.
146 |
147 | ### License
148 | ```
149 |
164 | ```
165 |
166 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 Yavor Ivanov
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/library/src/main/java/io/github/yavski/fabspeeddial/FabSpeedDial.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Yavor Ivanov
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.github.yavski.fabspeeddial;
18 |
19 | import android.annotation.TargetApi;
20 | import android.content.Context;
21 | import android.content.res.ColorStateList;
22 | import android.content.res.TypedArray;
23 | import android.graphics.Typeface;
24 | import android.graphics.drawable.Drawable;
25 | import android.os.Build;
26 | import android.os.Parcel;
27 | import android.os.Parcelable;
28 | import android.support.design.internal.NavigationMenu;
29 | import android.support.design.widget.CoordinatorLayout;
30 | import android.support.design.widget.FloatingActionButton;
31 | import android.support.v4.content.ContextCompat;
32 | import android.support.v4.view.ViewCompat;
33 | import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
34 | import android.support.v4.view.animation.FastOutLinearInInterpolator;
35 | import android.support.v4.view.animation.FastOutSlowInInterpolator;
36 | import android.support.v7.view.SupportMenuInflater;
37 | import android.support.v7.view.menu.MenuBuilder;
38 | import android.support.v7.widget.CardView;
39 | import android.text.TextUtils;
40 | import android.util.AndroidRuntimeException;
41 | import android.util.AttributeSet;
42 | import android.util.Log;
43 | import android.view.Gravity;
44 | import android.view.KeyEvent;
45 | import android.view.LayoutInflater;
46 | import android.view.MenuItem;
47 | import android.view.View;
48 | import android.view.ViewGroup;
49 | import android.view.ViewParent;
50 | import android.widget.FrameLayout;
51 | import android.widget.LinearLayout;
52 | import android.widget.RelativeLayout;
53 | import android.widget.TextView;
54 |
55 | import java.util.HashMap;
56 | import java.util.Map;
57 |
58 | import io.github.yavski.fabmenu.R;
59 |
60 | /**
61 | * Created by yavorivanov on 01/01/2016.
62 | */
63 | @CoordinatorLayout.DefaultBehavior(FabSpeedDialBehaviour.class)
64 | public class FabSpeedDial extends LinearLayout implements View.OnClickListener {
65 |
66 | /**
67 | * Called to notify of close and selection changes.
68 | */
69 | public interface MenuListener {
70 |
71 | /**
72 | * Called just before the menu items are about to become visible.
73 | * Don't block as it's called on the main thread.
74 | *
75 | * @param navigationMenu The menu containing all menu items.
76 | * @return You must return true for the menu to be displayed;
77 | * if you return false it will not be shown.
78 | */
79 | boolean onPrepareMenu(NavigationMenu navigationMenu);
80 |
81 | /**
82 | * Called when a menu item is selected.
83 | *
84 | * @param menuItem The menu item that is selected
85 | * @return whether the menu item selection was handled
86 | */
87 | boolean onMenuItemSelected(MenuItem menuItem);
88 |
89 | void onMenuClosed();
90 | }
91 |
92 | private static final String TAG = FabSpeedDial.class.getSimpleName();
93 |
94 | private static final int VSYNC_RHYTHM = 16;
95 |
96 | public static final FastOutSlowInInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR =
97 | new FastOutSlowInInterpolator();
98 |
99 | public static final int BOTTOM_END = 0;
100 | public static final int BOTTOM_START = 1;
101 | public static final int TOP_END = 2;
102 | public static final int TOP_START = 3;
103 | private static final int DEFAULT_MENU_POSITION = BOTTOM_END;
104 |
105 | private MenuListener menuListener;
106 | private NavigationMenu navigationMenu;
107 | private Map fabMenuItemMap;
108 | private Map cardViewMenuItemMap;
109 |
110 | private LinearLayout menuItemsLayout;
111 | FloatingActionButton fab;
112 | private View touchGuard = null;
113 |
114 | private int menuId;
115 | private int fabGravity;
116 | private Drawable fabDrawable;
117 | private ColorStateList fabDrawableTint;
118 | private ColorStateList fabBackgroundTint;
119 | private ColorStateList miniFabDrawableTint;
120 | private ColorStateList miniFabBackgroundTint;
121 | private int[] miniFabBackgroundTintArray;
122 | private ColorStateList miniFabTitleBackgroundTint;
123 | private boolean miniFabTitlesEnabled;
124 | private int miniFabTitleTextColor;
125 | private int[] miniFabTitleTextColorArray;
126 | private Drawable touchGuardDrawable;
127 | private boolean useTouchGuard;
128 |
129 | private boolean isAnimating;
130 |
131 | // Variable to hold whether the menu was open or not on config change
132 | private boolean shouldOpenMenu;
133 |
134 | private FabSpeedDial(Context context) {
135 | super(context);
136 | }
137 |
138 | public FabSpeedDial(Context context, AttributeSet attrs) {
139 | super(context, attrs);
140 | init(context, attrs);
141 | }
142 |
143 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
144 | public FabSpeedDial(Context context, AttributeSet attrs, int defStyleAttr) {
145 | super(context, attrs, defStyleAttr);
146 | init(context, attrs);
147 | }
148 |
149 | private void init(Context context, AttributeSet attrs) {
150 | TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FabSpeedDial, 0, 0);
151 | resolveCompulsoryAttributes(typedArray);
152 | resolveOptionalAttributes(typedArray);
153 | typedArray.recycle();
154 |
155 | if (isGravityBottom()) {
156 | LayoutInflater.from(context).inflate(R.layout.fab_speed_dial_bottom, this, true);
157 | } else {
158 | LayoutInflater.from(context).inflate(R.layout.fab_speed_dial_top, this, true);
159 | }
160 |
161 | if (isGravityEnd()) {
162 | setGravity(Gravity.END);
163 | }
164 |
165 | menuItemsLayout = (LinearLayout) findViewById(R.id.menu_items_layout);
166 |
167 | setOrientation(VERTICAL);
168 |
169 | newNavigationMenu();
170 |
171 | int menuItemCount = navigationMenu.size();
172 | fabMenuItemMap = new HashMap<>(menuItemCount);
173 | cardViewMenuItemMap = new HashMap<>(menuItemCount);
174 | }
175 |
176 | private void resolveCompulsoryAttributes(TypedArray typedArray) {
177 | if (typedArray.hasValue(R.styleable.FabSpeedDial_fabMenu)) {
178 | menuId = typedArray.getResourceId(R.styleable.FabSpeedDial_fabMenu, 0);
179 | } else {
180 | throw new AndroidRuntimeException("You must provide the id of the menu resource.");
181 | }
182 |
183 | if (typedArray.hasValue(R.styleable.FabSpeedDial_fabGravity)) {
184 | fabGravity = typedArray.getInt(R.styleable.FabSpeedDial_fabGravity, DEFAULT_MENU_POSITION);
185 | } else {
186 | throw new AndroidRuntimeException("You must specify the gravity of the Fab.");
187 | }
188 | }
189 |
190 | private void resolveOptionalAttributes(TypedArray typedArray) {
191 | fabDrawable = typedArray.getDrawable(R.styleable.FabSpeedDial_fabDrawable);
192 | if (fabDrawable == null) {
193 | fabDrawable = ContextCompat.getDrawable(getContext(), R.drawable.fab_add_clear_selector);
194 | }
195 |
196 | fabDrawableTint = typedArray.getColorStateList(R.styleable.FabSpeedDial_fabDrawableTint);
197 | if (fabDrawableTint == null) {
198 | fabDrawableTint = getColorStateList(R.color.fab_drawable_tint);
199 | }
200 |
201 | if (typedArray.hasValue(R.styleable.FabSpeedDial_fabBackgroundTint)) {
202 | fabBackgroundTint = typedArray.getColorStateList(R.styleable.FabSpeedDial_fabBackgroundTint);
203 | }
204 |
205 | miniFabBackgroundTint = typedArray.getColorStateList(R.styleable.FabSpeedDial_miniFabBackgroundTint);
206 | if (miniFabBackgroundTint == null) {
207 | miniFabBackgroundTint = getColorStateList(R.color.fab_background_tint);
208 | }
209 |
210 | if (typedArray.hasValue(R.styleable.FabSpeedDial_miniFabBackgroundTintList)) {
211 | int miniFabBackgroundTintListId = typedArray.getResourceId(R.styleable.FabSpeedDial_miniFabBackgroundTintList, 0);
212 | TypedArray miniFabBackgroundTintRes = getResources().obtainTypedArray(miniFabBackgroundTintListId);
213 | miniFabBackgroundTintArray = new int[miniFabBackgroundTintRes.length()];
214 | for (int i = 0; i < miniFabBackgroundTintRes.length(); i++) {
215 | miniFabBackgroundTintArray[i] = miniFabBackgroundTintRes.getResourceId(i, 0);
216 | }
217 | miniFabBackgroundTintRes.recycle();
218 | }
219 |
220 | miniFabDrawableTint = typedArray.getColorStateList(R.styleable.FabSpeedDial_miniFabDrawableTint);
221 | if (miniFabDrawableTint == null) {
222 | miniFabDrawableTint = getColorStateList(R.color.mini_fab_drawable_tint);
223 | }
224 |
225 | miniFabTitleBackgroundTint = typedArray.getColorStateList(R.styleable.FabSpeedDial_miniFabTitleBackgroundTint);
226 | if (miniFabTitleBackgroundTint == null) {
227 | miniFabTitleBackgroundTint = getColorStateList(R.color.mini_fab_title_background_tint);
228 | }
229 |
230 | miniFabTitlesEnabled = typedArray.getBoolean(R.styleable.FabSpeedDial_miniFabTitlesEnabled, true);
231 |
232 |
233 | miniFabTitleTextColor = typedArray.getColor(R.styleable.FabSpeedDial_miniFabTitleTextColor,
234 | ContextCompat.getColor(getContext(), R.color.title_text_color));
235 |
236 | if (typedArray.hasValue(R.styleable.FabSpeedDial_miniFabTitleTextColorList)) {
237 | int miniFabTitleTextColorListId = typedArray.getResourceId(R.styleable.FabSpeedDial_miniFabTitleTextColorList, 0);
238 | TypedArray miniFabTitleTextColorTa = getResources().obtainTypedArray(miniFabTitleTextColorListId);
239 | miniFabTitleTextColorArray = new int[miniFabTitleTextColorTa.length()];
240 | for (int i = 0; i < miniFabTitleTextColorTa.length(); i++) {
241 | miniFabTitleTextColorArray[i] = miniFabTitleTextColorTa.getResourceId(i, 0);
242 | }
243 | miniFabTitleTextColorTa.recycle();
244 | }
245 |
246 | touchGuardDrawable = typedArray.getDrawable(R.styleable.FabSpeedDial_touchGuardDrawable);
247 |
248 | useTouchGuard = typedArray.getBoolean(R.styleable.FabSpeedDial_touchGuard, true);
249 | }
250 |
251 | @Override
252 | protected void onAttachedToWindow() {
253 | super.onAttachedToWindow();
254 |
255 | LayoutParams layoutParams =
256 | new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
257 | int coordinatorLayoutOffset = getResources().getDimensionPixelSize(R.dimen.coordinator_layout_offset);
258 | if (fabGravity == BOTTOM_END || fabGravity == TOP_END) {
259 | layoutParams.setMargins(0, 0, coordinatorLayoutOffset, 0);
260 | } else {
261 | layoutParams.setMargins(coordinatorLayoutOffset, 0, 0, 0);
262 | }
263 | menuItemsLayout.setLayoutParams(layoutParams);
264 |
265 | // Set up the client's FAB
266 | fab = (FloatingActionButton) findViewById(R.id.fab);
267 | fab.setImageDrawable(fabDrawable);
268 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
269 | fab.setImageTintList(fabDrawableTint);
270 | }
271 | if (fabBackgroundTint != null) {
272 | fab.setBackgroundTintList(fabBackgroundTint);
273 | }
274 |
275 | fab.setOnClickListener(new OnClickListener() {
276 | @Override
277 | public void onClick(View v) {
278 | if (isAnimating) return;
279 |
280 | if (isMenuOpen()) {
281 | closeMenu();
282 | } else {
283 | openMenu();
284 | }
285 | }
286 | });
287 |
288 | // Needed in order to intercept key events
289 | setFocusableInTouchMode(true);
290 |
291 | if (useTouchGuard) {
292 | ViewParent parent = getParent();
293 |
294 | touchGuard = new View(getContext());
295 | touchGuard.setOnClickListener(this);
296 | touchGuard.setWillNotDraw(true);
297 | touchGuard.setVisibility(GONE);
298 |
299 | if (touchGuardDrawable != null) {
300 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
301 | touchGuard.setBackground(touchGuardDrawable);
302 | } else {
303 | touchGuard.setBackgroundDrawable(touchGuardDrawable);
304 | }
305 | }
306 |
307 | if (parent instanceof FrameLayout) {
308 | FrameLayout frameLayout = (FrameLayout) parent;
309 | frameLayout.addView(touchGuard);
310 | bringToFront();
311 | } else if (parent instanceof CoordinatorLayout) {
312 | CoordinatorLayout coordinatorLayout = (CoordinatorLayout) parent;
313 | coordinatorLayout.addView(touchGuard);
314 | bringToFront();
315 | } else if (parent instanceof RelativeLayout) {
316 | RelativeLayout relativeLayout = (RelativeLayout) parent;
317 | relativeLayout.addView(touchGuard,
318 | new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
319 | ViewGroup.LayoutParams.MATCH_PARENT));
320 | bringToFront();
321 | } else {
322 | Log.d(TAG, "touchGuard requires that the parent of this FabSpeedDialer be a FrameLayout or RelativeLayout");
323 | }
324 | }
325 |
326 | setOnClickListener(this);
327 |
328 | if (shouldOpenMenu)
329 | openMenu();
330 | }
331 |
332 | private void newNavigationMenu() {
333 | navigationMenu = new NavigationMenu(getContext());
334 | new SupportMenuInflater(getContext()).inflate(menuId, navigationMenu);
335 |
336 | navigationMenu.setCallback(new MenuBuilder.Callback() {
337 | @Override
338 | public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
339 | return menuListener != null && menuListener.onMenuItemSelected(item);
340 | }
341 |
342 | @Override
343 | public void onMenuModeChange(MenuBuilder menu) {
344 | }
345 | });
346 | }
347 |
348 | @Override
349 | public void onClick(View v) {
350 | fab.setSelected(false);
351 | removeFabMenuItems();
352 |
353 | if (menuListener != null) {
354 | if (v == this || v == touchGuard) {
355 | menuListener.onMenuClosed();
356 | } else if (v instanceof FloatingActionButton) {
357 | menuListener.onMenuItemSelected(fabMenuItemMap.get(v));
358 | } else if (v instanceof CardView) {
359 | menuListener.onMenuItemSelected(cardViewMenuItemMap.get(v));
360 | }
361 | } else {
362 | Log.d(TAG, "You haven't provided a MenuListener.");
363 | }
364 | }
365 |
366 | @Override
367 | protected Parcelable onSaveInstanceState() {
368 | Parcelable superState = super.onSaveInstanceState();
369 | SavedState ss = new SavedState(superState);
370 |
371 | ss.isShowingMenu = isMenuOpen();
372 |
373 | return ss;
374 | }
375 |
376 | @Override
377 | protected void onRestoreInstanceState(Parcelable state) {
378 | if (!(state instanceof SavedState)) {
379 | super.onRestoreInstanceState(state);
380 | return;
381 | }
382 |
383 | SavedState ss = (SavedState) state;
384 | super.onRestoreInstanceState(ss.getSuperState());
385 |
386 | this.shouldOpenMenu = ss.isShowingMenu;
387 | }
388 |
389 | public void setMenuListener(MenuListener menuListener) {
390 | this.menuListener = menuListener;
391 | }
392 |
393 | public boolean isMenuOpen() {
394 | return menuItemsLayout.getChildCount() > 0;
395 | }
396 |
397 | public void openMenu() {
398 | if (!ViewCompat.isAttachedToWindow(this))
399 | return;
400 | requestFocus();
401 |
402 | boolean showMenu = true;
403 | if (menuListener != null) {
404 | newNavigationMenu();
405 | showMenu = menuListener.onPrepareMenu(navigationMenu);
406 | }
407 |
408 | if (showMenu) {
409 | addMenuItems();
410 | fab.setSelected(true);
411 | } else {
412 | fab.setSelected(false);
413 | }
414 | }
415 |
416 | public void closeMenu() {
417 | if (!ViewCompat.isAttachedToWindow(this))
418 | return;
419 |
420 | if (isMenuOpen()) {
421 | fab.setSelected(false);
422 | removeFabMenuItems();
423 | if (menuListener != null) {
424 | menuListener.onMenuClosed();
425 | }
426 | }
427 | }
428 |
429 | public void show() {
430 | if (!ViewCompat.isAttachedToWindow(this))
431 | return;
432 | setVisibility(View.VISIBLE);
433 | fab.show();
434 | }
435 |
436 | public void hide() {
437 | if (!ViewCompat.isAttachedToWindow(this))
438 | return;
439 |
440 | if (isMenuOpen()) {
441 | closeMenu();
442 | }
443 | fab.hide();
444 | }
445 |
446 | private void addMenuItems() {
447 | ViewCompat.setAlpha(menuItemsLayout, 1f);
448 | for (int i = 0; i < navigationMenu.size(); i++) {
449 | MenuItem menuItem = navigationMenu.getItem(i);
450 | if (menuItem.isVisible()) {
451 | menuItemsLayout.addView(createFabMenuItem(menuItem));
452 | }
453 | }
454 | animateFabMenuItemsIn();
455 | }
456 |
457 | private View createFabMenuItem(MenuItem menuItem) {
458 | ViewGroup fabMenuItem = (ViewGroup) LayoutInflater.from(getContext())
459 | .inflate(getMenuItemLayoutId(), this, false);
460 |
461 | FloatingActionButton miniFab = (FloatingActionButton) fabMenuItem.findViewById(R.id.mini_fab);
462 | CardView cardView = (CardView) fabMenuItem.findViewById(R.id.card_view);
463 | TextView titleView = (TextView) fabMenuItem.findViewById(R.id.title_view);
464 |
465 | fabMenuItemMap.put(miniFab, menuItem);
466 | cardViewMenuItemMap.put(cardView, menuItem);
467 |
468 | miniFab.setImageDrawable(menuItem.getIcon());
469 | miniFab.setOnClickListener(this);
470 | cardView.setOnClickListener(this);
471 |
472 | ViewCompat.setAlpha(miniFab, 0f);
473 | ViewCompat.setAlpha(cardView, 0f);
474 |
475 | final CharSequence title = menuItem.getTitle();
476 | if (!TextUtils.isEmpty(title) && miniFabTitlesEnabled) {
477 | cardView.setCardBackgroundColor(miniFabTitleBackgroundTint.getDefaultColor());
478 | titleView.setText(title);
479 | titleView.setTypeface(null, Typeface.BOLD);
480 | titleView.setTextColor(miniFabTitleTextColor);
481 |
482 | if (miniFabTitleTextColorArray != null) {
483 | titleView.setTextColor(ContextCompat.getColorStateList(getContext(),
484 | miniFabTitleTextColorArray[menuItem.getOrder()]));
485 | }
486 | } else {
487 | fabMenuItem.removeView(cardView);
488 | }
489 |
490 | miniFab.setBackgroundTintList(miniFabBackgroundTint);
491 |
492 | if (miniFabBackgroundTintArray != null) {
493 | miniFab.setBackgroundTintList(ContextCompat.getColorStateList(getContext(),
494 | miniFabBackgroundTintArray[menuItem.getOrder()]));
495 | }
496 |
497 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
498 | miniFab.setImageTintList(miniFabDrawableTint);
499 | }
500 |
501 | return fabMenuItem;
502 | }
503 |
504 | private void removeFabMenuItems() {
505 | if (touchGuard != null) touchGuard.setVisibility(GONE);
506 |
507 | ViewCompat.animate(menuItemsLayout)
508 | .setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime))
509 | .alpha(0f)
510 | .setInterpolator(new FastOutLinearInInterpolator())
511 | .setListener(new ViewPropertyAnimatorListenerAdapter() {
512 | @Override
513 | public void onAnimationStart(View view) {
514 | super.onAnimationStart(view);
515 | isAnimating = true;
516 | }
517 |
518 | @Override
519 | public void onAnimationEnd(View view) {
520 | super.onAnimationEnd(view);
521 | menuItemsLayout.removeAllViews();
522 | isAnimating = false;
523 | }
524 | })
525 | .start();
526 | }
527 |
528 | private void animateFabMenuItemsIn() {
529 | if (touchGuard != null) touchGuard.setVisibility(VISIBLE);
530 |
531 | final int count = menuItemsLayout.getChildCount();
532 |
533 | if (isGravityBottom()) {
534 | for (int i = count - 1; i >= 0; i--) {
535 | final View fabMenuItem = menuItemsLayout.getChildAt(i);
536 | animateViewIn(fabMenuItem.findViewById(R.id.mini_fab), Math.abs(count - 1 - i));
537 | View cardView = fabMenuItem.findViewById(R.id.card_view);
538 | if (cardView != null) {
539 | animateViewIn(cardView, Math.abs(count - 1 - i));
540 | }
541 | }
542 | } else {
543 | for (int i = 0; i < count; i++) {
544 | final View fabMenuItem = menuItemsLayout.getChildAt(i);
545 | animateViewIn(fabMenuItem.findViewById(R.id.mini_fab), i);
546 | View cardView = fabMenuItem.findViewById(R.id.card_view);
547 | if (cardView != null) {
548 | animateViewIn(cardView, i);
549 | }
550 | }
551 | }
552 | }
553 |
554 | private void animateViewIn(final View view, int position) {
555 | final float offsetY = getResources().getDimensionPixelSize(R.dimen.keyline_1);
556 |
557 | ViewCompat.setScaleX(view, 0.25f);
558 | ViewCompat.setScaleY(view, 0.25f);
559 | ViewCompat.setY(view, ViewCompat.getY(view) + offsetY);
560 |
561 | ViewCompat.animate(view)
562 | .setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime))
563 | .scaleX(1f)
564 | .scaleY(1f)
565 | .translationYBy(-offsetY)
566 | .alpha(1f)
567 | .setStartDelay(4 * position * VSYNC_RHYTHM)
568 | .setInterpolator(new FastOutSlowInInterpolator())
569 | .setListener(new ViewPropertyAnimatorListenerAdapter() {
570 | @Override
571 | public void onAnimationStart(View view) {
572 | super.onAnimationStart(view);
573 | isAnimating = true;
574 | }
575 |
576 | @Override
577 | public void onAnimationEnd(View view) {
578 | super.onAnimationEnd(view);
579 | isAnimating = false;
580 | }
581 | })
582 | .start();
583 | }
584 |
585 | private int getMenuItemLayoutId() {
586 | if (isGravityEnd()) {
587 | return R.layout.fab_menu_item_end;
588 | } else {
589 | return R.layout.fab_menu_item_start;
590 | }
591 | }
592 |
593 | private boolean isGravityBottom() {
594 | return fabGravity == BOTTOM_END || fabGravity == BOTTOM_START;
595 | }
596 |
597 | private boolean isGravityEnd() {
598 | return fabGravity == BOTTOM_END || fabGravity == TOP_END;
599 | }
600 |
601 | private ColorStateList getColorStateList(int colorRes) {
602 | int[][] states = new int[][]{
603 | new int[]{android.R.attr.state_enabled}, // enabled
604 | new int[]{-android.R.attr.state_enabled}, // disabled
605 | new int[]{-android.R.attr.state_checked}, // unchecked
606 | new int[]{android.R.attr.state_pressed} // pressed
607 | };
608 |
609 | int color = ContextCompat.getColor(getContext(), colorRes);
610 |
611 | int[] colors = new int[]{color, color, color, color};
612 | return new ColorStateList(states, colors);
613 | }
614 |
615 | @Override
616 | public boolean dispatchKeyEventPreIme(KeyEvent event) {
617 | if (isMenuOpen()
618 | && event.getKeyCode() == KeyEvent.KEYCODE_BACK
619 | && event.getAction() == KeyEvent.ACTION_UP
620 | && event.getRepeatCount() == 0) {
621 | closeMenu();
622 | return true;
623 | }
624 |
625 | return super.dispatchKeyEventPreIme(event);
626 | }
627 |
628 | static class SavedState extends BaseSavedState {
629 |
630 | boolean isShowingMenu;
631 |
632 | public SavedState(Parcel source) {
633 | super(source);
634 | this.isShowingMenu = source.readInt() == 1;
635 | }
636 |
637 | public SavedState(Parcelable superState) {
638 | super(superState);
639 | }
640 |
641 | @Override
642 | public void writeToParcel(Parcel out, int flags) {
643 | super.writeToParcel(out, flags);
644 | out.writeInt(this.isShowingMenu ? 1 : 0);
645 | }
646 |
647 | public static final Parcelable.Creator CREATOR = new Creator() {
648 | @Override
649 | public SavedState createFromParcel(Parcel parcel) {
650 | return new SavedState(parcel);
651 | }
652 |
653 | @Override
654 | public SavedState[] newArray(int i) {
655 | return new SavedState[i];
656 | }
657 | };
658 |
659 | }
660 |
661 | }
--------------------------------------------------------------------------------