├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.md.template ├── bottomsheet-commons ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── android │ │ └── support │ │ │ └── v4 │ │ │ └── app │ │ │ └── AccessFragmentInternals.java │ └── com │ │ └── flipboard │ │ └── bottomsheet │ │ └── commons │ │ ├── BottomSheetFragment.java │ │ ├── BottomSheetFragmentDelegate.java │ │ ├── BottomSheetFragmentInterface.java │ │ ├── ImagePickerSheetImageView.java │ │ ├── ImagePickerSheetView.java │ │ ├── IntentPickerSheetView.java │ │ ├── MenuSheetView.java │ │ └── Util.java │ └── res │ ├── drawable-hdpi │ ├── bottomsheet_camera.png │ └── bottomsheet_collections.png │ ├── drawable-xhdpi │ ├── bottomsheet_camera.png │ └── bottomsheet_collections.png │ ├── drawable-xxhdpi │ ├── bottomsheet_camera.png │ └── bottomsheet_collections.png │ ├── drawable-xxxhdpi │ ├── bottomsheet_camera.png │ └── bottomsheet_collections.png │ ├── layout │ ├── grid_sheet_view.xml │ ├── list_sheet_view.xml │ ├── sheet_grid_item.xml │ ├── sheet_image_grid_item.xml │ ├── sheet_list_item.xml │ ├── sheet_list_item_separator.xml │ └── sheet_list_item_subheader.xml │ └── values │ ├── colors.xml │ └── dimen.xml ├── bottomsheet-sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── flipboard │ │ └── bottomsheet │ │ └── sample │ │ ├── BottomSheetFragmentActivity.java │ │ ├── ImagePickerActivity.java │ │ ├── MainActivity.java │ │ ├── MenuActivity.java │ │ ├── MyFragment.java │ │ └── PickerActivity.java │ └── res │ ├── layout │ ├── activity_bottom_sheet_fragment.xml │ ├── activity_image_picker.xml │ ├── activity_main.xml │ ├── activity_menu.xml │ ├── activity_picker.xml │ └── fragment_my.xml │ ├── menu │ └── create.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── bottomsheet ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── flipboard │ │ └── bottomsheet │ │ ├── BaseViewTransformer.java │ │ ├── BottomSheetLayout.java │ │ ├── OnSheetDismissedListener.java │ │ └── ViewTransformer.java │ └── res │ ├── values-sw600dp │ ├── bool.xml │ └── dimens.xml │ ├── values-sw720dp │ ├── bool.xml │ └── dimens.xml │ └── values │ ├── bool.xml │ └── dimens.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── version.properties /.gitignore: -------------------------------------------------------------------------------- 1 | ###Android### 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | 31 | ###OSX### 32 | 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Icon must end with two \r 38 | Icon 39 | 40 | # Thumbnails 41 | ._* 42 | 43 | # Files that might appear on external disk 44 | .Spotlight-V100 45 | .Trashes 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | 55 | ###Linux### 56 | 57 | *~ 58 | 59 | # KDE directory preferences 60 | .directory 61 | 62 | 63 | ###Windows### 64 | 65 | # Windows image file caches 66 | Thumbs.db 67 | ehthumbs.db 68 | 69 | # Folder config file 70 | Desktop.ini 71 | 72 | # Recycle Bin used on file shares 73 | $RECYCLE.BIN/ 74 | 75 | # Windows Installer files 76 | *.cab 77 | *.msi 78 | *.msm 79 | *.msp 80 | 81 | # Windows shortcuts 82 | *.lnk 83 | 84 | 85 | ###IntelliJ### 86 | 87 | *.iml 88 | *.ipr 89 | *.iws 90 | .idea/ 91 | 92 | 93 | ###Gradle### 94 | 95 | .gradle 96 | build/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - platform-tools 5 | - tools 6 | - build-tools-23.0.2 7 | - android-23 8 | - extra-android-m2repository 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Flipboard 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of Flipboard nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BottomSheet 2 | 3 | [![Build Status](https://travis-ci.org/Flipboard/bottomsheet.svg)](https://travis-ci.org/Flipboard/bottomsheet) [![Join the chat at https://gitter.im/Flipboard/bottomsheet](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Flipboard/bottomsheet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | BottomSheet is an Android component which presents a dismissible view from the bottom of the screen. BottomSheet can be a useful replacement for dialogs and menus but can hold any view so the use cases are endless. This repository includes the BottomSheet component itself but also includes a set of common view components presented in a bottom sheet. These are located in the commons module. 6 | 7 | BottomSheet has been used in production at Flipboard for a while now so it is thoroughly tested. Here is a GIF of it in action inside of Flipboard! 8 | 9 | ![FlipUI gif](http://i.imgur.com/2e3ZhoU.gif) 10 | 11 | ## Installation 12 | 13 | If all you want is the BottomSheet component and don't need things from commons you can skip that dependency. 14 | 15 | ```groovy 16 | repositories { 17 | jcenter() 18 | } 19 | 20 | dependencies { 21 | compile 'com.flipboard:bottomsheet-core:1.5.3' 22 | compile 'com.flipboard:bottomsheet-commons:1.5.3' // optional 23 | } 24 | ``` 25 | 26 | ## Getting Started 27 | 28 | Get started by wrapping your layout in a `BottomSheetLayout`. So if you currently have this: 29 | 30 | ```xml 31 | 36 | 37 | 41 | 42 | 43 | ``` 44 | 45 | You would have to update it to look like this: 46 | 47 | ```xml 48 | 52 | 53 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | Back in your activity or fragment you would get a reference to the BottomSheetLayout like any other view. 70 | 71 | ```java 72 | BottomSheetLayout bottomSheet = (BottomSheetLayout) findViewById(R.id.bottomsheet); 73 | ``` 74 | 75 | Now all you need to do is show a view in the bottomSheet: 76 | 77 | ```java 78 | bottomSheet.showWithSheetView(LayoutInflater.from(context).inflate(R.layout.my_sheet_layout, bottomSheet, false)); 79 | ``` 80 | 81 | You could also use one of the sheet views from the commons module. 82 | 83 | ```java 84 | bottomSheet.showWithSheetView(new IntentPickerSheetView(this, shareIntent, "Share with...", new IntentPickerSheetView.OnIntentPickedListener() { 85 | @Override 86 | public void onIntentPicked(IntentPickerSheetView.ActivityInfo activityInfo) { 87 | bottomSheet.dismissSheet(); 88 | startActivity(activityInfo.getConcreteIntent(shareIntent)); 89 | } 90 | })); 91 | ``` 92 | 93 | That's it for the simplest of use cases. Check out the [API documentation](https://github.com/Flipboard/bottomsheet/wiki/API-Documentation) to find out how to customize BottomSheet to fit your use cases. 94 | 95 | For more examples, also see the [Recipes](https://github.com/Flipboard/bottomsheet/wiki/Recipes) wiki. 96 | 97 | ## Common Components 98 | 99 | These are located in the optional `bottomsheet-commons` dependency and implement common use cases for bottom sheet. 100 | 101 | Intent Picker | Menu Sheet | ImagePicker Sheet 102 | --- | --- | --- 103 | ![IntentPickerSheetView gif](http://i.imgur.com/wr9HJD1.gif) | ![MenuSheetView gif](http://i.imgur.com/f2j9Y5e.gif) | ![ImagePickerSheetView gif](https://camo.githubusercontent.com/23a9cf2bf9353a98d1b585e79d06639c7f5297c7/687474703a2f2f692e696d6775722e636f6d2f6f67764b4735692e676966) 104 | 105 | ### IntentPickerSheetView 106 | 107 | This component presents an intent chooser in the form of a BottomSheet view. Give it an intent such as a share intent and let the user choose what activity they want to share the intent with in a BottomSheet. 108 | 109 | Example from the sample app. 110 | 111 | ```java 112 | IntentPickerSheetView intentPickerSheet = new IntentPickerSheetView(MainActivity.this, shareIntent, "Share with...", new IntentPickerSheetView.OnIntentPickedListener() { 113 | @Override 114 | public void onIntentPicked(IntentPickerSheetView.ActivityInfo activityInfo) { 115 | bottomSheet.dismissSheet(); 116 | startActivity(activityInfo.getConcreteIntent(shareIntent)); 117 | } 118 | }); 119 | // Filter out built in sharing options such as bluetooth and beam. 120 | intentPickerSheet.setFilter(new IntentPickerSheetView.Filter() { 121 | @Override 122 | public boolean include(IntentPickerSheetView.ActivityInfo info) { 123 | return !info.componentName.getPackageName().startsWith("com.android"); 124 | } 125 | }); 126 | // Sort activities in reverse order for no good reason 127 | intentPickerSheet.setSortMethod(new Comparator() { 128 | @Override 129 | public int compare(IntentPickerSheetView.ActivityInfo lhs, IntentPickerSheetView.ActivityInfo rhs) { 130 | return rhs.label.compareTo(lhs.label); 131 | } 132 | }); 133 | bottomSheet.showWithSheetView(intentPickerSheet); 134 | ``` 135 | 136 | ### MenuSheetView 137 | 138 | This component presents a BottomSheet view that's backed by a menu. It behaves similarly to the new `NavigationView` in the Design support library, and is intended to mimic the examples in the Material Design spec. It supports list and grid states, with the former adding further support for separators and subheaders. 139 | 140 | Example from the sample app. 141 | 142 | ```java 143 | MenuSheetView menuSheetView = 144 | new MenuSheetView(MenuActivity.this, MenuSheetView.MenuType.LIST, "Create...", new MenuSheetView.OnMenuItemClickListener() { 145 | @Override 146 | public boolean onMenuItemClick(MenuItem item) { 147 | Toast.makeText(MenuActivity.this, item.getTitle(), Toast.LENGTH_SHORT).show(); 148 | if (bottomSheetLayout.isSheetShowing()) { 149 | bottomSheetLayout.dismissSheet(); 150 | } 151 | return true; 152 | } 153 | }); 154 | menuSheetView.inflateMenu(R.menu.create); 155 | bottomSheetLayout.showWithSheetView(menuSheetView); 156 | ``` 157 | 158 | ## Contributing 159 | 160 | We welcome pull requests for bug fixes, new features, and improvements to BottomSheet. Contributors to the main BottomSheet repository must accept Flipboard's Apache-style [Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/1gh9y6_i8xFn6pA15PqFeye19VqasuI9-bGp_e0owy74/viewform) before any changes can be merged. 161 | -------------------------------------------------------------------------------- /README.md.template: -------------------------------------------------------------------------------- 1 | #BottomSheet 2 | 3 | [![Build Status](https://travis-ci.org/Flipboard/bottomsheet.svg)](https://travis-ci.org/Flipboard/bottomsheet) [![Join the chat at https://gitter.im/Flipboard/bottomsheet](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Flipboard/bottomsheet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | BottomSheet is an Android component which presents a dismissible view from the bottom of the screen. BottomSheet can be a useful replacement for dialogs and menus but can hold any view so the use cases are endless. This repository includes the BottomSheet component itself but also includes a set of common view components presented in a bottom sheet. These are located in the commons module. 6 | 7 | BottomSheet has been used in production at Flipboard for a while now so it is thoroughly tested. Here is a GIF of it in action inside of Flipboard! 8 | 9 | ![FlipUI gif](http://i.imgur.com/2e3ZhoU.gif) 10 | 11 | ##Installation 12 | If all you want is the BottomSheet component and don't need things from commons you can skip that dependency. 13 | ```groovy 14 | repositories { 15 | jcenter() 16 | } 17 | 18 | dependencies { 19 | compile 'com.flipboard:bottomsheet-core:%%version%%' 20 | compile 'com.flipboard:bottomsheet-commons:%%version%%' // optional 21 | } 22 | ``` 23 | 24 | ##Getting Started 25 | Get started by wrapping your layout in a `BottomSheetLayout`. So if you currently have this: 26 | ```xml 27 | 32 | 33 | 37 | 38 | 39 | ``` 40 | 41 | You would have to update it to look like this: 42 | ```xml 43 | 47 | 48 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | ``` 63 | 64 | Back in your activity or fragment you would get a reference to the BottomSheetLayout like any other view. 65 | ```java 66 | BottomSheetLayout bottomSheet = (BottomSheetLayout) findViewById(R.id.bottomsheet); 67 | ``` 68 | 69 | Now all you need to do is show a view in the bottomSheet: 70 | ```java 71 | bottomSheet.showWithSheetView(LayoutInflater.from(context).inflate(R.layout.my_sheet_layout, bottomSheet, false)); 72 | ``` 73 | 74 | You could also use one of the sheet views from the commons module. 75 | ```java 76 | bottomSheet.showWithSheetView(new IntentPickerSheetView(this, shareIntent, "Share with...", new IntentPickerSheetView.OnIntentPickedListener() { 77 | @Override 78 | public void onIntentPicked(IntentPickerSheetView.ActivityInfo activityInfo) { 79 | bottomSheet.dismissSheet(); 80 | startActivity(activityInfo.getConcreteIntent(shareIntent)); 81 | } 82 | })); 83 | ``` 84 | 85 | That's it for the simplest of use cases. Check out the [API documentation](https://github.com/Flipboard/bottomsheet/wiki/API-Documentation) to find out how to customize BottomSheet to fit your use cases. 86 | 87 | For more examples, also see the [Recipes](https://github.com/Flipboard/bottomsheet/wiki/Recipes) wiki. 88 | 89 | ##Common Components 90 | These are located in the optional `bottomsheet-commons` dependency and implement common use cases for bottom sheet. 91 | 92 | Intent Picker | Menu Sheet | ImagePicker Sheet 93 | --- | --- | --- 94 | ![IntentPickerSheetView gif](http://i.imgur.com/wr9HJD1.gif) | ![MenuSheetView gif](http://i.imgur.com/f2j9Y5e.gif) | ![ImagePickerSheetView gif](https://camo.githubusercontent.com/23a9cf2bf9353a98d1b585e79d06639c7f5297c7/687474703a2f2f692e696d6775722e636f6d2f6f67764b4735692e676966) 95 | 96 | ####IntentPickerSheetView 97 | This component presents an intent chooser in the form of a BottomSheet view. Give it an intent such as a share intent and let the user choose what activity they want to share the intent with in a BottomSheet. 98 | 99 | Example from the sample app. 100 | ```java 101 | IntentPickerSheetView intentPickerSheet = new IntentPickerSheetView(MainActivity.this, shareIntent, "Share with...", new IntentPickerSheetView.OnIntentPickedListener() { 102 | @Override 103 | public void onIntentPicked(IntentPickerSheetView.ActivityInfo activityInfo) { 104 | bottomSheet.dismissSheet(); 105 | startActivity(activityInfo.getConcreteIntent(shareIntent)); 106 | } 107 | }); 108 | // Filter out built in sharing options such as bluetooth and beam. 109 | intentPickerSheet.setFilter(new IntentPickerSheetView.Filter() { 110 | @Override 111 | public boolean include(IntentPickerSheetView.ActivityInfo info) { 112 | return !info.componentName.getPackageName().startsWith("com.android"); 113 | } 114 | }); 115 | // Sort activities in reverse order for no good reason 116 | intentPickerSheet.setSortMethod(new Comparator() { 117 | @Override 118 | public int compare(IntentPickerSheetView.ActivityInfo lhs, IntentPickerSheetView.ActivityInfo rhs) { 119 | return rhs.label.compareTo(lhs.label); 120 | } 121 | }); 122 | bottomSheet.showWithSheetView(intentPickerSheet); 123 | ``` 124 | 125 | ####MenuSheetView 126 | This component presents a BottomSheet view that's backed by a menu. It behaves similarly to the new `NavigationView` in the Design support library, and is intended to mimic the examples in the Material Design spec. It supports list and grid states, with the former adding further support for separators and subheaders. 127 | 128 | Example from the sample app. 129 | ```java 130 | MenuSheetView menuSheetView = 131 | new MenuSheetView(MenuActivity.this, MenuSheetView.MenuType.LIST, "Create...", new MenuSheetView.OnMenuItemClickListener() { 132 | @Override 133 | public boolean onMenuItemClick(MenuItem item) { 134 | Toast.makeText(MenuActivity.this, item.getTitle(), Toast.LENGTH_SHORT).show(); 135 | if (bottomSheetLayout.isSheetShowing()) { 136 | bottomSheetLayout.dismissSheet(); 137 | } 138 | return true; 139 | } 140 | }); 141 | menuSheetView.inflateMenu(R.menu.create); 142 | bottomSheetLayout.showWithSheetView(menuSheetView); 143 | ``` 144 | 145 | ##Contributing 146 | We welcome pull requests for bug fixes, new features, and improvements to BottomSheet. Contributors to the main BottomSheet repository must accept Flipboard's Apache-style [Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/1gh9y6_i8xFn6pA15PqFeye19VqasuI9-bGp_e0owy74/viewform) before any changes can be merged. 147 | -------------------------------------------------------------------------------- /bottomsheet-commons/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' 3 | 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion '23.0.2' 7 | 8 | defaultConfig { 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | } 12 | lintOptions { 13 | abortOnError false 14 | } 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_7 17 | targetCompatibility JavaVersion.VERSION_1_7 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile project(':bottomsheet') 24 | compile 'com.android.support:appcompat-v7:23.2.0' 25 | } 26 | 27 | publish { 28 | userOrg = 'flipboard' 29 | groupId = 'com.flipboard' 30 | artifactId = 'bottomsheet-commons' 31 | publishVersion = VERSION 32 | description = 'Common components library for BottomSheet' 33 | website = 'https://github.com/Flipboard/bottomsheet' 34 | licences = ['BSD 3-Clause'] 35 | } 36 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/android/support/v4/app/AccessFragmentInternals.java: -------------------------------------------------------------------------------- 1 | package android.support.v4.app; 2 | 3 | public final class AccessFragmentInternals { 4 | private AccessFragmentInternals() { 5 | throw new AssertionError("No instances."); 6 | } 7 | 8 | public static int getContainerId(Fragment fragment) { 9 | return fragment.mContainerId; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/BottomSheetFragment.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.annotation.IdRes; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentManager; 9 | import android.support.v4.app.FragmentTransaction; 10 | import android.view.LayoutInflater; 11 | 12 | import com.flipboard.bottomsheet.BottomSheetLayout; 13 | import com.flipboard.bottomsheet.ViewTransformer; 14 | 15 | /** 16 | * A fragment that shows itself in a {@link BottomSheetLayout}. Like a {@link 17 | * android.support.v4.app.DialogFragment}, you can show this either in a bottom sheet by using 18 | * {@link #show(FragmentManager, int)} or attach it to a view with the normal fragment transaction 19 | * methods. 20 | *

