├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── it │ │ └── sephiroth │ │ └── android │ │ └── library │ │ └── floatingmenu │ │ └── app │ │ ├── ListFragment1.java │ │ ├── MainActivity.java │ │ └── MainActivity2.java │ └── res │ ├── drawable-v21 │ └── custom_item_background_selector.xml │ ├── drawable-xxhdpi │ ├── ic_facebook.png │ ├── ic_googleplus.png │ ├── ic_instagram.png │ └── ic_twitter.png │ ├── drawable │ ├── custom_item_background_selector.xml │ ├── pink_oval_shape.xml │ └── pink_oval_shape_pressed.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_main_activity2.xml │ └── fragment_main_activity2.xml │ ├── menu │ ├── main.xml │ └── main_activity2.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── floatingmenu ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── it │ │ └── sephiroth │ │ └── android │ │ └── library │ │ └── floatingmenu │ │ ├── ApiHelper.java │ │ ├── FloatingActionItem.java │ │ ├── FloatingActionItemImageView.java │ │ └── FloatingActionMenu.java │ └── res │ ├── anim-v21 │ └── action_item_raise.xml │ ├── anim │ └── action_item_raise.xml │ ├── drawable-v21 │ └── action_item_background_selector.xml │ ├── drawable-xxhdpi │ └── action_item_background.png │ ├── drawable │ └── action_item_background_selector.xml │ ├── layout │ └── floating_action_item_view.xml │ └── values │ ├── attrs.xml │ └── dimen.xml ├── gradle.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | .idea 26 | 27 | android-floating-action-menu.iml 28 | 29 | app/app.iml 30 | 31 | gradle/* 32 | gradlew 33 | 34 | gradlew.bat 35 | 36 | floatingmenu/library.iml 37 | 38 | floatingmenu/floatingmenu.iml 39 | 40 | 41 | demo.mp4 42 | 43 | graphics/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ==== 3 | 4 | 1.0.0 Initial release 5 | 1.0.1 ScrollDelegate is now public. action items can now have custom background too -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android-floating-action-menu 2 | ============================ 3 | 4 | Floating Action Menu for Android. Inspired by the Google Plus floating menu. 5 | 6 | Demo 7 | ==== 8 | 9 | [![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/mLplXAJ5vug/0.jpg)](https://www.youtube.com/watch?v=mLplXAJ5vug&feature=youtu.be) 10 | 11 | 12 | Setup 13 | ===== 14 | 15 | The simplest way to use this library is to add the library as a gradle aar dependency to your build. See the CHANGELOG.md for the latest version number. 16 | ```gradle 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | compile 'it.sephiroth.android.library.floatingmenu:floatingmenu:x.x.x' // see changelog 23 | } 24 | ``` 25 | Usage 26 | ===== 27 | 28 | In your activity create a reference to the `FloatingMenu`: 29 | 30 | ```java 31 | FloatingActionItem item1 = new FloatingActionItem.Builder(0) 32 | .withResId(R.drawable.ic_facebook) 33 | .withDelay(0) 34 | .withPadding(action_item_padding) 35 | .build(); 36 | 37 | FloatingActionItem item2 = new FloatingActionItem.Builder(1) 38 | .withResId(R.drawable.ic_googleplus) 39 | .withDelay(50) 40 | .withPadding(action_item_padding) 41 | .build(); 42 | 43 | FloatingActionItem item3 = new FloatingActionItem.Builder(2) 44 | .withResId(R.drawable.ic_twitter) 45 | .withDelay(100) 46 | .withPadding(action_item_padding) 47 | .build(); 48 | 49 | mFloatingMenu = new FloatingActionMenu 50 | .Builder(this) 51 | .addItem(item1) 52 | .addItem(item2) 53 | .addItem(item3) 54 | .withScrollDelegate(new FloatingActionMenu.AbsListViewScrollDelegate(mListView)) 55 | .withThreshold(R.dimen.float_action_threshold) 56 | .withGap(R.dimen.float_action_item_gap) 57 | .withHorizontalPadding(R.dimen.float_action_h_padding) 58 | .withVerticalPadding(R.dimen.float_action_v_padding) 59 | .withGravity(FloatingActionMenu.Gravity.CENTER_HORIZONTAL | FloatingActionMenu.Gravity.BOTTOM) 60 | .withDirection(FloatingActionMenu.Direction.Vertical) 61 | .animationDuration(300) 62 | .animationInterpolator(new OvershootInterpolator()) 63 | .visible(visible) 64 | .build(); 65 | 66 | mFloatingMenu.setOnItemClickListener(this); 67 | ``` 68 | 69 | License 70 | ======= 71 | 72 | Copyright (c) 2014 Alessandro Crugnola 73 | 74 | Licensed under the Apache License, Version 2.0 (the "License"); 75 | you may not use this file except in compliance with the License. 76 | You may obtain a copy of the License at 77 | 78 | http://www.apache.org/licenses/LICENSE-2.0 79 | 80 | Unless required by applicable law or agreed to in writing, software 81 | distributed under the License is distributed on an "AS IS" BASIS, 82 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 83 | See the License for the specific language governing permissions and 84 | limitations under the License. 85 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion 21 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile project(':floatingmenu') 24 | compile 'com.android.support:appcompat-v7:21.0.3' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/it/sephiroth/android/library/floatingmenu/app/ListFragment1.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu.app; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.ListFragment; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.view.animation.OvershootInterpolator; 10 | import android.widget.ArrayAdapter; 11 | import android.widget.Toast; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import it.sephiroth.android.library.floatingmenu.FloatingActionItem; 17 | import it.sephiroth.android.library.floatingmenu.FloatingActionMenu; 18 | import it.sephiroth.android.library.floatingmenu.FloatingActionMenu.Gravity; 19 | 20 | public class ListFragment1 extends ListFragment implements FloatingActionMenu.OnItemClickListener { 21 | private static final String TAG = "ListFragment"; 22 | public static final String ARG_SECTION_NUMBER = "section_number"; 23 | public static final String ARG_SECTION_VISIBLE = "section_visible"; 24 | private FloatingActionMenu mFloatingMenu; 25 | private int sectionNumber; 26 | 27 | public static ListFragment1 newInstance(int sectionNumber, boolean visible) { 28 | Log.i(TAG, "newInstance"); 29 | ListFragment1 fragment = new ListFragment1(); 30 | Bundle args = new Bundle(); 31 | args.putInt(ARG_SECTION_NUMBER, sectionNumber); 32 | args.putBoolean(ARG_SECTION_VISIBLE, visible); 33 | fragment.setArguments(args); 34 | return fragment; 35 | } 36 | 37 | public ListFragment1() {} 38 | 39 | @Override 40 | public View onCreateView( 41 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 42 | sectionNumber = getArguments().getInt(ARG_SECTION_NUMBER); 43 | Log.i(TAG, "onCreateView: " + sectionNumber); 44 | 45 | View rootView = inflater.inflate(R.layout.fragment_main_activity2, container, false); 46 | List data = createData(); 47 | setListAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, data)); 48 | return rootView; 49 | } 50 | 51 | @Override 52 | public void onDestroyView() { 53 | super.onDestroyView(); 54 | if (null != mFloatingMenu) { 55 | mFloatingMenu.clear(); 56 | } 57 | } 58 | 59 | @Override 60 | public void onViewCreated(final View view, final Bundle savedInstanceState) { 61 | super.onViewCreated(view, savedInstanceState); 62 | Log.i(TAG, "onViewCreated: " + sectionNumber); 63 | initFloatingMenu(sectionNumber, savedInstanceState); 64 | 65 | if (null != mFloatingMenu) { 66 | view.postDelayed( 67 | new Runnable() { 68 | @Override 69 | public void run() { 70 | int currentItem = ((MainActivity2) getActivity()).mViewPager.getCurrentItem(); 71 | if (currentItem == sectionNumber) { 72 | mFloatingMenu.show(true, true); 73 | } 74 | } 75 | }, 400); 76 | } 77 | } 78 | 79 | private void initFloatingMenu(final int sectionNumber, final Bundle savedInstanceState) { 80 | int currentItem = ((MainActivity2) getActivity()).mViewPager.getCurrentItem(); 81 | 82 | FloatingActionMenu.Builder builder = createMenu(sectionNumber); 83 | 84 | FloatingActionItem item1 = new FloatingActionItem.Builder(getActivity(), 0, R.style.FloatingActionMenuItemStyle) 85 | .withResId(R.drawable.ic_facebook) 86 | .build(); 87 | 88 | FloatingActionItem item2 = new FloatingActionItem.Builder(getActivity(), 1, R.style.FloatingActionMenuItemStyle) 89 | .withResId(R.drawable.ic_twitter) 90 | .withDelay(sectionNumber == 3 ? 50 : 0) 91 | .build(); 92 | 93 | FloatingActionItem item3 = new FloatingActionItem.Builder(getActivity(), 2, R.style.FloatingActionMenuItemStyle) 94 | .withResId(R.drawable.ic_facebook) 95 | .withDelay(sectionNumber == 3 ? 100 : 0) 96 | .build(); 97 | 98 | FloatingActionItem item4 = new FloatingActionItem.Builder(getActivity(), 3, R.style.FloatingActionMenuItemStyle) 99 | .withResId(R.drawable.ic_instagram) 100 | .withDelay(sectionNumber == 3 ? 150 : 0) 101 | .build(); 102 | 103 | if (sectionNumber == 0) { 104 | builder.addItem(item1); 105 | } else if (sectionNumber == 1) { 106 | builder.addItem(item1).addItem(item2); 107 | } else if (sectionNumber == 2) { 108 | builder.addItem(item1).addItem(item2).addItem(item3); 109 | } else if (sectionNumber == 3) { 110 | builder.addItem(item1).addItem(item2).addItem(item3).addItem(item4); 111 | } 112 | 113 | mFloatingMenu = builder.build(); 114 | mFloatingMenu.setOnItemClickListener(this); 115 | } 116 | 117 | private FloatingActionMenu.Builder createMenu(int sectionNumber) { 118 | FloatingActionMenu.Builder builder = new FloatingActionMenu 119 | .Builder( 120 | getActivity(), sectionNumber < 2 ? R.style.FloatingActionMenuStyle_Horizontal : R.style.FloatingActionMenuStyle) 121 | .withScrollDelegate(new FloatingActionMenu.AbsListViewScrollDelegate(getListView())) 122 | .animationInterpolator(new OvershootInterpolator()) 123 | .visible(false); 124 | 125 | if (sectionNumber == 0) { 126 | builder.withGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 127 | } else if (sectionNumber == 1) { 128 | builder.withGravity(Gravity.RIGHT | Gravity.BOTTOM); 129 | } else if (sectionNumber == 2) { 130 | builder.withGravity(Gravity.RIGHT | Gravity.BOTTOM); 131 | } else { 132 | builder.withGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); 133 | } 134 | 135 | return builder; 136 | } 137 | 138 | private Boolean visible; 139 | 140 | boolean getVisible() { 141 | if (null != visible) { 142 | return visible.booleanValue(); 143 | } 144 | return false; 145 | } 146 | 147 | public void onVisibilityChanged(boolean visible) { 148 | if (null == this.visible || this.visible != visible) { 149 | this.visible = visible; 150 | if (null != mFloatingMenu) { 151 | Log.i(TAG, "onVisibilityChanged: " + sectionNumber + ", visible: " + visible); 152 | 153 | if (visible) { 154 | mFloatingMenu.show(true, true); 155 | } else { 156 | mFloatingMenu.hide(true, true); 157 | } 158 | } 159 | } 160 | } 161 | 162 | @Override 163 | public void onSaveInstanceState(final Bundle outState) { 164 | Log.i(TAG, "onSaveInstanceState: " + sectionNumber); 165 | super.onSaveInstanceState(outState); 166 | } 167 | 168 | private List createData() { 169 | List data = new ArrayList(); 170 | for (int i = 0; i < 100; i++) { 171 | data.add(String.format("fragment %d - item %d", sectionNumber, i)); 172 | } 173 | return data; 174 | } 175 | 176 | @Override 177 | public void onItemClick(final FloatingActionMenu menu, final int id) { 178 | Log.i(TAG, "onItemClick: " + id); 179 | Toast.makeText(getActivity(), "clicked item " + id, Toast.LENGTH_SHORT).show(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/it/sephiroth/android/library/floatingmenu/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu.app; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.ActionBarActivity; 6 | import android.util.Log; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.view.animation.OvershootInterpolator; 11 | import android.widget.AdapterView; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.ListView; 14 | import android.widget.Toast; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import it.sephiroth.android.library.floatingmenu.FloatingActionItem; 20 | import it.sephiroth.android.library.floatingmenu.FloatingActionMenu; 21 | 22 | public class MainActivity extends ActionBarActivity 23 | implements AdapterView.OnItemClickListener, FloatingActionMenu.OnItemClickListener { 24 | private static final String TAG = "MainActivity"; 25 | private ListView mListView; 26 | private FloatingActionMenu mFloatingMenu; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | Log.i(TAG, "onCreate: " + savedInstanceState); 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_main); 33 | 34 | List objects = new ArrayList<>(); 35 | populateData(objects); 36 | 37 | ArrayAdapter adapter = 38 | new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, objects); 39 | mListView.setAdapter(adapter); 40 | mListView.setOnItemClickListener(this); 41 | 42 | boolean visible = true; 43 | if (null != savedInstanceState) { 44 | visible = savedInstanceState.getBoolean("floating-menu-visible", true); 45 | } 46 | 47 | FloatingActionItem item1 = new FloatingActionItem.Builder(this, 0, R.style.FloatingActionMenuItemStyle) 48 | .withResId(R.drawable.ic_facebook) 49 | .withDelay(0) 50 | .build(); 51 | 52 | FloatingActionItem item2 = new FloatingActionItem.Builder(this, 1, R.style.FloatingActionMenuItemStyle) 53 | .withResId(R.drawable.ic_googleplus) 54 | .withDelay(50) 55 | .build(); 56 | 57 | FloatingActionItem item3 = new FloatingActionItem.Builder(this, 2, R.style.FloatingActionMenuItemStyle) 58 | .withResId(R.drawable.ic_twitter) 59 | .withDelay(100) 60 | .build(); 61 | 62 | mFloatingMenu = new FloatingActionMenu 63 | .Builder(this, R.style.FloatingActionMenuStyle) 64 | .addItem(item1) 65 | .addItem(item2) 66 | .addItem(item3) 67 | .withScrollDelegate(new FloatingActionMenu.AbsListViewScrollDelegate(mListView)) 68 | .withThreshold(R.dimen.float_action_threshold) 69 | .withGap(R.dimen.float_action_item_gap) 70 | .withVerticalPadding(R.dimen.float_action_v_padding) 71 | .withGravity(FloatingActionMenu.Gravity.CENTER_HORIZONTAL | FloatingActionMenu.Gravity.BOTTOM) 72 | .withDirection(FloatingActionMenu.Direction.Vertical) 73 | .animationDuration(400) 74 | .animationInterpolator(new OvershootInterpolator()) 75 | .visible(visible) 76 | .build(); 77 | 78 | mFloatingMenu.setOnItemClickListener(this); 79 | } 80 | 81 | @Override 82 | protected void onSaveInstanceState(final Bundle outState) { 83 | Log.i(TAG, "onSaveInstanceState"); 84 | super.onSaveInstanceState(outState); 85 | 86 | outState.putBoolean("floating-menu-visible", mFloatingMenu.getVisible()); 87 | } 88 | 89 | @Override 90 | protected void onRestoreInstanceState(final Bundle savedInstanceState) { 91 | Log.i(TAG, "onRestoreInstanceState: " + savedInstanceState); 92 | super.onRestoreInstanceState(savedInstanceState); 93 | } 94 | 95 | private void populateData(final List objects) { 96 | for (int i = 0; i < 100; i++) { 97 | objects.add(String.format("Item %d", i)); 98 | } 99 | } 100 | 101 | @Override 102 | public void onSupportContentChanged() { 103 | super.onSupportContentChanged(); 104 | mListView = (ListView) findViewById(android.R.id.list); 105 | } 106 | 107 | @Override 108 | public boolean onCreateOptionsMenu(Menu menu) { 109 | getMenuInflater().inflate(R.menu.main, menu); 110 | return true; 111 | } 112 | 113 | @Override 114 | public boolean onOptionsItemSelected(MenuItem item) { 115 | int id = item.getItemId(); 116 | if (id == R.id.example2) { 117 | startActivity(new Intent(this, MainActivity2.class)); 118 | } 119 | return super.onOptionsItemSelected(item); 120 | } 121 | 122 | @Override 123 | public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { 124 | } 125 | 126 | @Override 127 | public void onItemClick(final FloatingActionMenu menu, final int id) { 128 | Log.i(TAG, "onItemClick: " + id); 129 | 130 | Toast.makeText(this, "item click " + id, Toast.LENGTH_SHORT).show(); 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/it/sephiroth/android/library/floatingmenu/app/MainActivity2.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu.app; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentManager; 7 | import android.support.v4.app.FragmentStatePagerAdapter; 8 | import android.support.v4.app.FragmentTransaction; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.app.ActionBar; 11 | import android.support.v7.app.ActionBarActivity; 12 | import android.util.Log; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.view.ViewGroup; 16 | 17 | import java.util.HashMap; 18 | import java.util.Locale; 19 | 20 | 21 | public class MainActivity2 extends ActionBarActivity implements ActionBar.TabListener { 22 | private static final String TAG = "MainActivity"; 23 | SectionsPagerAdapter mSectionsPagerAdapter; 24 | ViewPager mViewPager; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | Log.i(TAG, "onCreate: " + savedInstanceState); 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_main_activity2); 31 | 32 | // Set up the action bar. 33 | final ActionBar actionBar = getSupportActionBar(); 34 | actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); 35 | 36 | mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); 37 | 38 | int viewPagerItem = 0; 39 | if (null != savedInstanceState) { 40 | viewPagerItem = savedInstanceState.getInt("viewpage-current-item", 0); 41 | } 42 | 43 | Log.i(TAG, "viewPagerItem: " + viewPagerItem); 44 | 45 | mViewPager = (ViewPager) findViewById(R.id.pager); 46 | mViewPager.setAdapter(mSectionsPagerAdapter); 47 | 48 | mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { 49 | @Override 50 | public void onPageSelected(int position) { 51 | actionBar.setSelectedNavigationItem(position); 52 | } 53 | }); 54 | 55 | for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) { 56 | actionBar.addTab(actionBar.newTab().setText(mSectionsPagerAdapter.getPageTitle(i)).setTabListener(this)); 57 | } 58 | } 59 | 60 | 61 | @Override 62 | public boolean onCreateOptionsMenu(Menu menu) { 63 | getMenuInflater().inflate(R.menu.main_activity2, menu); 64 | return true; 65 | } 66 | 67 | @Override 68 | public boolean onOptionsItemSelected(MenuItem item) { 69 | int id = item.getItemId(); 70 | if (id == R.id.example1) { 71 | startActivity(new Intent(this, MainActivity.class)); 72 | } 73 | return super.onOptionsItemSelected(item); 74 | } 75 | 76 | @Override 77 | public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 78 | Log.i(TAG, "onTabSelected: " + tab.getPosition()); 79 | mViewPager.setCurrentItem(tab.getPosition()); 80 | 81 | ListFragment1 fragment = mSectionsPagerAdapter.getFragmentAt(tab.getPosition()); 82 | if (null != fragment) { 83 | fragment.onVisibilityChanged(true); 84 | } 85 | } 86 | 87 | @Override 88 | public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 89 | Log.i(TAG, "onTabUnselected: " + tab.getPosition()); 90 | 91 | ListFragment1 fragment = mSectionsPagerAdapter.getFragmentAt(tab.getPosition()); 92 | if (null != fragment) { 93 | fragment.onVisibilityChanged(false); 94 | } 95 | } 96 | 97 | @Override 98 | public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 99 | } 100 | 101 | @Override 102 | protected void onSaveInstanceState(final Bundle outState) { 103 | super.onSaveInstanceState(outState); 104 | 105 | outState.putInt("viewpage-current-item", mViewPager.getCurrentItem()); 106 | } 107 | 108 | @Override 109 | protected void onRestoreInstanceState(final Bundle savedInstanceState) { 110 | Log.i(TAG, "onRestoreInstanceState: " + savedInstanceState); 111 | super.onRestoreInstanceState(savedInstanceState); 112 | } 113 | 114 | public class SectionsPagerAdapter extends FragmentStatePagerAdapter { 115 | final HashMap mFragmentTable; 116 | 117 | public SectionsPagerAdapter(FragmentManager fm) { 118 | super(fm); 119 | mFragmentTable = new HashMap(); 120 | } 121 | 122 | @Override 123 | public Fragment getItem(int position) { 124 | ListFragment1 item = ListFragment1.newInstance(position, false); 125 | return item; 126 | } 127 | 128 | public ListFragment1 getFragmentAt(int position) { 129 | return mFragmentTable.get(position); 130 | } 131 | 132 | @Override 133 | public void destroyItem(ViewGroup container, int position, Object object) { 134 | Log.i(TAG, "destroyItem: " + position); 135 | super.destroyItem(container, position, object); 136 | mFragmentTable.remove(position); 137 | } 138 | 139 | @Override 140 | public Object instantiateItem(final ViewGroup container, final int position) { 141 | Log.i(TAG, "instantiateItem: " + position); 142 | Object object = super.instantiateItem(container, position); 143 | mFragmentTable.put(position, (ListFragment1) object); 144 | return object; 145 | } 146 | 147 | @Override 148 | public int getCount() { 149 | return 4; 150 | } 151 | 152 | @Override 153 | public CharSequence getPageTitle(int position) { 154 | Locale l = Locale.getDefault(); 155 | switch (position) { 156 | case 0: 157 | return getString(R.string.title_section1).toUpperCase(l); 158 | case 1: 159 | return getString(R.string.title_section2).toUpperCase(l); 160 | case 2: 161 | return getString(R.string.title_section3).toUpperCase(l); 162 | case 3: 163 | return getString(R.string.title_section4).toUpperCase(l); 164 | } 165 | return null; 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/custom_item_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/drawable-xxhdpi/ic_facebook.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_googleplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/drawable-xxhdpi/ic_googleplus.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/drawable-xxhdpi/ic_instagram.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/drawable-xxhdpi/ic_twitter.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/custom_item_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pink_oval_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pink_oval_shape_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_activity2.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_activity2.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_activity2.xml: -------------------------------------------------------------------------------- 1 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF4081 4 | #FF80AB 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0dp 4 | 0dp 5 | 6 | 16dip 7 | 10dip 8 | 16dip 9 | 16dip 10 | 24dip 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Floating Action 5 | Fam Example 2 6 | Fam Example 7 | Section 1 8 | Section 2 9 | Section 3 10 | Section 4 11 | Advanced Example 12 | Simple Example 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 21 | 22 | 26 | 27 | 30 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.+' 9 | } 10 | } 11 | 12 | def isReleaseBuild() { 13 | return version.contains("SNAPSHOT") == false 14 | } 15 | 16 | allprojects { 17 | 18 | version = VERSION_NAME 19 | group = GROUP 20 | 21 | repositories { 22 | jcenter() 23 | } 24 | } 25 | 26 | task wrapper(type: Wrapper) { 27 | gradleVersion = '2.3' 28 | } 29 | -------------------------------------------------------------------------------- /floatingmenu/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /floatingmenu/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | group GROUP 4 | version VERSION_NAME 5 | 6 | android { 7 | compileSdkVersion Integer.parseInt(ANDROID_BUILD_SDK_VERSION) 8 | buildToolsVersion ANDROID_BUILD_TOOLS_VERSION 9 | 10 | defaultConfig { 11 | minSdkVersion 9 12 | targetSdkVersion Integer.parseInt(ANDROID_BUILD_TARGET_SDK_VERSION) 13 | versionCode 1 14 | versionName VERSION_NAME 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | compile 'com.nineoldandroids:library:2.4.0' 27 | compile 'com.android.support:appcompat-v7:21.0.3' 28 | } 29 | 30 | 31 | apply from: 'https://raw.githubusercontent.com/sephiroth74/gradle-mvn-push/master/gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /floatingmenu/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=floatingmenu 2 | POM_ARTIFACT_ID=floatingmenu 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /floatingmenu/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} -------------------------------------------------------------------------------- /floatingmenu/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /floatingmenu/src/main/java/it/sephiroth/android/library/floatingmenu/ApiHelper.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu; 2 | 3 | import android.os.Build; 4 | 5 | /** 6 | * Created by crugnola on 12/6/14. 7 | */ 8 | class ApiHelper { 9 | static final boolean AT_LEAST_21 = Build.VERSION.SDK_INT >= 21; 10 | static final boolean AT_LEAST_11 = Build.VERSION.SDK_INT >= 11; 11 | } 12 | -------------------------------------------------------------------------------- /floatingmenu/src/main/java/it/sephiroth/android/library/floatingmenu/FloatingActionItem.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | 6 | /** 7 | * Created by alessandro on 24/05/14. 8 | */ 9 | public class FloatingActionItem { 10 | int id; 11 | int resId; 12 | int delay = 0; 13 | int paddingTop = 0; 14 | int paddingBottom = 0; 15 | int paddingLeft = 0; 16 | int paddingRight = 0; 17 | int stateListAnimId = 0; 18 | int backgroundResId; 19 | 20 | static public class Builder { 21 | int id; 22 | int resId = -1; 23 | int delay = 0; 24 | int paddingTop = 0; 25 | int paddingBottom = 0; 26 | int paddingLeft = 0; 27 | int paddingRight = 0; 28 | int backgroundResId = 0; 29 | int stateListAnimResId = 0; 30 | 31 | /** 32 | * @param id unique id to be used with the 33 | * {@link it.sephiroth.android.library.floatingmenu.FloatingActionMenu.OnItemClickListener} 34 | */ 35 | public Builder(final Context context, int id) { 36 | this(context, id, 0); 37 | } 38 | 39 | public Builder(final Context context, int id, int styleId) { 40 | this.id = id; 41 | 42 | TypedArray array = 43 | context.getTheme().obtainStyledAttributes(styleId, R.styleable.FloatingActionItem); 44 | 45 | this.backgroundResId = array.getResourceId( 46 | R.styleable.FloatingActionItem_fam_item_background, 47 | R.drawable.action_item_background_selector); 48 | 49 | this.resId = array.getResourceId(R.styleable.FloatingActionItem_android_src, 0); 50 | this.delay = array.getInteger(R.styleable.FloatingActionItem_fam_item_delay, 0); 51 | this.stateListAnimResId = 52 | array.getResourceId(R.styleable.FloatingActionItem_fam_item_stateListAnimator, R.anim.action_item_raise); 53 | 54 | int defaultPadding = context.getResources().getDimensionPixelSize(R.dimen.floating_action_item_default_padding); 55 | 56 | int padding = array.getDimensionPixelSize(R.styleable.FloatingActionItem_android_padding, defaultPadding); 57 | array.recycle(); 58 | 59 | withPadding(padding); 60 | } 61 | 62 | /** 63 | * Available only for API level 21 64 | * 65 | * @param resId 66 | * @return 67 | */ 68 | public Builder withStateListAnimator(int resId) { 69 | this.stateListAnimResId = resId; 70 | return this; 71 | } 72 | 73 | /** 74 | * Assign a custom background resource 75 | */ 76 | public Builder withBackgroundResId(int resId) { 77 | this.backgroundResId = resId; 78 | return this; 79 | } 80 | 81 | /** 82 | * Image drawable resource-id 83 | * 84 | * @param resId 85 | * @return 86 | */ 87 | public Builder withResId(final int resId) { 88 | this.resId = resId; 89 | return this; 90 | } 91 | 92 | /** 93 | * Show/Hide delay time 94 | * 95 | * @param delay 96 | * @return 97 | */ 98 | public Builder withDelay(final int delay) { 99 | this.delay = delay; 100 | return this; 101 | } 102 | 103 | /** 104 | * Image padding 105 | * 106 | * @param left 107 | * @param top 108 | * @param right 109 | * @param bottom 110 | * @return 111 | */ 112 | public Builder withPadding(int left, int top, int right, int bottom) { 113 | this.paddingBottom = bottom; 114 | this.paddingLeft = left; 115 | this.paddingRight = right; 116 | this.paddingTop = top; 117 | return this; 118 | } 119 | 120 | /** 121 | * Image padding, applied to all the padding directions 122 | * 123 | * @param padding 124 | * @return 125 | */ 126 | public Builder withPadding(int padding) { 127 | return withPadding(padding, padding, padding, padding); 128 | } 129 | 130 | public FloatingActionItem build() { 131 | if (resId == -1) { 132 | throw new IllegalArgumentException("resId missing"); 133 | } 134 | FloatingActionItem item = new FloatingActionItem(); 135 | item.id = id; 136 | item.resId = resId; 137 | item.delay = delay; 138 | item.paddingBottom = paddingBottom; 139 | item.paddingLeft = paddingLeft; 140 | item.paddingRight = paddingRight; 141 | item.paddingTop = paddingTop; 142 | item.backgroundResId = backgroundResId; 143 | item.stateListAnimId = stateListAnimResId; 144 | return item; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /floatingmenu/src/main/java/it/sephiroth/android/library/floatingmenu/FloatingActionItemImageView.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.util.Log; 7 | import android.view.animation.Interpolator; 8 | import android.widget.FrameLayout; 9 | import android.widget.ImageView; 10 | 11 | import com.nineoldandroids.animation.Animator; 12 | import com.nineoldandroids.animation.ObjectAnimator; 13 | import com.nineoldandroids.view.ViewHelper; 14 | 15 | import static it.sephiroth.android.library.floatingmenu.FloatingActionMenu.LOG_ENABLED; 16 | 17 | final class FloatingActionItemImageView extends ImageView { 18 | final String TAG = "ActionItemImageView"; 19 | 20 | static enum Status { 21 | None, Invisible, Visible, Animating 22 | } 23 | 24 | private static final int ANIMATION_SHOW = 0; 25 | private static final int ANIMATION_HIDE = 1; 26 | private Status mStatus; 27 | FloatingActionMenu.Direction direction; 28 | Interpolator interpolator; 29 | Animator.AnimatorListener mShowListener; 30 | Animator.AnimatorListener mHideListener; 31 | ObjectAnimator mShowAnimator; 32 | ObjectAnimator mHideAnimator; 33 | /** final x for for horizontal animation */ 34 | int x1; 35 | /** final y for for vertical animation */ 36 | int y1; 37 | int x2; 38 | int y2; 39 | int duration; 40 | 41 | public FloatingActionItemImageView(final Context context) { 42 | this(context, null); 43 | } 44 | 45 | public FloatingActionItemImageView(final Context context, AttributeSet attrs) { 46 | this(context, attrs, 0); 47 | } 48 | 49 | public FloatingActionItemImageView(Context context, AttributeSet attrs, int defStyleAttr) { 50 | super(context, attrs, defStyleAttr); 51 | 52 | mShowListener = new Animator.AnimatorListener() { 53 | boolean isCancelled; 54 | 55 | @Override 56 | public void onAnimationStart(final Animator animation) { 57 | isCancelled = false; 58 | FloatingActionItemImageView.this.onAnimationStart(ANIMATION_SHOW); 59 | } 60 | 61 | @Override 62 | public void onAnimationEnd(final Animator animation) { 63 | if (!isCancelled) { 64 | FloatingActionItemImageView.this.onAnimationEnd(ANIMATION_SHOW); 65 | } 66 | } 67 | 68 | @Override 69 | public void onAnimationCancel(final Animator animation) { 70 | isCancelled = true; 71 | } 72 | 73 | @Override 74 | public void onAnimationRepeat(final Animator animation) {} 75 | }; 76 | 77 | mHideListener = new Animator.AnimatorListener() { 78 | boolean isCancelled; 79 | 80 | @Override 81 | public void onAnimationStart(final Animator animation) { 82 | isCancelled = false; 83 | FloatingActionItemImageView.this.onAnimationStart(ANIMATION_HIDE); 84 | } 85 | 86 | @Override 87 | public void onAnimationEnd(final Animator animation) { 88 | if (!isCancelled) { 89 | FloatingActionItemImageView.this.onAnimationEnd(ANIMATION_HIDE); 90 | } 91 | } 92 | 93 | @Override 94 | public void onAnimationCancel(final Animator animation) { 95 | isCancelled = true; 96 | } 97 | 98 | @Override 99 | public void onAnimationRepeat(final Animator animation) {} 100 | }; 101 | 102 | mShowAnimator = new ObjectAnimator(); 103 | mShowAnimator.setTarget(this); 104 | if (null != mShowListener) { 105 | mShowAnimator.addListener(mShowListener); 106 | } 107 | 108 | mHideAnimator = new ObjectAnimator(); 109 | mHideAnimator.setTarget(this); 110 | if (null != mHideListener) { 111 | mHideAnimator.addListener(mHideListener); 112 | } 113 | } 114 | 115 | void setStyle(int style) { 116 | TypedArray array = 117 | getContext().getTheme().obtainStyledAttributes(style, R.styleable.FloatingActionItem); 118 | 119 | array.recycle(); 120 | } 121 | 122 | void setDirection(final FloatingActionMenu.Direction direction) { 123 | if (LOG_ENABLED) { 124 | Log.v(TAG, "setDirection: " + direction); 125 | } 126 | this.direction = direction; 127 | this.mShowAnimator.setPropertyName(direction == FloatingActionMenu.Direction.Vertical ? "translationY" : "translationX"); 128 | this.mHideAnimator.setPropertyName(direction == FloatingActionMenu.Direction.Vertical ? "translationY" : "translationX"); 129 | } 130 | 131 | void setAnimationDuration(int ms) { 132 | if (LOG_ENABLED) Log.v(TAG, "setAnimationDuration: " + ms); 133 | this.duration = ms; 134 | this.mHideAnimator.setDuration(duration); 135 | this.mShowAnimator.setDuration(duration); 136 | } 137 | 138 | void setAnimationInterpolator(Interpolator interpolator) { 139 | if (LOG_ENABLED) Log.v(TAG, "setAnimationInterpolator: " + interpolator); 140 | this.interpolator = interpolator; 141 | this.mShowAnimator.setInterpolator(interpolator); 142 | this.mHideAnimator.setInterpolator(interpolator); 143 | } 144 | 145 | void setX1(final int x1, final boolean immediate) { 146 | if (LOG_ENABLED) Log.v(TAG, "setX1: " + x1 + ", immediate: " + immediate); 147 | this.x1 = x1; 148 | if (immediate) { 149 | ViewHelper.setTranslationX(this, x1); 150 | } 151 | } 152 | 153 | void setX2(final int x2) { 154 | if (LOG_ENABLED) Log.v(TAG, "setX2: " + x2); 155 | this.x2 = x2; 156 | } 157 | 158 | void setY1(final int y1, boolean immediate) { 159 | if (LOG_ENABLED) Log.v(TAG, "setY1: " + y1 + ", immediate: " + immediate); 160 | this.y1 = y1; 161 | if (immediate) { 162 | ViewHelper.setTranslationY(this, y1); 163 | } 164 | } 165 | 166 | void setY2(final int y2) { 167 | if (LOG_ENABLED) Log.v(TAG, "setY2: " + y2); 168 | this.y2 = y2; 169 | } 170 | 171 | public void show(boolean animate, int delay) { 172 | if (LOG_ENABLED) { 173 | Log.i(TAG, "show, current status: " + mStatus); 174 | Log.v(TAG, "direction: " + direction); 175 | Log.v(TAG, "animate: " + animate); 176 | } 177 | 178 | if (mStatus == Status.Visible) { 179 | if (LOG_ENABLED) { 180 | Log.w(TAG, "already visible"); 181 | } 182 | return; 183 | } 184 | 185 | mStatus = Status.Animating; 186 | 187 | if (mHideAnimator.isStarted()) { 188 | mHideAnimator.cancel(); 189 | } 190 | 191 | if (mShowAnimator.isStarted()) { 192 | mShowAnimator.cancel(); 193 | } 194 | 195 | if (direction == FloatingActionMenu.Direction.Vertical) { 196 | if (!animate) { 197 | ViewHelper.setTranslationY(this, y1); 198 | onAnimationEnd(ANIMATION_SHOW); 199 | } else { 200 | if (ApiHelper.AT_LEAST_11) { 201 | mShowAnimator.setFloatValues(y1); 202 | } else { 203 | mShowAnimator.setFloatValues(y2, y1); 204 | } 205 | mShowAnimator.setStartDelay(delay); 206 | mShowAnimator.start(); 207 | } 208 | } else { 209 | if (!animate) { 210 | ViewHelper.setTranslationX(this, x1); 211 | onAnimationEnd(ANIMATION_SHOW); 212 | } else { 213 | if (ApiHelper.AT_LEAST_11) { 214 | mShowAnimator.setFloatValues(x1); 215 | } else { 216 | mShowAnimator.setFloatValues(x2, x1); 217 | } 218 | mShowAnimator.setStartDelay(delay); 219 | mShowAnimator.start(); 220 | } 221 | } 222 | } 223 | 224 | public void hide(boolean animate, int delay) { 225 | if (LOG_ENABLED) { 226 | Log.i(TAG, "hide, current status: " + mStatus); 227 | Log.v(TAG, "direction: " + direction); 228 | Log.v(TAG, "animate: " + animate); 229 | } 230 | 231 | if (mStatus == Status.Invisible) { 232 | if (LOG_ENABLED) { 233 | Log.w(TAG, "already hidden"); 234 | } 235 | return; 236 | } 237 | 238 | mStatus = Status.Animating; 239 | 240 | if (mShowAnimator.isStarted()) { 241 | mShowAnimator.cancel(); 242 | } 243 | 244 | if (mHideAnimator.isStarted()) { 245 | mHideAnimator.cancel(); 246 | } 247 | 248 | if (direction == FloatingActionMenu.Direction.Vertical) { 249 | if (!animate) { 250 | ViewHelper.setTranslationY(this, y2); 251 | onAnimationEnd(ANIMATION_HIDE); 252 | } else { 253 | if (ApiHelper.AT_LEAST_11) { 254 | mHideAnimator.setFloatValues(y2); 255 | } else { 256 | mHideAnimator.setFloatValues(y1, y2); 257 | } 258 | mHideAnimator.setStartDelay(delay); 259 | mHideAnimator.start(); 260 | } 261 | } else { 262 | if (!animate) { 263 | ViewHelper.setTranslationX(this, x2); 264 | onAnimationEnd(ANIMATION_HIDE); 265 | } else { 266 | if (ApiHelper.AT_LEAST_11) { 267 | mHideAnimator.setFloatValues(x2); 268 | } else { 269 | mHideAnimator.setFloatValues(x1, x2); 270 | } 271 | mHideAnimator.setStartDelay(delay); 272 | mHideAnimator.start(); 273 | } 274 | } 275 | } 276 | 277 | @SuppressWarnings ("unused") 278 | private float getCurrentY() { 279 | if (LOG_ENABLED) Log.i(TAG, "getCurrentY"); 280 | if (mShowAnimator.isRunning() || mHideAnimator.isRunning()) { 281 | return ViewHelper.getTranslationY(this); 282 | } else { 283 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); 284 | return params.topMargin; 285 | } 286 | } 287 | 288 | @SuppressWarnings ("unused") 289 | private float getCurrentX() { 290 | if (LOG_ENABLED) Log.i(TAG, "getCurrentX"); 291 | if (mShowAnimator.isRunning() || mHideAnimator.isRunning()) { 292 | return ViewHelper.getTranslationX(this); 293 | } else { 294 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); 295 | return params.leftMargin; 296 | } 297 | } 298 | 299 | public FloatingActionItem getItem() { 300 | return (FloatingActionItem) getTag(); 301 | } 302 | 303 | public void setItem(FloatingActionItem item) { 304 | setTag(item); 305 | } 306 | 307 | protected void onAnimationStart(int type) { 308 | if (!ApiHelper.AT_LEAST_11) { 309 | 310 | if (LOG_ENABLED) { 311 | Log.v(TAG, "onAnimationStart: " + type); 312 | } 313 | 314 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); 315 | params.setMargins(0, 0, 0, 0); 316 | setLayoutParams(params); 317 | 318 | if (direction == FloatingActionMenu.Direction.Vertical) { 319 | if (LOG_ENABLED) { 320 | Log.v(TAG, "setTranslationX: " + x1); 321 | Log.v(TAG, "setTranslationY: " + (type == ANIMATION_SHOW ? y2 : y1)); 322 | } 323 | ViewHelper.setTranslationX(this, x1); 324 | ViewHelper.setTranslationY(this, type == ANIMATION_SHOW ? y2 : y1); 325 | } else { 326 | ViewHelper.setTranslationX(this, type == ANIMATION_SHOW ? x2 : x1); 327 | ViewHelper.setTranslationY(this, y1); 328 | } 329 | } 330 | } 331 | 332 | protected void onAnimationEnd(int type) { 333 | mStatus = type == ANIMATION_SHOW ? Status.Visible : Status.Invisible; 334 | 335 | if (LOG_ENABLED) { 336 | Log.v(TAG, "onAnimationEnd. status: " + mStatus + ", type: " + type); 337 | } 338 | 339 | if (!ApiHelper.AT_LEAST_11) { 340 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); 341 | 342 | ViewHelper.setTranslationX(this, 0); 343 | ViewHelper.setTranslationY(this, 0); 344 | 345 | if (direction == FloatingActionMenu.Direction.Vertical) { 346 | params.setMargins(x1, type == ANIMATION_SHOW ? y1 : y2, 0, 0); 347 | } else { 348 | params.setMargins(type == ANIMATION_SHOW ? x1 : x2, y1, 0, 0); 349 | } 350 | 351 | setLayoutParams(params); 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /floatingmenu/src/main/java/it/sephiroth/android/library/floatingmenu/FloatingActionMenu.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.floatingmenu; 2 | 3 | import android.animation.AnimatorInflater; 4 | import android.annotation.TargetApi; 5 | import android.app.Activity; 6 | import android.content.res.Resources; 7 | import android.content.res.TypedArray; 8 | import android.os.Build; 9 | import android.util.DisplayMetrics; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.view.animation.AnimationUtils; 14 | import android.view.animation.Interpolator; 15 | import android.view.animation.LinearInterpolator; 16 | import android.widget.AbsListView; 17 | import android.widget.FrameLayout; 18 | import android.widget.RelativeLayout; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Iterator; 22 | import java.util.List; 23 | 24 | public class FloatingActionMenu implements AbsListView.OnScrollListener, View.OnClickListener { 25 | private static final String TAG = "FloatingMenu"; 26 | final Activity activity; 27 | final List itemViews; 28 | private final ScrollDelegate scrollDelegate; 29 | private final int verticalPadding; 30 | private final int horizontalPadding; 31 | private final int animationDuration; 32 | private final Interpolator animationInterpolator; 33 | private final int gravity; 34 | private Direction direction; 35 | private OnItemClickListener clickListener; 36 | private int itemGap; 37 | private int threshold; 38 | private boolean visible; 39 | private ViewGroup root; 40 | static final boolean LOG_ENABLED = false; 41 | 42 | public FloatingActionMenu(final Builder builder) { 43 | this.activity = builder.activity; 44 | this.visible = builder.visible; 45 | this.threshold = builder.threshold; 46 | this.itemGap = builder.gap; 47 | this.verticalPadding = builder.verticalPadding; 48 | this.horizontalPadding = builder.horitontalPadding; 49 | this.gravity = builder.gravity; 50 | this.direction = builder.direction; 51 | this.animationDuration = builder.animationDuration; 52 | this.animationInterpolator = builder.interpolator; 53 | this.scrollDelegate = builder.scrollDelegate; 54 | 55 | if (null != scrollDelegate) { 56 | scrollDelegate.setOnScrollListener(this); 57 | } 58 | 59 | this.root = createRoot(this.activity); 60 | this.itemViews = create(builder.items); 61 | layout(); 62 | } 63 | 64 | public void setOnItemClickListener(OnItemClickListener listener) { 65 | clickListener = listener; 66 | } 67 | 68 | /** 69 | * Hide all the menu items 70 | * 71 | * @param animate turn on/off the animation 72 | */ 73 | public void hide(boolean animate) { 74 | hide(animate, false); 75 | } 76 | 77 | /** 78 | * Show all the menu items 79 | * 80 | * @param animate turn on/off the animation 81 | */ 82 | public void show(boolean animate) { 83 | show(animate, false); 84 | } 85 | 86 | /** 87 | * @param animate 88 | * @param play_together play all the items together (override the item's delay value) 89 | * @see #hide(boolean) 90 | */ 91 | public void hide(boolean animate, boolean play_together) { 92 | for (FloatingActionItemImageView view : itemViews) { 93 | view.hide(animate, play_together ? 0 : view.getItem().delay); 94 | } 95 | visible = false; 96 | } 97 | 98 | /** 99 | * @param animate 100 | * @param play_together play all the items together (override the item's delay value) 101 | * @see #show(boolean) 102 | */ 103 | public void show(boolean animate, boolean play_together) { 104 | for (FloatingActionItemImageView view : itemViews) { 105 | view.show(animate, play_together ? 0 : view.getItem().delay); 106 | } 107 | visible = true; 108 | } 109 | 110 | /** 111 | * Get the current menu visibility 112 | * 113 | * @return 114 | */ 115 | public boolean getVisible() { 116 | return visible; 117 | } 118 | 119 | private ViewGroup createRoot(final Activity activity) { 120 | ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); 121 | 122 | final ViewGroup viewGroup = new RelativeLayout(activity); 123 | decorView 124 | .addView( 125 | viewGroup, 126 | 1, 127 | new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 128 | 129 | return viewGroup; 130 | } 131 | 132 | @TargetApi (Build.VERSION_CODES.LOLLIPOP) 133 | private List create(final List items) { 134 | List result = new ArrayList<>(); 135 | 136 | FrameLayout.LayoutParams params; 137 | ArrayList focusables = new ArrayList<>(); 138 | 139 | for (FloatingActionItem actionItem : items) { 140 | params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 141 | params.gravity = android.view.Gravity.TOP | android.view.Gravity.LEFT; 142 | FloatingActionItemImageView view = 143 | (FloatingActionItemImageView) LayoutInflater.from(activity).inflate( 144 | R.layout.floating_action_item_view, root, false); 145 | view.setImageResource(actionItem.resId); 146 | view.setPadding(actionItem.paddingLeft, actionItem.paddingTop, actionItem.paddingRight, actionItem.paddingBottom); 147 | view.setItem(actionItem); 148 | 149 | if (actionItem.backgroundResId != 0) { 150 | view.setBackgroundResource(actionItem.backgroundResId); 151 | } else { 152 | view.setBackgroundResource(R.drawable.action_item_background_selector); 153 | } 154 | 155 | if (ApiHelper.AT_LEAST_21) { 156 | if (actionItem.stateListAnimId != 0) { 157 | view.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activity, actionItem.stateListAnimId)); 158 | } 159 | } 160 | 161 | view.setOnClickListener(this); 162 | root.addView(view, params); 163 | result.add(view); 164 | focusables.add(view); 165 | } 166 | root.addFocusables(focusables, View.FOCUS_FORWARD); 167 | // root.addTouchables(focusables); 168 | return result; 169 | } 170 | 171 | private void layout() { 172 | Resources resources = activity.getResources(); 173 | DisplayMetrics metrics = resources.getDisplayMetrics(); 174 | boolean measured = false; 175 | 176 | int x = 0; 177 | int y = 0; 178 | 179 | boolean right = false; 180 | boolean bottom = false; 181 | 182 | if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { 183 | x = metrics.widthPixels - horizontalPadding; 184 | right = true; 185 | } else if ((gravity & Gravity.LEFT) == Gravity.LEFT) { 186 | x = horizontalPadding; 187 | } 188 | 189 | if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { 190 | y = metrics.heightPixels - verticalPadding; 191 | bottom = true; 192 | } else if ((gravity & Gravity.TOP) == Gravity.TOP) { 193 | y = verticalPadding; 194 | } 195 | 196 | if ((gravity & Gravity.CENTER_VERTICAL) == Gravity.CENTER_VERTICAL || 197 | (gravity & Gravity.CENTER_HORIZONTAL) == Gravity.CENTER_HORIZONTAL) { 198 | int totalWidth = 0; 199 | int totalHeight = 0; 200 | for (FloatingActionItemImageView view : itemViews) { 201 | view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 202 | totalWidth += view.getMeasuredWidth() + itemGap; 203 | totalHeight += view.getMeasuredHeight() + itemGap; 204 | } 205 | totalWidth -= itemGap; 206 | totalHeight -= itemGap; 207 | 208 | if ((gravity & Gravity.CENTER_HORIZONTAL) != 0) { 209 | x = ((metrics.widthPixels - totalWidth) / 2) + horizontalPadding; 210 | } 211 | 212 | if ((gravity & Gravity.CENTER_VERTICAL) != 0) { 213 | y = ((metrics.heightPixels - totalHeight) / 2) + verticalPadding; 214 | } 215 | 216 | measured = true; 217 | } 218 | 219 | int x1; 220 | int x2; 221 | int y1; 222 | int y2; 223 | 224 | for (FloatingActionItemImageView view : itemViews) { 225 | if (!measured) { 226 | view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 227 | } 228 | 229 | int width = view.getMeasuredWidth(); 230 | int height = view.getMeasuredHeight(); 231 | 232 | view.setDirection(direction); 233 | view.setAnimationDuration(animationDuration); 234 | view.setAnimationInterpolator(animationInterpolator); 235 | 236 | if (direction == Direction.Vertical) { 237 | if (bottom) { 238 | y1 = metrics.heightPixels - height - verticalPadding; 239 | y2 = metrics.heightPixels + height; 240 | } else { 241 | y1 = verticalPadding * 2; 242 | y2 = -height - verticalPadding; 243 | } 244 | 245 | view.setX1(x - (right ? width : 0), true); 246 | view.setY1(y1, false); 247 | view.setY2(y2); 248 | } else { 249 | if (right) { 250 | x1 = metrics.widthPixels - width - horizontalPadding; 251 | x2 = metrics.widthPixels + width; 252 | } else { 253 | x1 = horizontalPadding * 2; 254 | x2 = -width - horizontalPadding; 255 | } 256 | view.setY1(y - (bottom ? height : 0), true); 257 | view.setX1(x1, false); 258 | view.setX2(x2); 259 | } 260 | 261 | if (visible) { 262 | view.show(false, 0); 263 | } else { 264 | view.hide(false, 0); 265 | } 266 | 267 | if (direction == Direction.Vertical) { 268 | if (right) { 269 | x -= (width + itemGap); 270 | } else { 271 | x += (width + itemGap); 272 | } 273 | } else { 274 | if (bottom) { 275 | y -= (height + itemGap); 276 | } else { 277 | y += (height + itemGap); 278 | } 279 | } 280 | } 281 | } 282 | 283 | View mFirstView; 284 | boolean mScrollStarted; 285 | int mFirstVisibleItem = -1; 286 | int mFirstViewTop; 287 | int mFirstViewTopAmount; 288 | int mAnimationDirection; 289 | 290 | @Override 291 | public void onScrollStateChanged(final AbsListView view, final int scrollState) { 292 | if (scrollState == 1) { 293 | // scroll starting 294 | if (view.getChildCount() > 0) { 295 | mFirstView = view.getChildAt(0); 296 | if (null == mFirstView) { 297 | return; 298 | } 299 | mFirstViewTop = mFirstView.getTop(); 300 | mScrollStarted = true; 301 | } 302 | mFirstViewTopAmount = 0; 303 | mAnimationDirection = 0; 304 | } else if (scrollState == 0) { 305 | // scroll ended 306 | mFirstVisibleItem = -1; 307 | mScrollStarted = false; 308 | } 309 | } 310 | 311 | @Override 312 | public void onScroll( 313 | final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { 314 | if (totalItemCount == 0 || !mScrollStarted) { 315 | return; 316 | } 317 | 318 | if (mFirstVisibleItem != firstVisibleItem) { 319 | mFirstVisibleItem = firstVisibleItem; 320 | mFirstView = view.getChildAt(0); 321 | if (null == mFirstView) { 322 | return; 323 | } 324 | mFirstViewTop = mFirstView.getTop(); 325 | } 326 | 327 | int newTop = mFirstView.getTop(); 328 | int delta = (mFirstViewTop - newTop); 329 | 330 | mFirstViewTopAmount += delta; 331 | mFirstViewTop = newTop; 332 | 333 | if (Math.abs(mFirstViewTopAmount) > threshold) { 334 | if (mFirstViewTopAmount > 0) { 335 | if (mAnimationDirection != 1) { 336 | hide(true, false); 337 | } 338 | mAnimationDirection = 1; 339 | } else { 340 | if (mAnimationDirection != -1) { 341 | show(true, false); 342 | } 343 | mAnimationDirection = -1; 344 | } 345 | mFirstViewTopAmount = 0; 346 | } 347 | 348 | } 349 | 350 | @Override 351 | public void onClick(final View v) { 352 | FloatingActionItemImageView view = (FloatingActionItemImageView) v; 353 | 354 | if (null != clickListener) { 355 | clickListener.onItemClick(this, view.getItem().id); 356 | } 357 | } 358 | 359 | /** 360 | * Remove immediately all the items from their parent. 361 | */ 362 | public void clear() { 363 | // Iterator iterator = itemViews.iterator(); 364 | // while (iterator.hasNext()) { 365 | // FloatingActionItemImageView item = iterator.next(); 366 | // root.removeView(item); 367 | // iterator.remove(); 368 | // } 369 | 370 | itemViews.clear(); 371 | ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); 372 | decorView.removeView(root); 373 | root = null; 374 | 375 | if (null != scrollDelegate) { 376 | scrollDelegate.setOnScrollListener(null); 377 | } 378 | 379 | } 380 | 381 | public static interface OnItemClickListener { 382 | void onItemClick(FloatingActionMenu menu, final int id); 383 | } 384 | 385 | static public class Builder { 386 | private final List items = new ArrayList<>(); 387 | private final Activity activity; 388 | private final Resources res; 389 | boolean visible; 390 | int threshold = 36; 391 | int gap = 12; 392 | int verticalPadding = 0; 393 | int horitontalPadding = 0; 394 | int gravity = Gravity.RIGHT; 395 | Direction direction = Direction.Vertical; 396 | ScrollDelegate scrollDelegate; 397 | int animationDuration = 300; 398 | Interpolator interpolator = new LinearInterpolator(); 399 | 400 | public Builder(Activity context) { 401 | this(context, 0); 402 | } 403 | 404 | public Builder(Activity context, int styleId) { 405 | this.activity = context; 406 | this.res = activity.getResources(); 407 | 408 | Resources.Theme theme = context.getTheme(); 409 | TypedArray array = theme.obtainStyledAttributes(styleId, R.styleable.FloatingActionMenu); 410 | 411 | this.threshold = array.getDimensionPixelSize(R.styleable.FloatingActionMenu_fam_threshold, 36); 412 | this.gap = array.getDimensionPixelSize(R.styleable.FloatingActionMenu_fam_gap, 12); 413 | this.horitontalPadding = array.getDimensionPixelSize(R.styleable.FloatingActionMenu_fam_horizontalPadding, 0); 414 | this.verticalPadding = array.getDimensionPixelSize(R.styleable.FloatingActionMenu_fam_verticalPadding, 0); 415 | this.gravity = array.getInt(R.styleable.FloatingActionMenu_fam_gravity, Gravity.RIGHT); 416 | this.direction = Direction.values()[array.getInt( 417 | R.styleable.FloatingActionMenu_fam_direction, 418 | Direction.Horizontal.ordinal())]; 419 | this.animationDuration = array.getInt(R.styleable.FloatingActionMenu_android_animationDuration, 200); 420 | 421 | int resId = array.getResourceId( 422 | R.styleable.FloatingActionMenu_android_interpolator, 423 | android.R.anim.anticipate_overshoot_interpolator); 424 | this.interpolator = AnimationUtils.loadInterpolator(activity, resId); 425 | array.recycle(); 426 | 427 | this.visible = true; 428 | } 429 | 430 | /** 431 | * Interpolator used for the item's animation 432 | * 433 | * @param interpolator 434 | * @return 435 | */ 436 | public Builder animationInterpolator(Interpolator interpolator) { 437 | this.interpolator = interpolator; 438 | return this; 439 | } 440 | 441 | /** 442 | * Duration for the item's animation 443 | * 444 | * @param ms 445 | * @return 446 | */ 447 | public Builder animationDuration(int ms) { 448 | this.animationDuration = ms; 449 | return this; 450 | } 451 | 452 | /** 453 | * Direction of the menu 454 | * 455 | * @param direction 456 | * @return 457 | */ 458 | public Builder withDirection(Direction direction) { 459 | this.direction = direction; 460 | return this; 461 | } 462 | 463 | /** 464 | * Menu gravity. Use one of the constants defined in the {@link it.sephiroth.android.library.floatingmenu 465 | * .FloatingActionMenu.Gravity } class.
466 | * Both horizontal and vertical gravity should be passed using the '|' bitwise operator 467 | * Example:
468 | * 469 | * .withGravity(Gravity.RIGHT | Gravity.BOTTOM); 470 | * 471 | * 472 | * @param gravity 473 | * @return 474 | * @see it.sephiroth.android.library.floatingmenu.FloatingActionMenu.Gravity 475 | */ 476 | public Builder withGravity(int gravity) { 477 | this.gravity = gravity; 478 | return this; 479 | } 480 | 481 | /** 482 | * Distance of the menu from the top/bottom border. If gravity is Gravity.BOTTOM then the distance is measured from the 483 | * screen bottom. 484 | * 485 | * @param resId resource-id 486 | * @return 487 | */ 488 | public Builder withVerticalPadding(int resId) { 489 | verticalPadding = res.getDimensionPixelSize(resId); 490 | return this; 491 | } 492 | 493 | /** 494 | * Distance of the menu from the left/right border. If gravity is Gravity.RIGHT then the distance is measured from the 495 | * screen right. 496 | * 497 | * @param resId resource-id 498 | * @return 499 | */ 500 | public Builder withHorizontalPadding(int resId) { 501 | horitontalPadding = res.getDimensionPixelSize(resId); 502 | return this; 503 | } 504 | 505 | /** 506 | * Movement required to hide/show the floating menu 507 | * 508 | * @param resId resource-id 509 | * @return 510 | */ 511 | public Builder withThreshold(int resId) { 512 | this.threshold = res.getDimensionPixelSize(resId); 513 | return this; 514 | } 515 | 516 | /** 517 | * Gap distance between items 518 | * 519 | * @param resId resource-id 520 | * @return 521 | */ 522 | public Builder withGap(int resId) { 523 | gap = res.getDimensionPixelSize(resId); 524 | return this; 525 | } 526 | 527 | /** 528 | * Set a scroll delegate 529 | * 530 | * @param delegate 531 | * @return 532 | */ 533 | public Builder withScrollDelegate(ScrollDelegate delegate) { 534 | this.scrollDelegate = delegate; 535 | return this; 536 | } 537 | 538 | public Builder visible(boolean visible) { 539 | this.visible = visible; 540 | return this; 541 | } 542 | 543 | public Builder addItem(FloatingActionItem item) { 544 | items.add(item); 545 | return this; 546 | } 547 | 548 | public FloatingActionMenu build() { 549 | return new FloatingActionMenu(this); 550 | } 551 | } 552 | 553 | /** 554 | * Default scroll delegate abstract class 555 | */ 556 | public static interface ScrollDelegate { 557 | void setOnScrollListener(AbsListView.OnScrollListener listener); 558 | } 559 | 560 | /** 561 | * Scroll delegate used for all the AbsListView(s) 562 | */ 563 | public static class AbsListViewScrollDelegate implements ScrollDelegate { 564 | final AbsListView listView; 565 | 566 | public AbsListViewScrollDelegate(AbsListView listView) { 567 | this.listView = listView; 568 | } 569 | 570 | @Override 571 | public void setOnScrollListener(final AbsListView.OnScrollListener listener) { 572 | listView.setOnScrollListener(listener); 573 | } 574 | } 575 | 576 | static public enum Direction { 577 | Horizontal, Vertical 578 | } 579 | 580 | @SuppressWarnings ("unused") 581 | static public class Gravity { 582 | public static final int LEFT = 1 << 0; 583 | public static final int RIGHT = 1 << 1; 584 | public static final int TOP = 1 << 2; 585 | public static final int BOTTOM = 1 << 3; 586 | public static final int CENTER_VERTICAL = 1 << 4; 587 | public static final int CENTER_HORIZONTAL = 1 << 5; 588 | public static final int CENTER = CENTER_HORIZONTAL | CENTER_VERTICAL; 589 | } 590 | 591 | private static class RootTag { 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/anim-v21/action_item_raise.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/anim/action_item_raise.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/drawable-v21/action_item_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/drawable-xxhdpi/action_item_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/android-floating-action-menu/6529698eddfaac1a5e7c69cb3cd11d6f3f76fad4/floatingmenu/src/main/res/drawable-xxhdpi/action_item_background.png -------------------------------------------------------------------------------- /floatingmenu/src/main/res/drawable/action_item_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/layout/floating_action_item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /floatingmenu/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2dip 4 | 8dip 5 | 18dip 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=1.0.7 2 | VERSION_CODE=15 3 | GROUP=it.sephiroth.android.library.floatingmenu 4 | 5 | POM_DESCRIPTION=Floating Action Menu for Android. Inspired by the Google Plus floating menu 6 | POM_URL=https://github.com/sephiroth74/android-floating-action-menu 7 | POM_SCM_URL=https://github.com/sephiroth74/android-floating-action-menu 8 | POM_SCM_CONNECTION=scm:git@github.com:sephiroth74/android-floating-action-menu.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:sephiroth74/android-floating-action-menu.git 10 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 11 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=sephiroth74 14 | POM_DEVELOPER_NAME=Alessandro Crugnola 15 | POM_DEVELOPER_EMAIL=alessandro.crugnola@gmail.com 16 | POM_DEVELOPER_URL=http://blog.sephiroth.it 17 | POM_DEVELOPER_ROLE=author 18 | 19 | ANDROID_BUILD_TARGET_SDK_VERSION=21 20 | ANDROID_BUILD_TOOLS_VERSION=21.1.2 21 | ANDROID_BUILD_SDK_VERSION=21 22 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':floatingmenu' 2 | include ':app' 3 | 4 | --------------------------------------------------------------------------------