21 | * If you don't want to extend from this for your fragment instance, you can use {@link BottomSheetFragmentDelegate} 22 | * in your fragment implementation instead. You must, however, still implement {@link BottomSheetFragmentInterface}. 23 | */ 24 | public class BottomSheetFragment extends Fragment implements BottomSheetFragmentInterface { 25 | 26 | private BottomSheetFragmentDelegate delegate; 27 | 28 | public BottomSheetFragment() { } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | @Override 34 | public void show(FragmentManager manager, @IdRes int bottomSheetLayoutId) { 35 | getDelegate().show(manager, bottomSheetLayoutId); 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | @Override 42 | public int show(FragmentTransaction transaction, @IdRes int bottomSheetLayoutId) { 43 | return getDelegate().show(transaction, bottomSheetLayoutId); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | @Override 50 | public void dismiss() { 51 | getDelegate().dismiss(); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | @Override 58 | public void dismissAllowingStateLoss() { 59 | getDelegate().dismissAllowingStateLoss(); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | @Override 66 | public ViewTransformer getViewTransformer() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public void onAttach(Context context) { 72 | super.onAttach(context); 73 | getDelegate().onAttach(context); 74 | } 75 | 76 | @Override 77 | public void onDetach() { 78 | super.onDetach(); 79 | getDelegate().onDetach(); 80 | } 81 | 82 | @Override 83 | public void onCreate(Bundle savedInstanceState) { 84 | super.onCreate(savedInstanceState); 85 | getDelegate().onCreate(savedInstanceState); 86 | } 87 | 88 | @Override 89 | public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { 90 | return getDelegate().getLayoutInflater(savedInstanceState, super.getLayoutInflater(savedInstanceState)); 91 | } 92 | 93 | @Override 94 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 95 | super.onActivityCreated(savedInstanceState); 96 | getDelegate().onActivityCreated(savedInstanceState); 97 | } 98 | 99 | @Override 100 | public void onStart() { 101 | super.onStart(); 102 | getDelegate().onStart(); 103 | } 104 | 105 | @Override 106 | public void onSaveInstanceState(Bundle outState) { 107 | super.onSaveInstanceState(outState); 108 | getDelegate().onSaveInstanceState(outState); 109 | } 110 | 111 | @Override 112 | public void onDestroyView() { 113 | getDelegate().onDestroyView(); 114 | super.onDestroyView(); 115 | } 116 | 117 | private BottomSheetFragmentDelegate getDelegate() { 118 | if (delegate == null) { 119 | delegate = BottomSheetFragmentDelegate.create(this); 120 | } 121 | return delegate; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/BottomSheetFragmentDelegate.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.support.annotation.CallSuper; 7 | import android.support.annotation.CheckResult; 8 | import android.support.annotation.IdRes; 9 | import android.support.annotation.Nullable; 10 | import android.support.v4.app.AccessFragmentInternals; 11 | import android.support.v4.app.Fragment; 12 | import android.support.v4.app.FragmentManager; 13 | import android.support.v4.app.FragmentTransaction; 14 | import android.view.LayoutInflater; 15 | import android.view.View; 16 | 17 | import com.flipboard.bottomsheet.BottomSheetLayout; 18 | import com.flipboard.bottomsheet.OnSheetDismissedListener; 19 | 20 | /** 21 | * This class represents a delegate which you can use to extend BottomSheet's fragment support to any 22 | * {@link Fragment} implementing {@link BottomSheetFragmentInterface}. 23 | *

24 | * When using an {@link BottomSheetFragmentDelegate}, you should any methods exposed in it rather than the 25 | * {@link Fragment} method of the same name. This applies to: 26 | *

32 | * There also some Fragment lifecycle methods which should be proxied to the delegate: 33 | * 43 | *

44 | * An {@link Activity} can only be linked with one {@link BottomSheetFragmentDelegate} instance, 45 | * so the instance returned from {@link #create(BottomSheetFragmentInterface)} should be kept 46 | * until the Activity is destroyed. 47 | */ 48 | public final class BottomSheetFragmentDelegate implements OnSheetDismissedListener { 49 | 50 | private static final String SAVED_SHOWS_BOTTOM_SHEET = "bottomsheet:savedBottomSheet"; 51 | private static final String SAVED_BACK_STACK_ID = "bottomsheet:backStackId"; 52 | private static final String SAVED_BOTTOM_SHEET_LAYOUT_ID = "bottomsheet:bottomSheetLayoutId"; 53 | 54 | @IdRes 55 | private int bottomSheetLayoutId = View.NO_ID; 56 | private BottomSheetLayout bottomSheetLayout; 57 | private boolean dismissed; 58 | private boolean shownByMe; 59 | private boolean viewDestroyed; 60 | private boolean showsBottomSheet = true; 61 | private int backStackId = -1; 62 | 63 | private BottomSheetFragmentInterface sheetFragmentInterface; 64 | private Fragment fragment; 65 | 66 | public static BottomSheetFragmentDelegate create(BottomSheetFragmentInterface sheetFragmentInterface) { 67 | return new BottomSheetFragmentDelegate(sheetFragmentInterface); 68 | } 69 | 70 | private BottomSheetFragmentDelegate(BottomSheetFragmentInterface sheetFragmentInterface) { 71 | 72 | if (!(sheetFragmentInterface instanceof Fragment)) { 73 | throw new IllegalArgumentException("sheetFragmentInterface must be an instance of a Fragment too!"); 74 | } 75 | 76 | this.sheetFragmentInterface = sheetFragmentInterface; 77 | this.fragment = (Fragment) sheetFragmentInterface; 78 | } 79 | 80 | /** 81 | * DialogFragment-like show() method for displaying this the associated sheet fragment 82 | * 83 | * @param manager FragmentManager instance 84 | * @param bottomSheetLayoutId Resource ID of the {@link BottomSheetLayout} 85 | */ 86 | public void show(FragmentManager manager, @IdRes int bottomSheetLayoutId) { 87 | dismissed = false; 88 | shownByMe = true; 89 | this.bottomSheetLayoutId = bottomSheetLayoutId; 90 | manager.beginTransaction() 91 | .add(fragment, String.valueOf(bottomSheetLayoutId)) 92 | .commit(); 93 | } 94 | 95 | /** 96 | * DialogFragment-like show() method for displaying this the associated sheet fragment 97 | * 98 | * @param transaction FragmentTransaction instance 99 | * @param bottomSheetLayoutId Resource ID of the {@link BottomSheetLayout} 100 | * @return the back stack ID of the fragment after the transaction is committed. 101 | */ 102 | public int show(FragmentTransaction transaction, @IdRes int bottomSheetLayoutId) { 103 | dismissed = false; 104 | shownByMe = true; 105 | this.bottomSheetLayoutId = bottomSheetLayoutId; 106 | transaction.add(fragment, String.valueOf(bottomSheetLayoutId)); 107 | viewDestroyed = false; 108 | backStackId = transaction.commit(); 109 | return backStackId; 110 | } 111 | 112 | /** 113 | * Dismiss the fragment and it's bottom sheet. If the fragment was added to the back stack, all 114 | * back stack state up to and including this entry will be popped. Otherwise, a new transaction 115 | * will be committed to remove this fragment. 116 | */ 117 | public void dismiss() { 118 | dismissInternal(/*allowStateLoss=*/false); 119 | } 120 | 121 | /** 122 | * Version of {@link #dismiss()} that uses {@link FragmentTransaction#commitAllowingStateLoss()}. 123 | * See linked documentation for further details. 124 | */ 125 | public void dismissAllowingStateLoss() { 126 | dismissInternal(/*allowStateLoss=*/true); 127 | } 128 | 129 | private void dismissInternal(boolean allowStateLoss) { 130 | if (dismissed) { 131 | return; 132 | } 133 | dismissed = true; 134 | shownByMe = false; 135 | if (bottomSheetLayout != null) { 136 | bottomSheetLayout.dismissSheet(); 137 | bottomSheetLayout = null; 138 | } 139 | viewDestroyed = true; 140 | if (backStackId >= 0) { 141 | fragment.getFragmentManager().popBackStack(backStackId, FragmentManager.POP_BACK_STACK_INCLUSIVE); 142 | backStackId = -1; 143 | } else { 144 | FragmentTransaction ft = fragment.getFragmentManager().beginTransaction(); 145 | ft.remove(fragment); 146 | if (allowStateLoss) { 147 | ft.commitAllowingStateLoss(); 148 | } else { 149 | ft.commit(); 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Corresponding onAttach() method 156 | * 157 | * @param context Context to match the Fragment API, unused. 158 | */ 159 | public void onAttach(Context context) { 160 | if (!shownByMe) { 161 | dismissed = false; 162 | } 163 | } 164 | 165 | /** 166 | * Corresponding onDetach() method 167 | */ 168 | public void onDetach() { 169 | if (!shownByMe && !dismissed) { 170 | dismissed = true; 171 | } 172 | } 173 | 174 | /** 175 | * Corresponding onCreate() method 176 | * 177 | * @param savedInstanceState Instance state, can be null. 178 | */ 179 | public void onCreate(@Nullable Bundle savedInstanceState) { 180 | showsBottomSheet = AccessFragmentInternals.getContainerId(fragment) == 0; 181 | 182 | if (savedInstanceState != null) { 183 | showsBottomSheet = savedInstanceState.getBoolean(SAVED_SHOWS_BOTTOM_SHEET, showsBottomSheet); 184 | backStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); 185 | bottomSheetLayoutId = savedInstanceState.getInt(SAVED_BOTTOM_SHEET_LAYOUT_ID, View.NO_ID); 186 | } 187 | } 188 | 189 | /** 190 | * Retrieves the appropriate layout inflater, either the sheet's or the view's super container. Note that you should 191 | * handle the result of this in your getLayoutInflater method. 192 | * 193 | * @param savedInstanceState Instance state, here to match Fragment API but unused. 194 | * @param superInflater The result of the view's inflater, usually the result of super.getLayoutInflater() 195 | * @return the layout inflater to use 196 | */ 197 | @CheckResult 198 | public LayoutInflater getLayoutInflater(Bundle savedInstanceState, LayoutInflater superInflater) { 199 | if (!showsBottomSheet) { 200 | return superInflater; 201 | } 202 | bottomSheetLayout = getBottomSheetLayout(); 203 | if (bottomSheetLayout != null) { 204 | return LayoutInflater.from(bottomSheetLayout.getContext()); 205 | } 206 | return LayoutInflater.from(fragment.getContext()); 207 | } 208 | 209 | /** 210 | * @return this fragment sheet's {@link BottomSheetLayout}. 211 | */ 212 | public BottomSheetLayout getBottomSheetLayout() { 213 | if (bottomSheetLayout == null) { 214 | bottomSheetLayout = findBottomSheetLayout(); 215 | } 216 | 217 | return bottomSheetLayout; 218 | } 219 | 220 | @Nullable 221 | private BottomSheetLayout findBottomSheetLayout() { 222 | Fragment parentFragment = fragment.getParentFragment(); 223 | if (parentFragment != null) { 224 | View view = parentFragment.getView(); 225 | if (view != null) { 226 | return (BottomSheetLayout) view.findViewById(bottomSheetLayoutId); 227 | } else { 228 | return null; 229 | } 230 | } 231 | Activity parentActivity = fragment.getActivity(); 232 | if (parentActivity != null) { 233 | return (BottomSheetLayout) parentActivity.findViewById(bottomSheetLayoutId); 234 | } 235 | return null; 236 | } 237 | 238 | /** 239 | * Corresponding onActivityCreated() method 240 | * 241 | * @param savedInstanceState Instance state, here to match the Fragment API but unused 242 | */ 243 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 244 | if (!showsBottomSheet) { 245 | return; 246 | } 247 | 248 | View view = fragment.getView(); 249 | if (view != null) { 250 | if (view.getParent() != null) { 251 | throw new IllegalStateException("BottomSheetFragment can not be attached to a container view"); 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Corresponding onStart() method 258 | */ 259 | public void onStart() { 260 | if (bottomSheetLayout != null) { 261 | viewDestroyed = false; 262 | bottomSheetLayout.showWithSheetView(fragment.getView(), sheetFragmentInterface.getViewTransformer()); 263 | bottomSheetLayout.addOnSheetDismissedListener(this); 264 | } 265 | } 266 | 267 | /** 268 | * Corresponding onSaveInstanceState() method 269 | * 270 | * @param outState The output state, here to match the Fragment API but unused 271 | */ 272 | public void onSaveInstanceState(Bundle outState) { 273 | if (!showsBottomSheet) { 274 | outState.putBoolean(SAVED_SHOWS_BOTTOM_SHEET, false); 275 | } 276 | if (backStackId != -1) { 277 | outState.putInt(SAVED_BACK_STACK_ID, backStackId); 278 | } 279 | if (bottomSheetLayoutId != View.NO_ID) { 280 | outState.putInt(SAVED_BOTTOM_SHEET_LAYOUT_ID, bottomSheetLayoutId); 281 | } 282 | } 283 | 284 | /** 285 | * Corresponding onDestroyView() method 286 | */ 287 | public void onDestroyView() { 288 | if (bottomSheetLayout != null) { 289 | viewDestroyed = true; 290 | bottomSheetLayout.dismissSheet(); 291 | bottomSheetLayout = null; 292 | } 293 | } 294 | 295 | @Override 296 | @CallSuper 297 | public void onDismissed(BottomSheetLayout bottomSheetLayout) { 298 | if (!viewDestroyed) { 299 | dismissInternal(true); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/BottomSheetFragmentInterface.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.support.annotation.IdRes; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentTransaction; 6 | 7 | import com.flipboard.bottomsheet.ViewTransformer; 8 | 9 | /** 10 | * This interface can be applied to a {@link android.support.v4.app.Fragment} to make it compatible with 11 | * {@link BottomSheetFragmentDelegate}. You should only have to implement 12 | */ 13 | public interface BottomSheetFragmentInterface { 14 | 15 | /** 16 | * Display the bottom sheet, adding the fragment to the given FragmentManager. This does 17 | * not add the transaction to the back stack. When teh fragment is dismissed, a new 18 | * transaction will be executed to remove it from the activity. 19 | * 20 | * @param manager The FragmentManager this fragment will be added to. 21 | * @param bottomSheetLayoutId The bottom sheet layoutId in the parent view to attach the 22 | * fragment to. 23 | */ 24 | void show(FragmentManager manager, @IdRes int bottomSheetLayoutId); 25 | 26 | /** 27 | * Display the bottom sheet, adding the fragment using an excising transaction and then 28 | * committing the transaction. 29 | * 30 | * @param transaction An existing transaction in which to add the fragment. 31 | * @param bottomSheetLayoutId The bottom sheet layoutId in the parent view to attach the 32 | * fragment to. 33 | */ 34 | int show(FragmentTransaction transaction, @IdRes int bottomSheetLayoutId); 35 | 36 | /** 37 | * Dismiss the fragment and it's bottom sheet. If the fragment was added to the back stack, all 38 | * back stack state up to and including this entry will be popped. Otherwise, a new transaction 39 | * will be committed to remove this fragment. 40 | */ 41 | void dismiss(); 42 | 43 | /** 44 | * Version of {@link #dismiss()} that uses {@link FragmentTransaction#commitAllowingStateLoss()}. 45 | * See linked documentation for further details. 46 | */ 47 | void dismissAllowingStateLoss(); 48 | 49 | /** 50 | * Override this to proved a custom {@link ViewTransformer}. 51 | */ 52 | ViewTransformer getViewTransformer(); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/ImagePickerSheetImageView.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.widget.ImageView; 8 | 9 | /** 10 | * An ImageView that tries to keep a 1:1 aspect ratio 11 | */ 12 | final class ImagePickerSheetImageView extends ImageView { 13 | public ImagePickerSheetImageView(Context context) { 14 | super(context); 15 | } 16 | 17 | public ImagePickerSheetImageView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public ImagePickerSheetImageView(Context context, AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 26 | public ImagePickerSheetImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 27 | super(context, attrs, defStyleAttr, defStyleRes); 28 | } 29 | 30 | @SuppressWarnings("UnnecessaryLocalVariable") 31 | @Override 32 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 33 | int bothDimensionsSpec = widthMeasureSpec; 34 | super.onMeasure(bothDimensionsSpec, bothDimensionsSpec); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/ImagePickerSheetView.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.content.ContentResolver; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | import android.database.Cursor; 9 | import android.graphics.drawable.Drawable; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.provider.MediaStore; 13 | import android.support.annotation.CheckResult; 14 | import android.support.annotation.DrawableRes; 15 | import android.support.annotation.IntDef; 16 | import android.support.annotation.LayoutRes; 17 | import android.support.annotation.NonNull; 18 | import android.support.annotation.Nullable; 19 | import android.support.annotation.StringRes; 20 | import android.support.v4.content.ContextCompat; 21 | import android.support.v4.content.res.ResourcesCompat; 22 | import android.support.v4.view.ViewCompat; 23 | import android.text.TextUtils; 24 | import android.view.LayoutInflater; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.widget.AdapterView; 28 | import android.widget.BaseAdapter; 29 | import android.widget.FrameLayout; 30 | import android.widget.GridView; 31 | import android.widget.ImageView; 32 | import android.widget.TextView; 33 | 34 | import java.io.File; 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | 38 | import flipboard.bottomsheet.commons.R; 39 | 40 | import static com.flipboard.bottomsheet.commons.ImagePickerSheetView.ImagePickerTile.CAMERA; 41 | import static com.flipboard.bottomsheet.commons.ImagePickerSheetView.ImagePickerTile.PICKER; 42 | 43 | /** 44 | * A Sheet view for displaying recent images or options to pick or take pictures. 45 | */ 46 | @SuppressLint("ViewConstructor") 47 | public class ImagePickerSheetView extends FrameLayout { 48 | 49 | /** 50 | * Callback for whenever a tile is selected in the sheet. 51 | */ 52 | public interface OnTileSelectedListener { 53 | /** 54 | * @param selectedTile The selected tile, in the form of an {@link ImagePickerTile} 55 | */ 56 | void onTileSelected(ImagePickerTile selectedTile); 57 | } 58 | 59 | /** 60 | * Interface for providing an image given the {@link ImageView} and {@link Uri}. 61 | */ 62 | public interface ImageProvider { 63 | /** 64 | * This is called when the underlying adapter is ready to show an image 65 | * 66 | * @param imageView ImageView target. If you care about memory leaks and performance, DO NOT 67 | * HOLD ON TO THIS INSTANCE! 68 | * @param imageUri Uri for the image. 69 | * @param size Destination size of the image (it's a square, so assume this is the height 70 | * and width). 71 | */ 72 | void onProvideImage(ImageView imageView, Uri imageUri, int size); 73 | } 74 | 75 | /** 76 | * Backing class for image tiles in the grid. 77 | */ 78 | public static class ImagePickerTile { 79 | 80 | public static final int IMAGE = 1; 81 | public static final int CAMERA = 2; 82 | public static final int PICKER = 3; 83 | 84 | @IntDef({IMAGE, CAMERA, PICKER}) 85 | public @interface TileType {} 86 | 87 | @IntDef({CAMERA, PICKER}) 88 | public @interface SpecialTileType {} 89 | 90 | protected final Uri imageUri; 91 | protected final @TileType int tileType; 92 | 93 | ImagePickerTile(@SpecialTileType int tileType) { 94 | this(null, tileType); 95 | } 96 | 97 | ImagePickerTile(@NonNull Uri imageUri) { 98 | this(imageUri, IMAGE); 99 | } 100 | 101 | protected ImagePickerTile(@Nullable Uri imageUri, @TileType int tileType) { 102 | this.imageUri = imageUri; 103 | this.tileType = tileType; 104 | } 105 | 106 | /** 107 | * @return The image Uri backing this tile. Can be null if this is a placeholder for the 108 | * camera or picker tiles. 109 | */ 110 | @Nullable 111 | public Uri getImageUri() { 112 | return imageUri; 113 | } 114 | 115 | /** 116 | * @return The {@link TileType} of this tile: either {@link #IMAGE}, {@link #CAMERA}, or 117 | * {@link #PICKER} 118 | */ 119 | @TileType 120 | public int getTileType() { 121 | return tileType; 122 | } 123 | 124 | /** 125 | * Indicates whether or not this represents an image tile option. If it is, you can safely 126 | * retrieve the represented image's file Uri via {@link #getImageUri()} 127 | * 128 | * @return True if this is a camera option, false if not. 129 | */ 130 | public boolean isImageTile() { 131 | return tileType == IMAGE; 132 | } 133 | 134 | /** 135 | * Indicates whether or not this represents the camera tile option. If it is, you should do 136 | * something to facilitate taking a picture, such as firing a camera intent or using your 137 | * own. 138 | * 139 | * @return True if this is a camera option, false if not. 140 | */ 141 | public boolean isCameraTile() { 142 | return tileType == CAMERA; 143 | } 144 | 145 | /** 146 | * Indicates whether or not this represents the picker tile option. If it is, you should do 147 | * something to facilitate retrieving a picture from some other provider, such as firing an 148 | * image pick intent or retrieving it yourself. 149 | * 150 | * @return True if this is a picker tile, false if not. 151 | */ 152 | public boolean isPickerTile() { 153 | return tileType == PICKER; 154 | } 155 | 156 | @Override 157 | public String toString() { 158 | if (isImageTile()) { 159 | return "ImageTile: " + imageUri; 160 | } else if (isCameraTile()) { 161 | return "CameraTile"; 162 | } else if (isPickerTile()) { 163 | return "PickerTile"; 164 | } else { 165 | return "Invalid item"; 166 | } 167 | } 168 | } 169 | 170 | protected final TextView titleView; 171 | protected final GridView tileGrid; 172 | protected Adapter adapter; 173 | protected int thumbnailSize; 174 | protected final int spacing; 175 | protected final int originalGridPaddingTop; 176 | 177 | // Values provided by the builder 178 | protected int maxItems = 25; 179 | protected ImageProvider imageProvider; 180 | protected boolean showCameraOption = true; 181 | protected boolean showPickerOption = true; 182 | protected Drawable cameraDrawable = null; 183 | protected Drawable pickerDrawable = null; 184 | protected @LayoutRes int tileLayout = R.layout.sheet_image_grid_item; 185 | protected String title; 186 | private int columnWidthDp = 100; 187 | 188 | 189 | protected ImagePickerSheetView(final Builder builder) { 190 | super(builder.context); 191 | 192 | inflate(getContext(), R.layout.grid_sheet_view, this); 193 | 194 | // Set up the grid 195 | tileGrid = (GridView) findViewById(R.id.grid); 196 | spacing = getResources().getDimensionPixelSize(R.dimen.bottomsheet_image_tile_spacing); 197 | tileGrid.setDrawSelectorOnTop(true); 198 | tileGrid.setVerticalSpacing(spacing); 199 | tileGrid.setHorizontalSpacing(spacing); 200 | tileGrid.setPadding(spacing, 0, spacing, 0); 201 | 202 | // Set up the title 203 | titleView = (TextView) findViewById(R.id.title); 204 | originalGridPaddingTop = tileGrid.getPaddingTop(); 205 | setTitle(builder.title); 206 | 207 | // Hook up the remaining builder fields 208 | if (builder.onTileSelectedListener != null) { 209 | tileGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() { 210 | @Override 211 | public void onItemClick(@NonNull AdapterView parent, @NonNull View view, int position, long id) { 212 | builder.onTileSelectedListener.onTileSelected(adapter.getItem(position)); 213 | } 214 | }); 215 | } 216 | maxItems = builder.maxItems; 217 | imageProvider = builder.imageProvider; 218 | showCameraOption = builder.showCameraOption; 219 | showPickerOption = builder.showPickerOption; 220 | cameraDrawable = builder.cameraDrawable; 221 | pickerDrawable = builder.pickerDrawable; 222 | tileLayout = builder.tileLayout; 223 | 224 | ViewCompat.setElevation(this, Util.dp2px(getContext(), 16f)); 225 | } 226 | 227 | public void setTitle(@StringRes int titleRes) { 228 | setTitle(getResources().getString(titleRes)); 229 | } 230 | 231 | public void setTitle(String title) { 232 | this.title = title; 233 | if (!TextUtils.isEmpty(title)) { 234 | titleView.setText(title); 235 | } else { 236 | titleView.setVisibility(GONE); 237 | // Add some padding to the top to account for the missing title 238 | tileGrid.setPadding(tileGrid.getPaddingLeft(), originalGridPaddingTop + spacing, tileGrid.getPaddingRight(), tileGrid.getPaddingBottom()); 239 | } 240 | } 241 | 242 | public void setColumnWidthDp(int columnWidthDp) { 243 | this.columnWidthDp = columnWidthDp; 244 | } 245 | 246 | @Override 247 | protected void onAttachedToWindow() { 248 | super.onAttachedToWindow(); 249 | this.adapter = new Adapter(getContext()); 250 | tileGrid.setAdapter(this.adapter); 251 | } 252 | 253 | @Override 254 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 255 | // Necessary for showing elevation on 5.0+ 256 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 257 | setOutlineProvider(new Util.ShadowOutline(w, h)); 258 | } 259 | } 260 | 261 | @Override 262 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 263 | int width = MeasureSpec.getSize(widthMeasureSpec); 264 | float density = getResources().getDisplayMetrics().density; 265 | final int numColumns = (int) (width / (columnWidthDp * density)); 266 | thumbnailSize = Math.round((width - ((numColumns - 1) * spacing)) / 3.0f); 267 | tileGrid.setNumColumns(numColumns); 268 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 269 | } 270 | 271 | /** 272 | * Simple adapter that shows a grid of {@link ImagePickerSheetImageView}s that hold either a thumbnail of 273 | * the local image or placeholder for camera/picker actions. 274 | */ 275 | private class Adapter extends BaseAdapter { 276 | 277 | private List tiles = new ArrayList<>(); 278 | final LayoutInflater inflater; 279 | private final ContentResolver resolver; 280 | 281 | public Adapter(Context context) { 282 | inflater = LayoutInflater.from(context); 283 | 284 | if (showCameraOption) { 285 | tiles.add(new ImagePickerTile(CAMERA)); 286 | } 287 | if (showPickerOption) { 288 | tiles.add(new ImagePickerTile(PICKER)); 289 | } 290 | 291 | // Add local images, in descending order of date taken 292 | String[] projection = new String[]{ 293 | MediaStore.Images.ImageColumns._ID, 294 | MediaStore.Images.ImageColumns.DATA, 295 | MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, 296 | MediaStore.Images.ImageColumns.DATE_TAKEN, 297 | MediaStore.Images.ImageColumns.MIME_TYPE 298 | }; 299 | resolver = context.getContentResolver(); 300 | 301 | final Cursor cursor = resolver 302 | .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, 303 | null, MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"); 304 | 305 | if (cursor != null) { 306 | int count = 0; 307 | while (cursor.moveToNext() && count < maxItems) { 308 | String imageLocation = cursor.getString(1); 309 | File imageFile = new File(imageLocation); 310 | if (imageFile.exists()) { 311 | tiles.add(new ImagePickerTile(Uri.fromFile(imageFile))); 312 | } 313 | ++count; 314 | } 315 | cursor.close(); 316 | } 317 | } 318 | 319 | @Override 320 | public int getCount() { 321 | return tiles.size(); 322 | } 323 | 324 | @Override 325 | public ImagePickerTile getItem(int position) { 326 | return tiles.get(position); 327 | } 328 | 329 | @Override 330 | public long getItemId(int position) { 331 | return position; 332 | } 333 | 334 | @Override 335 | public View getView(int position, View recycled, @NonNull ViewGroup parent) { 336 | ImageView thumb; 337 | 338 | if (recycled == null) { 339 | if (tileLayout == 0) { 340 | thumb = (ImageView) inflater.inflate(R.layout.sheet_image_grid_item, parent, false); 341 | } else { 342 | thumb = (ImageView) inflater.inflate(tileLayout, parent, false); 343 | if (!(thumb instanceof ImageView)) { 344 | throw new IllegalArgumentException("Tile layout must have an ImageView as root view."); 345 | } 346 | } 347 | } else { 348 | thumb = (ImageView) recycled; 349 | } 350 | 351 | ImagePickerTile tile = tiles.get(position); 352 | thumb.setMinimumWidth(thumbnailSize); 353 | thumb.setMinimumHeight(thumbnailSize); 354 | thumb.setMaxHeight(thumbnailSize); 355 | thumb.setMaxWidth(thumbnailSize); 356 | if (tile.imageUri != null) { 357 | imageProvider.onProvideImage(thumb, tile.imageUri, thumbnailSize); 358 | } else { 359 | thumb.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 360 | if (tile.isCameraTile()) { 361 | thumb.setBackgroundResource(android.R.color.black); 362 | if (cameraDrawable == null) { 363 | thumb.setImageResource(R.drawable.bottomsheet_camera); 364 | } else { 365 | thumb.setImageDrawable(cameraDrawable); 366 | } 367 | } else if (tile.isPickerTile()) { 368 | thumb.setBackgroundResource(android.R.color.darker_gray); 369 | if (pickerDrawable == null) { 370 | thumb.setImageResource(R.drawable.bottomsheet_collections); 371 | } else { 372 | thumb.setImageDrawable(pickerDrawable); 373 | } 374 | } 375 | } 376 | 377 | return thumb; 378 | } 379 | } 380 | 381 | public static class Builder { 382 | 383 | Context context; 384 | int maxItems = 25; 385 | String title = null; 386 | OnTileSelectedListener onTileSelectedListener; 387 | ImageProvider imageProvider; 388 | boolean showCameraOption = true; 389 | boolean showPickerOption = true; 390 | Drawable cameraDrawable = null; 391 | Drawable pickerDrawable = null; 392 | @LayoutRes int tileLayout = 0; 393 | 394 | public Builder(@NonNull Context context) { 395 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN 396 | && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 397 | throw new RuntimeException("Missing required READ_EXTERNAL_STORAGE permission. Did you remember to request it first?"); 398 | } 399 | this.context = context; 400 | } 401 | 402 | /** 403 | * Sets the max number of tiles to show in the image picker. Default is 25 from local 404 | * storage and the two custom tiles. 405 | * 406 | * @param maxItems Max number of tiles to show 407 | * @return This builder instance 408 | */ 409 | public Builder setMaxItems(int maxItems) { 410 | this.maxItems = maxItems; 411 | return this; 412 | } 413 | 414 | /** 415 | * Sets a title via String resource ID. 416 | * 417 | * @param title String resource ID 418 | * @return This builder instance 419 | */ 420 | public Builder setTitle(@StringRes int title) { 421 | return setTitle(context.getString(title)); 422 | } 423 | 424 | /** 425 | * Sets a title for this sheet. If the title param is null, then no title will be shown and 426 | * the title view will be hidden. 427 | * 428 | * @param title The title String 429 | * @return This builder instance 430 | */ 431 | public Builder setTitle(@Nullable String title) { 432 | this.title = title; 433 | return this; 434 | } 435 | 436 | /** 437 | * Sets a listener for when a tile is selected. 438 | * 439 | * @param onTileSelectedListener Listener instance 440 | * @return This builder instance 441 | */ 442 | public Builder setOnTileSelectedListener(OnTileSelectedListener onTileSelectedListener) { 443 | this.onTileSelectedListener = onTileSelectedListener; 444 | return this; 445 | } 446 | 447 | /** 448 | * Sets a provider for providing images. 449 | * 450 | * @param imageProvider Provider instance 451 | * @return This builder instance 452 | */ 453 | public Builder setImageProvider(ImageProvider imageProvider) { 454 | this.imageProvider = imageProvider; 455 | return this; 456 | } 457 | 458 | /** 459 | * Sets a boolean to indicate whether or not to show a camera option. 460 | * 461 | * @param showCameraOption True to show the option, or false to hide the option 462 | * @return This builder instance 463 | */ 464 | public Builder setShowCameraOption(boolean showCameraOption) { 465 | this.showCameraOption = showCameraOption; 466 | return this; 467 | } 468 | 469 | /** 470 | * Sets a boolean to indicate whether or not to show the picker option. 471 | * 472 | * @param showPickerOption True to show the option, or false to hide the option. 473 | * @return This builder instance 474 | */ 475 | public Builder setShowPickerOption(boolean showPickerOption) { 476 | this.showPickerOption = showPickerOption; 477 | return this; 478 | } 479 | 480 | /** 481 | * Sets a drawable resource ID for the camera option tile. Default is to use the material 482 | * design version included in the library. 483 | * 484 | * @param resId Camera drawable resource ID 485 | * @return This builder instance 486 | */ 487 | public Builder setCameraDrawable(@DrawableRes int resId) { 488 | return setCameraDrawable(ResourcesCompat.getDrawable(context.getResources(), resId, null)); 489 | } 490 | 491 | /** 492 | * Sets a drawable for the camera option tile. Default is to use the material design 493 | * version included in the library. 494 | * 495 | * @param cameraDrawable Camera drawable instance 496 | * @return This builder instance 497 | */ 498 | public Builder setCameraDrawable(@Nullable Drawable cameraDrawable) { 499 | this.cameraDrawable = cameraDrawable; 500 | return this; 501 | } 502 | 503 | /** 504 | * Sets a drawable resource ID for the picker option tile. Default is to use the material 505 | * design version included in the library. 506 | * 507 | * @param resId Picker drawable resource ID 508 | * @return This builder instance 509 | */ 510 | public Builder setPickerDrawable(@DrawableRes int resId) { 511 | return setPickerDrawable(ResourcesCompat.getDrawable(context.getResources(), resId, null)); 512 | } 513 | 514 | /** 515 | * Sets a layout for the image tile which MUST have an ImageView as root view. Default is to 516 | * use plain image view included in the library. 517 | * 518 | * @param tileLayout Tile layout resource ID 519 | * @return This builder instance 520 | */ 521 | public Builder setTileLayout(@LayoutRes int tileLayout) { 522 | this.tileLayout = tileLayout; 523 | return this; 524 | } 525 | 526 | /** 527 | * Sets a drawable for the picker option tile. Default is to use the material design 528 | * version included in the library. 529 | * 530 | * @param pickerDrawable Picker drawable instance 531 | * @return This builder instance 532 | */ 533 | public Builder setPickerDrawable(Drawable pickerDrawable) { 534 | this.pickerDrawable = pickerDrawable; 535 | return this; 536 | } 537 | 538 | @CheckResult 539 | public ImagePickerSheetView create() { 540 | if (imageProvider == null) { 541 | throw new IllegalStateException("Must provide an ImageProvider!"); 542 | } 543 | return new ImagePickerSheetView(this); 544 | } 545 | } 546 | 547 | } 548 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/IntentPickerSheetView.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.content.pm.ResolveInfo; 9 | import android.graphics.drawable.Drawable; 10 | import android.os.AsyncTask; 11 | import android.os.Build; 12 | import android.support.annotation.NonNull; 13 | import android.support.annotation.StringRes; 14 | import android.support.v4.view.ViewCompat; 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.AdapterView; 19 | import android.widget.BaseAdapter; 20 | import android.widget.FrameLayout; 21 | import android.widget.GridView; 22 | import android.widget.ImageView; 23 | import android.widget.TextView; 24 | 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.Comparator; 28 | import java.util.List; 29 | 30 | import flipboard.bottomsheet.commons.R; 31 | 32 | @SuppressLint("ViewConstructor") 33 | public class IntentPickerSheetView extends FrameLayout { 34 | 35 | private int columnWidthDp = 100; 36 | 37 | public interface Filter { 38 | boolean include(ActivityInfo info); 39 | } 40 | 41 | public interface OnIntentPickedListener { 42 | void onIntentPicked(ActivityInfo activityInfo); 43 | } 44 | 45 | private class SortAlphabetically implements Comparator { 46 | @Override 47 | public int compare(ActivityInfo lhs, ActivityInfo rhs) { 48 | return lhs.label.compareTo(rhs.label); 49 | } 50 | } 51 | 52 | private class FilterNone implements Filter { 53 | @Override 54 | public boolean include(ActivityInfo info) { 55 | return true; 56 | } 57 | } 58 | 59 | @Override 60 | protected void onDetachedFromWindow() { 61 | super.onDetachedFromWindow(); 62 | for (ActivityInfo activityInfo : adapter.activityInfos) { 63 | if (activityInfo.iconLoadTask != null) { 64 | activityInfo.iconLoadTask.cancel(true); 65 | activityInfo.iconLoadTask = null; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Represents an item in the picker grid 72 | */ 73 | public static class ActivityInfo { 74 | public Drawable icon; 75 | public final String label; 76 | public final ComponentName componentName; 77 | public final ResolveInfo resolveInfo; 78 | private AsyncTask iconLoadTask; 79 | public Object tag; 80 | 81 | public ActivityInfo(Drawable icon, String label, Context context, Class clazz) { 82 | this.icon = icon; 83 | resolveInfo = null; 84 | this.label = label; 85 | this.componentName = new ComponentName(context, clazz.getName()); 86 | } 87 | 88 | ActivityInfo(ResolveInfo resolveInfo, CharSequence label, ComponentName componentName) { 89 | this.resolveInfo = resolveInfo; 90 | this.label = label.toString(); 91 | this.componentName = componentName; 92 | } 93 | 94 | public Intent getConcreteIntent(Intent intent) { 95 | Intent concreteIntent = new Intent(intent); 96 | concreteIntent.setComponent(componentName); 97 | return concreteIntent; 98 | } 99 | } 100 | 101 | protected final Intent intent; 102 | protected final GridView appGrid; 103 | protected final TextView titleView; 104 | protected final List mixins = new ArrayList<>(); 105 | 106 | protected Adapter adapter; 107 | protected Filter filter = new FilterNone(); 108 | protected Comparator sortMethod = new SortAlphabetically(); 109 | 110 | public IntentPickerSheetView(Context context, Intent intent, @StringRes int titleRes, OnIntentPickedListener listener) { 111 | this(context, intent, context.getString(titleRes), listener); 112 | } 113 | 114 | public IntentPickerSheetView(Context context, final Intent intent, final String title, final OnIntentPickedListener listener) { 115 | super(context); 116 | this.intent = intent; 117 | 118 | inflate(context, R.layout.grid_sheet_view, this); 119 | appGrid = (GridView) findViewById(R.id.grid); 120 | titleView = (TextView) findViewById(R.id.title); 121 | 122 | titleView.setText(title); 123 | appGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() { 124 | @Override 125 | public void onItemClick(AdapterView parent, View view, int position, long id) { 126 | listener.onIntentPicked(adapter.getItem(position)); 127 | } 128 | }); 129 | 130 | ViewCompat.setElevation(this, Util.dp2px(getContext(), 16f)); 131 | } 132 | 133 | public void setSortMethod(Comparator sortMethod) { 134 | this.sortMethod = sortMethod; 135 | } 136 | 137 | public void setFilter(Filter filter) { 138 | this.filter = filter; 139 | } 140 | 141 | public void setColumnWidthDp(int columnWidthDp) { 142 | this.columnWidthDp = columnWidthDp; 143 | } 144 | 145 | /** 146 | * Adds custom mixins to the resulting picker sheet 147 | * 148 | * @param infos Custom ActivityInfo classes to mix in 149 | */ 150 | public void setMixins(@NonNull List infos) { 151 | mixins.clear(); 152 | mixins.addAll(infos); 153 | } 154 | 155 | public List getMixins() { 156 | return this.mixins; 157 | } 158 | 159 | @Override 160 | protected void onAttachedToWindow() { 161 | super.onAttachedToWindow(); 162 | this.adapter = new Adapter(getContext(), intent, mixins); 163 | appGrid.setAdapter(this.adapter); 164 | } 165 | 166 | @Override 167 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 168 | int width = MeasureSpec.getSize(widthMeasureSpec); 169 | final float density = getResources().getDisplayMetrics().density; 170 | getResources().getDimensionPixelSize(R.dimen.bottomsheet_default_sheet_width); 171 | appGrid.setNumColumns((int) (width / (columnWidthDp * density))); 172 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 173 | } 174 | 175 | @Override 176 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 177 | // Necessary for showing elevation on 5.0+ 178 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 179 | setOutlineProvider(new Util.ShadowOutline(w, h)); 180 | } 181 | } 182 | 183 | private class Adapter extends BaseAdapter { 184 | 185 | final List activityInfos; 186 | final LayoutInflater inflater; 187 | private PackageManager packageManager; 188 | 189 | public Adapter(Context context, Intent intent, List mixins) { 190 | inflater = LayoutInflater.from(context); 191 | packageManager = context.getPackageManager(); 192 | List infos = packageManager.queryIntentActivities(intent, 0); 193 | activityInfos = new ArrayList<>(infos.size() + mixins.size()); 194 | activityInfos.addAll(mixins); 195 | for (ResolveInfo info : infos) { 196 | ComponentName componentName = new ComponentName(info.activityInfo.packageName, info.activityInfo.name); 197 | ActivityInfo activityInfo = new ActivityInfo(info, info.loadLabel(packageManager), componentName); 198 | if (filter.include(activityInfo)) { 199 | activityInfos.add(activityInfo); 200 | } 201 | } 202 | Collections.sort(activityInfos, sortMethod); 203 | } 204 | 205 | @Override 206 | public int getCount() { 207 | return activityInfos.size(); 208 | } 209 | 210 | @Override 211 | public ActivityInfo getItem(int position) { 212 | return activityInfos.get(position); 213 | } 214 | 215 | @Override 216 | public long getItemId(int position) { 217 | return activityInfos.get(position).componentName.hashCode(); 218 | } 219 | 220 | @Override 221 | public View getView(int position, View convertView, ViewGroup parent) { 222 | final ViewHolder holder; 223 | 224 | if (convertView == null) { 225 | convertView = inflater.inflate(R.layout.sheet_grid_item, parent, false); 226 | holder = new ViewHolder(convertView); 227 | convertView.setTag(holder); 228 | } else { 229 | holder = (ViewHolder) convertView.getTag(); 230 | } 231 | 232 | final ActivityInfo info = activityInfos.get(position); 233 | if (info.iconLoadTask != null) { 234 | info.iconLoadTask.cancel(true); 235 | info .iconLoadTask = null; 236 | } 237 | if (info.icon != null) { 238 | holder.icon.setImageDrawable(info.icon); 239 | } else { 240 | holder.icon.setImageDrawable(getResources().getDrawable(R.color.divider_gray)); 241 | info.iconLoadTask = new AsyncTask() { 242 | @Override 243 | protected Drawable doInBackground(@NonNull Void... params) { 244 | return info.resolveInfo.loadIcon(packageManager); 245 | } 246 | 247 | @Override 248 | protected void onPostExecute(@NonNull Drawable drawable) { 249 | info.icon = drawable; 250 | info.iconLoadTask = null; 251 | holder.icon.setImageDrawable(drawable); 252 | } 253 | }; 254 | info.iconLoadTask.execute(); 255 | } 256 | holder.label.setText(info.label); 257 | 258 | return convertView; 259 | } 260 | 261 | class ViewHolder { 262 | final ImageView icon; 263 | final TextView label; 264 | 265 | ViewHolder(View root) { 266 | icon = (ImageView) root.findViewById(R.id.icon); 267 | label = (TextView) root.findViewById(R.id.label); 268 | } 269 | } 270 | 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/MenuSheetView.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.MenuRes; 8 | import android.support.annotation.Nullable; 9 | import android.support.annotation.StringRes; 10 | import android.support.v4.view.ViewCompat; 11 | import android.support.v7.view.SupportMenuInflater; 12 | import android.support.v7.view.menu.MenuBuilder; 13 | import android.text.TextUtils; 14 | import android.view.LayoutInflater; 15 | import android.view.Menu; 16 | import android.view.MenuItem; 17 | import android.view.SubMenu; 18 | import android.view.View; 19 | import android.view.ViewGroup; 20 | import android.widget.AbsListView; 21 | import android.widget.AdapterView; 22 | import android.widget.BaseAdapter; 23 | import android.widget.FrameLayout; 24 | import android.widget.GridView; 25 | import android.widget.ImageView; 26 | import android.widget.TextView; 27 | 28 | import java.util.ArrayList; 29 | 30 | import flipboard.bottomsheet.commons.R; 31 | 32 | import static com.flipboard.bottomsheet.commons.MenuSheetView.MenuType.GRID; 33 | import static com.flipboard.bottomsheet.commons.MenuSheetView.MenuType.LIST; 34 | 35 | /** 36 | * A SheetView that can represent a menu resource as a list or grid. 37 | *

38 | * A list can support submenus, and will include a divider and header for them where appropriate. 39 | * Grids currently don't support submenus, and don't in the Material Design spec either. 40 | *

41 | */ 42 | @SuppressLint("ViewConstructor") 43 | public class MenuSheetView extends FrameLayout { 44 | 45 | public static final int DEFAULT_LAYOUT_LIST_ITEM = R.layout.sheet_list_item; 46 | public static final int DEFAULT_LAYOUT_GRID_ITEM = R.layout.sheet_grid_item; 47 | 48 | /** 49 | * A listener for menu item clicks in the sheet 50 | */ 51 | public interface OnMenuItemClickListener { 52 | boolean onMenuItemClick(MenuItem item); 53 | } 54 | 55 | /** 56 | * The supported display types for the menu items. 57 | */ 58 | public enum MenuType {LIST, GRID} 59 | 60 | private Menu menu; 61 | private final MenuType menuType; 62 | private ArrayList items = new ArrayList<>(); 63 | private Adapter adapter; 64 | private AbsListView absListView; 65 | private final TextView titleView; 66 | protected final int originalListPaddingTop; 67 | private int columnWidthDp = 100; 68 | private int listItemLayoutRes = DEFAULT_LAYOUT_LIST_ITEM; 69 | private int gridItemLayoutRes = DEFAULT_LAYOUT_GRID_ITEM; 70 | 71 | /** 72 | * @param context Context to construct the view with 73 | * @param menuType LIST or GRID 74 | * @param titleRes String resource ID for the title 75 | * @param listener Listener for menu item clicks in the sheet 76 | */ 77 | public MenuSheetView(final Context context, final MenuType menuType, @StringRes final int titleRes, final OnMenuItemClickListener listener) { 78 | this(context, menuType, context.getString(titleRes), listener); 79 | } 80 | 81 | /** 82 | * @param context Context to construct the view with 83 | * @param menuType LIST or GRID 84 | * @param title Title for the sheet. Can be null 85 | * @param listener Listener for menu item clicks in the sheet 86 | */ 87 | public MenuSheetView(final Context context, final MenuType menuType, @Nullable final CharSequence title, final OnMenuItemClickListener listener) { 88 | super(context); 89 | 90 | // Set up the menu 91 | this.menu = new MenuBuilder(context); 92 | this.menuType = menuType; 93 | 94 | // Inflate the appropriate view and set up the absListView 95 | inflate(context, menuType == GRID ? R.layout.grid_sheet_view : R.layout.list_sheet_view, this); 96 | absListView = (AbsListView) findViewById(menuType == GRID ? R.id.grid : R.id.list); 97 | if (listener != null) { 98 | absListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 99 | @Override 100 | public void onItemClick(AdapterView parent, View view, int position, long id) { 101 | listener.onMenuItemClick(adapter.getItem(position).getMenuItem()); 102 | } 103 | }); 104 | } 105 | 106 | // Set up the title 107 | titleView = (TextView) findViewById(R.id.title); 108 | originalListPaddingTop = absListView.getPaddingTop(); 109 | setTitle(title); 110 | 111 | ViewCompat.setElevation(this, Util.dp2px(getContext(), 16f)); 112 | } 113 | 114 | /** 115 | * Inflates a menu resource into the menu backing this sheet. 116 | * 117 | * @param menuRes Menu resource ID 118 | */ 119 | public void inflateMenu(@MenuRes int menuRes) { 120 | if (menuRes != -1) { 121 | SupportMenuInflater inflater = new SupportMenuInflater(getContext()); 122 | inflater.inflate(menuRes, menu); 123 | } 124 | 125 | prepareMenuItems(); 126 | } 127 | 128 | @Override 129 | protected void onAttachedToWindow() { 130 | super.onAttachedToWindow(); 131 | this.adapter = new Adapter(); 132 | absListView.setAdapter(this.adapter); 133 | } 134 | 135 | @Override 136 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 137 | if (menuType == GRID) { 138 | int width = MeasureSpec.getSize(widthMeasureSpec); 139 | final float density = getResources().getDisplayMetrics().density; 140 | ((GridView) absListView).setNumColumns((int) (width / (columnWidthDp * density))); 141 | } 142 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 143 | } 144 | 145 | @Override 146 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 147 | // Necessary for showing elevation on 5.0+ 148 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 149 | setOutlineProvider(new Util.ShadowOutline(w, h)); 150 | } 151 | } 152 | 153 | /** 154 | * Flattens the visible menu items of {@link #menu} into {@link #items}, 155 | * while inserting separators between items when necessary. 156 | * 157 | * Adapted from the Design support library's NavigationMenuPresenter implementation 158 | */ 159 | private void prepareMenuItems() { 160 | items.clear(); 161 | int currentGroupId = 0; 162 | 163 | // Iterate over the menu items 164 | for (int i = 0; i < menu.size(); i++) { 165 | MenuItem item = menu.getItem(i); 166 | if (item.isVisible()) { 167 | if (item.hasSubMenu()) { 168 | // Flatten the submenu 169 | SubMenu subMenu = item.getSubMenu(); 170 | if (subMenu.hasVisibleItems()) { 171 | if (menuType == LIST) { 172 | items.add(SheetMenuItem.SEPARATOR); 173 | 174 | // Add a header item if it has text 175 | if (!TextUtils.isEmpty(item.getTitle())) { 176 | items.add(SheetMenuItem.of(item)); 177 | } 178 | } 179 | 180 | // Add the sub-items 181 | for (int subI = 0, size = subMenu.size(); subI < size; subI++) { 182 | MenuItem subMenuItem = subMenu.getItem(subI); 183 | if (subMenuItem.isVisible()) { 184 | items.add(SheetMenuItem.of(subMenuItem)); 185 | } 186 | } 187 | 188 | // Add one more separator to the end to close it off if we have more items 189 | if (menuType == LIST && i != menu.size() - 1) { 190 | items.add(SheetMenuItem.SEPARATOR); 191 | } 192 | } 193 | } else { 194 | int groupId = item.getGroupId(); 195 | if (groupId != currentGroupId && menuType == LIST) { 196 | items.add(SheetMenuItem.SEPARATOR); 197 | } 198 | items.add(SheetMenuItem.of(item)); 199 | currentGroupId = groupId; 200 | } 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * @return The current {@link Menu} instance backing this sheet. Note that this is mutable, and 207 | * you should call {@link #updateMenu()} after any changes. 208 | */ 209 | public Menu getMenu() { 210 | return this.menu; 211 | } 212 | 213 | /** 214 | * Invalidates the current internal representation of the menu and rebuilds it. Should be used 215 | * if the developer dynamically adds items to the Menu returned by {@link #getMenu()} 216 | */ 217 | public void updateMenu() { 218 | // Invalidate menu and rebuild it, useful if the user has dynamically added menu items. 219 | prepareMenuItems(); 220 | } 221 | 222 | /** 223 | * @return The {@link MenuType} this instance was built with 224 | */ 225 | public MenuType getMenuType() { 226 | return this.menuType; 227 | } 228 | 229 | /** 230 | * Sets the title text of the sheet 231 | * 232 | * @param resId String resource ID for the text 233 | */ 234 | public void setTitle(@StringRes int resId) { 235 | setTitle(getResources().getText(resId)); 236 | } 237 | 238 | /** 239 | * Sets the title text of the sheet 240 | * 241 | * @param title Title text to use 242 | */ 243 | public void setTitle(CharSequence title) { 244 | if (!TextUtils.isEmpty(title)) { 245 | titleView.setText(title); 246 | } else { 247 | titleView.setVisibility(GONE); 248 | 249 | // Add some padding to the top to account for the missing title 250 | absListView.setPadding(absListView.getPaddingLeft(), originalListPaddingTop + Util.dp2px(getContext(), 8f), absListView.getPaddingRight(), absListView.getPaddingBottom()); 251 | } 252 | } 253 | 254 | /** 255 | * Only applies to GRID 256 | */ 257 | public void setColumnWidthDp(int columnWidthDp) { 258 | this.columnWidthDp = columnWidthDp; 259 | } 260 | 261 | /** 262 | * Override the layout for displaying a list item when of type {@link MenuType#LIST}.
263 | * Call this before you attach to window using {@link com.flipboard.bottomsheet.BottomSheetLayout#showWithSheetView}. 264 | * @param listItemLayoutRes needs to have an {@link ImageView} with id set to {@link R.id#icon} and {@link TextView} with id set to {@link R.id#label} 265 | */ 266 | public void setListItemLayoutRes(@LayoutRes int listItemLayoutRes) { 267 | this.listItemLayoutRes = listItemLayoutRes; 268 | } 269 | 270 | /** 271 | * Override the layout for displaying a grid item when of type {@link MenuType#GRID}.
272 | * Call this before you attach to window using {@link com.flipboard.bottomsheet.BottomSheetLayout#showWithSheetView}. 273 | * @param gridItemLayoutRes needs to have an {@link ImageView} with id set to {@link R.id#icon} and {@link TextView} with id set to {@link R.id#label} 274 | */ 275 | public void setGridItemLayoutRes(@LayoutRes int gridItemLayoutRes) { 276 | this.gridItemLayoutRes = gridItemLayoutRes; 277 | } 278 | 279 | 280 | /** 281 | * @return The current title text of the sheet 282 | */ 283 | public CharSequence getTitle() { 284 | return titleView.getText(); 285 | } 286 | 287 | private class Adapter extends BaseAdapter { 288 | 289 | private static final int VIEW_TYPE_NORMAL = 0; 290 | private static final int VIEW_TYPE_SUBHEADER = 1; 291 | private static final int VIEW_TYPE_SEPARATOR = 2; 292 | 293 | private final LayoutInflater inflater; 294 | 295 | public Adapter() { 296 | this.inflater = LayoutInflater.from(getContext()); 297 | } 298 | 299 | @Override 300 | public int getCount() { 301 | return items.size(); 302 | } 303 | 304 | @Override 305 | public SheetMenuItem getItem(int position) { 306 | return items.get(position); 307 | } 308 | 309 | @Override 310 | public long getItemId(int position) { 311 | return position; 312 | } 313 | 314 | @Override 315 | public int getViewTypeCount() { 316 | return 3; 317 | } 318 | 319 | @Override 320 | public int getItemViewType(int position) { 321 | SheetMenuItem item = getItem(position); 322 | if (item.isSeparator()) { 323 | return VIEW_TYPE_SEPARATOR; 324 | } else if (item.getMenuItem().hasSubMenu()) { 325 | return VIEW_TYPE_SUBHEADER; 326 | } else { 327 | return VIEW_TYPE_NORMAL; 328 | } 329 | } 330 | 331 | @Override 332 | public View getView(int position, View convertView, ViewGroup parent) { 333 | 334 | SheetMenuItem item = getItem(position); 335 | int viewType = getItemViewType(position); 336 | 337 | switch (viewType) { 338 | case VIEW_TYPE_NORMAL: 339 | NormalViewHolder holder; 340 | if (convertView == null) { 341 | convertView = inflater.inflate(menuType == GRID ? gridItemLayoutRes : listItemLayoutRes, parent, false); 342 | holder = new NormalViewHolder(convertView); 343 | convertView.setTag(holder); 344 | } else { 345 | holder = (NormalViewHolder) convertView.getTag(); 346 | } 347 | holder.bindView(item); 348 | break; 349 | case VIEW_TYPE_SUBHEADER: 350 | if (convertView == null) { 351 | convertView = inflater.inflate(R.layout.sheet_list_item_subheader, parent, false); 352 | } 353 | TextView subHeader = (TextView) convertView; 354 | subHeader.setText(item.getMenuItem().getTitle()); 355 | break; 356 | case VIEW_TYPE_SEPARATOR: 357 | if (convertView == null) { 358 | convertView = inflater.inflate(R.layout.sheet_list_item_separator, parent, false); 359 | } 360 | break; 361 | } 362 | 363 | return convertView; 364 | } 365 | 366 | @Override 367 | public boolean areAllItemsEnabled() { 368 | return false; 369 | } 370 | 371 | @Override 372 | public boolean isEnabled(int position) { 373 | return getItem(position).isEnabled(); 374 | } 375 | 376 | class NormalViewHolder { 377 | final ImageView icon; 378 | final TextView label; 379 | 380 | NormalViewHolder(View root) { 381 | icon = (ImageView) root.findViewById(R.id.icon); 382 | label = (TextView) root.findViewById(R.id.label); 383 | } 384 | 385 | public void bindView(SheetMenuItem item) { 386 | icon.setImageDrawable(item.getMenuItem().getIcon()); 387 | label.setText(item.getMenuItem().getTitle()); 388 | } 389 | } 390 | } 391 | 392 | private static class SheetMenuItem { 393 | 394 | private static final SheetMenuItem SEPARATOR = new SheetMenuItem(null); 395 | 396 | private final MenuItem menuItem; 397 | 398 | private SheetMenuItem(MenuItem item) { 399 | menuItem = item; 400 | } 401 | 402 | public static SheetMenuItem of(MenuItem item) { 403 | return new SheetMenuItem(item); 404 | } 405 | 406 | public boolean isSeparator() { 407 | return this == SEPARATOR; 408 | } 409 | 410 | public MenuItem getMenuItem() { 411 | return menuItem; 412 | } 413 | 414 | public boolean isEnabled() { 415 | // Separators and subheaders never respond to click 416 | return menuItem != null && !menuItem.hasSubMenu() && menuItem.isEnabled(); 417 | } 418 | 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/java/com/flipboard/bottomsheet/commons/Util.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.commons; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Outline; 6 | import android.util.TypedValue; 7 | import android.view.View; 8 | import android.view.ViewOutlineProvider; 9 | 10 | class Util { 11 | 12 | /** 13 | * Convert a dp float value to pixels 14 | * 15 | * @param dp float value in dps to convert 16 | * @return DP value converted to pixels 17 | */ 18 | static int dp2px(Context context, float dp) { 19 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); 20 | return Math.round(px); 21 | } 22 | 23 | /** 24 | * A helper class for providing a shadow on sheets 25 | */ 26 | @TargetApi(21) 27 | static class ShadowOutline extends ViewOutlineProvider { 28 | 29 | int width; 30 | int height; 31 | 32 | ShadowOutline(int width, int height) { 33 | this.width = width; 34 | this.height = height; 35 | } 36 | 37 | @Override 38 | public void getOutline(View view, Outline outline) { 39 | outline.setRect(0, 0, width, height); 40 | } 41 | } 42 | 43 | private Util() { 44 | throw new AssertionError("No Instances"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-hdpi/bottomsheet_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-hdpi/bottomsheet_camera.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-hdpi/bottomsheet_collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-hdpi/bottomsheet_collections.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xhdpi/bottomsheet_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xhdpi/bottomsheet_camera.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xhdpi/bottomsheet_collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xhdpi/bottomsheet_collections.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xxhdpi/bottomsheet_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xxhdpi/bottomsheet_camera.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xxhdpi/bottomsheet_collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xxhdpi/bottomsheet_collections.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xxxhdpi/bottomsheet_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xxxhdpi/bottomsheet_camera.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/drawable-xxxhdpi/bottomsheet_collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-commons/src/main/res/drawable-xxxhdpi/bottomsheet_collections.png -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/grid_sheet_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 30 | 31 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/list_sheet_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 33 | 34 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/sheet_grid_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/sheet_image_grid_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/sheet_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 34 | 35 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/sheet_list_item_separator.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/layout/sheet_list_item_subheader.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #8C000000 4 | #AAe2e2e2 5 | -------------------------------------------------------------------------------- /bottomsheet-commons/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 2dp 5 | 6 | -------------------------------------------------------------------------------- /bottomsheet-sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '23.0.2' 6 | 7 | defaultConfig { 8 | applicationId "com.flipboard.bottomsheet.sample" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_7 17 | targetCompatibility JavaVersion.VERSION_1_7 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | lintOptions { 27 | abortOnError false 28 | textReport true 29 | textOutput 'stdout' 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(dir: 'libs', include: ['*.jar']) 35 | compile project(':bottomsheet') 36 | compile project(':bottomsheet-commons') 37 | compile 'com.android.support:appcompat-v7:23.2.0' 38 | compile 'com.github.bumptech.glide:glide:3.6.1' 39 | } 40 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 31 | 32 | 36 | 39 | 40 | 44 | 47 | 48 | 51 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/BottomSheetFragmentActivity.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | 7 | import com.flipboard.bottomsheet.BottomSheetLayout; 8 | import com.flipboard.bottomsheet.R; 9 | import com.flipboard.bottomsheet.commons.ImagePickerSheetView; 10 | 11 | /** 12 | * Activity demonstrating the use of {@link ImagePickerSheetView} 13 | */ 14 | public final class BottomSheetFragmentActivity extends AppCompatActivity { 15 | 16 | protected BottomSheetLayout bottomSheetLayout; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_bottom_sheet_fragment); 22 | bottomSheetLayout = (BottomSheetLayout) findViewById(R.id.bottomsheet); 23 | findViewById(R.id.bottomsheet_fragment_button).setOnClickListener(new View.OnClickListener() { 24 | @Override 25 | public void onClick(View v) { 26 | new MyFragment().show(getSupportFragmentManager(), R.id.bottomsheet); 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/ImagePickerActivity.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.os.Environment; 12 | import android.provider.MediaStore; 13 | import android.support.annotation.NonNull; 14 | import android.support.annotation.Nullable; 15 | import android.support.v4.app.ActivityCompat; 16 | import android.support.v7.app.AppCompatActivity; 17 | import android.view.View; 18 | import android.widget.ImageView; 19 | import android.widget.Toast; 20 | 21 | import com.bumptech.glide.Glide; 22 | import com.flipboard.bottomsheet.BottomSheetLayout; 23 | import com.flipboard.bottomsheet.R; 24 | import com.flipboard.bottomsheet.commons.ImagePickerSheetView; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.text.SimpleDateFormat; 29 | import java.util.Date; 30 | import java.util.Locale; 31 | 32 | /** 33 | * Activity demonstrating the use of {@link ImagePickerSheetView} 34 | */ 35 | public final class ImagePickerActivity extends AppCompatActivity { 36 | 37 | private static final int REQUEST_STORAGE = 0; 38 | private static final int REQUEST_IMAGE_CAPTURE = REQUEST_STORAGE + 1; 39 | private static final int REQUEST_LOAD_IMAGE = REQUEST_IMAGE_CAPTURE + 1; 40 | protected BottomSheetLayout bottomSheetLayout; 41 | private Uri cameraImageUri = null; 42 | private ImageView selectedImage; 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | setContentView(R.layout.activity_image_picker); 48 | bottomSheetLayout = (BottomSheetLayout) findViewById(R.id.bottomsheet); 49 | bottomSheetLayout.setPeekOnDismiss(true); 50 | selectedImage = (ImageView) findViewById(R.id.image_picker_selected); 51 | findViewById(R.id.image_picker_button).setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | if (checkNeedsPermission()) { 55 | requestStoragePermission(); 56 | } else { 57 | showSheetView(); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | private boolean checkNeedsPermission() { 64 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ActivityCompat.checkSelfPermission(ImagePickerActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; 65 | } 66 | 67 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 68 | private void requestStoragePermission() { 69 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 70 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_STORAGE); 71 | } else { 72 | // Eh, prompt anyway 73 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_STORAGE); 74 | } 75 | } 76 | 77 | @TargetApi(Build.VERSION_CODES.M) 78 | @Override 79 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 80 | if (requestCode == REQUEST_STORAGE) { 81 | if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 82 | showSheetView(); 83 | } else { 84 | // Permission denied 85 | Toast.makeText(this, "Sheet is useless without access to external storage :/", Toast.LENGTH_SHORT).show(); 86 | } 87 | } else { 88 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 89 | } 90 | } 91 | 92 | /** 93 | * Show an {@link ImagePickerSheetView} 94 | */ 95 | private void showSheetView() { 96 | ImagePickerSheetView sheetView = new ImagePickerSheetView.Builder(this) 97 | .setMaxItems(30) 98 | .setShowCameraOption(createCameraIntent() != null) 99 | .setShowPickerOption(createPickIntent() != null) 100 | .setImageProvider(new ImagePickerSheetView.ImageProvider() { 101 | @Override 102 | public void onProvideImage(ImageView imageView, Uri imageUri, int size) { 103 | Glide.with(ImagePickerActivity.this) 104 | .load(imageUri) 105 | .centerCrop() 106 | .crossFade() 107 | .into(imageView); 108 | } 109 | }) 110 | .setOnTileSelectedListener(new ImagePickerSheetView.OnTileSelectedListener() { 111 | @Override 112 | public void onTileSelected(ImagePickerSheetView.ImagePickerTile selectedTile) { 113 | bottomSheetLayout.dismissSheet(); 114 | if (selectedTile.isCameraTile()) { 115 | dispatchTakePictureIntent(); 116 | } else if (selectedTile.isPickerTile()) { 117 | startActivityForResult(createPickIntent(), REQUEST_LOAD_IMAGE); 118 | } else if (selectedTile.isImageTile()) { 119 | showSelectedImage(selectedTile.getImageUri()); 120 | } else { 121 | genericError(); 122 | } 123 | } 124 | }) 125 | .setTitle("Choose an image...") 126 | .create(); 127 | 128 | bottomSheetLayout.showWithSheetView(sheetView); 129 | } 130 | 131 | /** 132 | * For images captured from the camera, we need to create a File first to tell the camera 133 | * where to store the image. 134 | * 135 | * @return the File created for the image to be store under. 136 | */ 137 | private File createImageFile() throws IOException { 138 | // Create an image file name 139 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); 140 | String imageFileName = "JPEG_" + timeStamp + "_"; 141 | File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); 142 | File imageFile = File.createTempFile( 143 | imageFileName, /* prefix */ 144 | ".jpg", /* suffix */ 145 | storageDir /* directory */ 146 | ); 147 | 148 | // Save a file: path for use with ACTION_VIEW intents 149 | cameraImageUri = Uri.fromFile(imageFile); 150 | return imageFile; 151 | } 152 | 153 | /** 154 | * This checks to see if there is a suitable activity to handle the `ACTION_PICK` intent 155 | * and returns it if found. {@link Intent#ACTION_PICK} is for picking an image from an external app. 156 | * 157 | * @return A prepared intent if found. 158 | */ 159 | @Nullable 160 | private Intent createPickIntent() { 161 | Intent picImageIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 162 | if (picImageIntent.resolveActivity(getPackageManager()) != null) { 163 | return picImageIntent; 164 | } else { 165 | return null; 166 | } 167 | } 168 | 169 | /** 170 | * This checks to see if there is a suitable activity to handle the {@link MediaStore#ACTION_IMAGE_CAPTURE} 171 | * intent and returns it if found. {@link MediaStore#ACTION_IMAGE_CAPTURE} is for letting another app take 172 | * a picture from the camera and store it in a file that we specify. 173 | * 174 | * @return A prepared intent if found. 175 | */ 176 | @Nullable 177 | private Intent createCameraIntent() { 178 | Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 179 | if (takePictureIntent.resolveActivity(getPackageManager()) != null) { 180 | return takePictureIntent; 181 | } else { 182 | return null; 183 | } 184 | } 185 | 186 | /** 187 | * This utility function combines the camera intent creation and image file creation, and 188 | * ultimately fires the intent. 189 | * 190 | * @see {@link #createCameraIntent()} 191 | * @see {@link #createImageFile()} 192 | */ 193 | private void dispatchTakePictureIntent() { 194 | Intent takePictureIntent = createCameraIntent(); 195 | // Ensure that there's a camera activity to handle the intent 196 | if (takePictureIntent != null) { 197 | // Create the File where the photo should go 198 | try { 199 | File imageFile = createImageFile(); 200 | takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(imageFile)); 201 | startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); 202 | } catch (IOException e) { 203 | // Error occurred while creating the File 204 | genericError("Could not create imageFile for camera"); 205 | } 206 | } 207 | } 208 | 209 | @Override 210 | public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 211 | super.onActivityResult(requestCode, resultCode, data); 212 | if (resultCode == Activity.RESULT_OK) { 213 | Uri selectedImage = null; 214 | if (requestCode == REQUEST_LOAD_IMAGE && data != null) { 215 | selectedImage = data.getData(); 216 | if (selectedImage == null) { 217 | genericError(); 218 | } 219 | } else if (requestCode == REQUEST_IMAGE_CAPTURE) { 220 | // Do something with imagePath 221 | selectedImage = cameraImageUri; 222 | } 223 | 224 | if (selectedImage != null) { 225 | showSelectedImage(selectedImage); 226 | } else { 227 | genericError(); 228 | } 229 | } 230 | } 231 | 232 | private void showSelectedImage(Uri selectedImageUri) { 233 | selectedImage.setImageDrawable(null); 234 | Glide.with(this) 235 | .load(selectedImageUri) 236 | .crossFade() 237 | .fitCenter() 238 | .into(selectedImage); 239 | } 240 | 241 | private void genericError() { 242 | genericError(null); 243 | } 244 | 245 | private void genericError(String message) { 246 | Toast.makeText(this, message == null ? "Something went wrong." : message, Toast.LENGTH_SHORT).show(); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.flipboard.bottomsheet.R; 9 | 10 | 11 | public class MainActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | 18 | findViewById(R.id.picker_button).setOnClickListener(new View.OnClickListener() { 19 | @Override 20 | public void onClick(View v) { 21 | startActivity(new Intent(MainActivity.this, PickerActivity.class)); 22 | } 23 | }); 24 | 25 | findViewById(R.id.menu_button).setOnClickListener(new View.OnClickListener() { 26 | @Override 27 | public void onClick(View v) { 28 | startActivity(new Intent(MainActivity.this, MenuActivity.class)); 29 | } 30 | }); 31 | 32 | findViewById(R.id.image_picker_button).setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | startActivity(new Intent(MainActivity.this, ImagePickerActivity.class)); 36 | } 37 | }); 38 | 39 | findViewById(R.id.bottomsheet_fragment_button).setOnClickListener(new View.OnClickListener() { 40 | @Override 41 | public void onClick(View v) { 42 | startActivity(new Intent(MainActivity.this, BottomSheetFragmentActivity.class)); 43 | } 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/MenuActivity.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.MenuItem; 6 | import android.view.View; 7 | import android.widget.Toast; 8 | 9 | import com.flipboard.bottomsheet.BottomSheetLayout; 10 | import com.flipboard.bottomsheet.R; 11 | import com.flipboard.bottomsheet.commons.MenuSheetView; 12 | 13 | /** 14 | * Activity demonstrating the use of {@link MenuSheetView} 15 | */ 16 | public class MenuActivity extends AppCompatActivity { 17 | 18 | protected BottomSheetLayout bottomSheetLayout; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_menu); 24 | bottomSheetLayout = (BottomSheetLayout) findViewById(R.id.bottomsheet); 25 | bottomSheetLayout.setPeekOnDismiss(true); 26 | findViewById(R.id.list_button).setOnClickListener(new View.OnClickListener() { 27 | @Override 28 | public void onClick(View v) { 29 | showMenuSheet(MenuSheetView.MenuType.LIST); 30 | } 31 | }); 32 | findViewById(R.id.grid_button).setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | showMenuSheet(MenuSheetView.MenuType.GRID); 36 | } 37 | }); 38 | } 39 | 40 | private void showMenuSheet(final MenuSheetView.MenuType menuType) { 41 | MenuSheetView menuSheetView = 42 | new MenuSheetView(MenuActivity.this, menuType, "Create...", new MenuSheetView.OnMenuItemClickListener() { 43 | @Override 44 | public boolean onMenuItemClick(MenuItem item) { 45 | Toast.makeText(MenuActivity.this, item.getTitle(), Toast.LENGTH_SHORT).show(); 46 | if (bottomSheetLayout.isSheetShowing()) { 47 | bottomSheetLayout.dismissSheet(); 48 | } 49 | if (item.getItemId() == R.id.reopen) { 50 | showMenuSheet(menuType == MenuSheetView.MenuType.LIST ? MenuSheetView.MenuType.GRID : MenuSheetView.MenuType.LIST); 51 | } 52 | return true; 53 | } 54 | }); 55 | menuSheetView.inflateMenu(R.menu.create); 56 | bottomSheetLayout.showWithSheetView(menuSheetView); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/MyFragment.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.flipboard.bottomsheet.R; 10 | import com.flipboard.bottomsheet.commons.BottomSheetFragment; 11 | 12 | public class MyFragment extends BottomSheetFragment { 13 | 14 | @Nullable 15 | @Override 16 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 17 | return inflater.inflate(R.layout.fragment_my, container, false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/java/com/flipboard/bottomsheet/sample/PickerActivity.java: -------------------------------------------------------------------------------- 1 | package com.flipboard.bottomsheet.sample; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.drawable.Drawable; 6 | import android.os.Bundle; 7 | import android.support.v4.content.res.ResourcesCompat; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.View; 10 | import android.view.inputmethod.InputMethodManager; 11 | import android.widget.Button; 12 | import android.widget.TextView; 13 | 14 | import com.flipboard.bottomsheet.BottomSheetLayout; 15 | import com.flipboard.bottomsheet.R; 16 | import com.flipboard.bottomsheet.commons.IntentPickerSheetView; 17 | 18 | import java.util.Collections; 19 | import java.util.Comparator; 20 | 21 | /** 22 | * Activity demonstrating the use of {@link IntentPickerSheetView} 23 | */ 24 | public class PickerActivity extends AppCompatActivity { 25 | 26 | protected BottomSheetLayout bottomSheetLayout; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_picker); 32 | bottomSheetLayout = (BottomSheetLayout) findViewById(R.id.bottomsheet); 33 | final TextView shareText = (TextView) findViewById(R.id.share_text); 34 | final Button shareButton = (Button) findViewById(R.id.share_button); 35 | 36 | shareButton.setOnClickListener(new View.OnClickListener() { 37 | @Override 38 | public void onClick(View v) { 39 | // Hide the keyboard 40 | InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 41 | imm.hideSoftInputFromWindow(shareText.getWindowToken(), 0); 42 | 43 | final Intent shareIntent = new Intent(Intent.ACTION_SEND); 44 | shareIntent.putExtra(Intent.EXTRA_TEXT, shareText.getText().toString()); 45 | shareIntent.setType("text/plain"); 46 | IntentPickerSheetView intentPickerSheet = new IntentPickerSheetView(PickerActivity.this, shareIntent, "Share with...", new IntentPickerSheetView.OnIntentPickedListener() { 47 | @Override 48 | public void onIntentPicked(IntentPickerSheetView.ActivityInfo activityInfo) { 49 | bottomSheetLayout.dismissSheet(); 50 | startActivity(activityInfo.getConcreteIntent(shareIntent)); 51 | } 52 | }); 53 | // Filter out built in sharing options such as bluetooth and beam. 54 | intentPickerSheet.setFilter(new IntentPickerSheetView.Filter() { 55 | @Override 56 | public boolean include(IntentPickerSheetView.ActivityInfo info) { 57 | return !info.componentName.getPackageName().startsWith("com.android"); 58 | } 59 | }); 60 | // Sort activities in reverse order for no good reason 61 | intentPickerSheet.setSortMethod(new Comparator() { 62 | @Override 63 | public int compare(IntentPickerSheetView.ActivityInfo lhs, IntentPickerSheetView.ActivityInfo rhs) { 64 | return rhs.label.compareTo(lhs.label); 65 | } 66 | }); 67 | 68 | // Add custom mixin example 69 | Drawable customDrawable = ResourcesCompat.getDrawable(getResources(), R.mipmap.ic_launcher, null); 70 | IntentPickerSheetView.ActivityInfo customInfo = new IntentPickerSheetView.ActivityInfo(customDrawable, "Custom mix-in", PickerActivity.this, MainActivity.class); 71 | intentPickerSheet.setMixins(Collections.singletonList(customInfo)); 72 | 73 | bottomSheetLayout.showWithSheetView(intentPickerSheet); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bottomsheet-sample/src/main/res/layout/activity_bottom_sheet_fragment.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 |