├── .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 | [](https://travis-ci.org/Flipboard/bottomsheet) [](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 | 
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 |  |  | 
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 | [](https://travis-ci.org/Flipboard/bottomsheet) [](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 | 
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 |  |  | 
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 | *
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 | *
27 | *
{@link #show(FragmentManager, int)}
28 | *
{@link #show(FragmentTransaction, int)}
29 | *
{@link #dismiss()}
30 | *
{@link #dismissAllowingStateLoss()}
31 | *
32 | * There also some Fragment lifecycle methods which should be proxied to the delegate:
33 | *
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 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/layout/activity_image_picker.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
22 |
23 |
29 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/layout/activity_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
22 |
23 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/layout/activity_picker.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
15 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/layout/fragment_my.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/menu/create.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/bottomsheet-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #FFE12828
5 | #c32727
6 | #FF0099CC
7 |
8 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BottomSheet Sample
3 | Share
4 | List
5 | Grid
6 | Intent Picker
7 | Inflating a Menu
8 | Document
9 | Spreadsheet
10 | Folder
11 | Upload photos or videos
12 | Use camera
13 | Share some text!
14 | Image picker
15 | Show
16 | Reopen as other type
17 | BottomSheet Fragment
18 |
19 |
--------------------------------------------------------------------------------
/bottomsheet-sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/bottomsheet/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 | resourcePrefix 'bottomsheet_'
21 | }
22 |
23 | dependencies {
24 | compile fileTree(dir: 'libs', include: ['*.jar'])
25 | compile 'com.android.support:support-v4:23.2.0'
26 | }
27 |
28 | publish {
29 | userOrg = 'flipboard'
30 | groupId = 'com.flipboard'
31 | artifactId = 'bottomsheet-core'
32 | publishVersion = VERSION
33 | description = 'Android component which presents a dismissible view from the bottom of the screen'
34 | website = 'https://github.com/Flipboard/bottomsheet'
35 | licences = ['BSD 3-Clause']
36 | }
37 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/java/com/flipboard/bottomsheet/BaseViewTransformer.java:
--------------------------------------------------------------------------------
1 | package com.flipboard.bottomsheet;
2 |
3 | import android.view.View;
4 |
5 | /**
6 | * A very simple view transformer which only handles applying a correct dim.
7 | */
8 | public abstract class BaseViewTransformer implements ViewTransformer {
9 |
10 | public static final float MAX_DIM_ALPHA = 0.7f;
11 |
12 | @Override
13 | public float getDimAlpha(float translation, float maxTranslation, float peekedTranslation, BottomSheetLayout parent, View view) {
14 | float progress = translation / maxTranslation;
15 | return progress * MAX_DIM_ALPHA;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/java/com/flipboard/bottomsheet/BottomSheetLayout.java:
--------------------------------------------------------------------------------
1 | package com.flipboard.bottomsheet;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ObjectAnimator;
6 | import android.animation.TimeInterpolator;
7 | import android.annotation.TargetApi;
8 | import android.content.Context;
9 | import android.graphics.Color;
10 | import android.graphics.Point;
11 | import android.graphics.Rect;
12 | import android.os.Build;
13 | import android.support.annotation.NonNull;
14 | import android.util.AttributeSet;
15 | import android.util.Property;
16 | import android.view.Gravity;
17 | import android.view.KeyEvent;
18 | import android.view.MotionEvent;
19 | import android.view.VelocityTracker;
20 | import android.view.View;
21 | import android.view.ViewConfiguration;
22 | import android.view.ViewGroup;
23 | import android.view.ViewTreeObserver;
24 | import android.view.WindowManager;
25 | import android.view.animation.DecelerateInterpolator;
26 | import android.widget.FrameLayout;
27 |
28 | import java.util.concurrent.CopyOnWriteArraySet;
29 |
30 | import flipboard.bottomsheet.R;
31 |
32 | public class BottomSheetLayout extends FrameLayout {
33 |
34 | private static final Property SHEET_TRANSLATION = new Property(Float.class, "sheetTranslation") {
35 | @Override
36 | public Float get(BottomSheetLayout object) {
37 | return object.sheetTranslation;
38 | }
39 |
40 | @Override
41 | public void set(BottomSheetLayout object, Float value) {
42 | object.setSheetTranslation(value);
43 | }
44 | };
45 | private Runnable runAfterDismiss;
46 |
47 | /**
48 | * Utility class which registers if the animation has been canceled so that subclasses may respond differently in onAnimationEnd
49 | */
50 | private static class CancelDetectionAnimationListener extends AnimatorListenerAdapter {
51 |
52 | protected boolean canceled;
53 |
54 | @Override
55 | public void onAnimationCancel(Animator animation) {
56 | canceled = true;
57 | }
58 |
59 | }
60 |
61 | private static class IdentityViewTransformer extends BaseViewTransformer {
62 | @Override
63 | public void transformView(float translation, float maxTranslation, float peekedTranslation, BottomSheetLayout parent, View view) {
64 | // no-op
65 | }
66 | }
67 |
68 | public enum State {
69 | HIDDEN,
70 | PREPARING,
71 | PEEKED,
72 | EXPANDED
73 | }
74 |
75 | public interface OnSheetStateChangeListener {
76 | void onSheetStateChanged(State state);
77 | }
78 |
79 | private static final long ANIMATION_DURATION = 300;
80 |
81 | private Rect contentClipRect = new Rect();
82 | private State state = State.HIDDEN;
83 | private boolean peekOnDismiss = false;
84 | private TimeInterpolator animationInterpolator = new DecelerateInterpolator(1.6f);
85 | public boolean bottomSheetOwnsTouch;
86 | private boolean sheetViewOwnsTouch;
87 | private float sheetTranslation;
88 | private VelocityTracker velocityTracker;
89 | private float minFlingVelocity;
90 | private float touchSlop;
91 | private ViewTransformer defaultViewTransformer = new IdentityViewTransformer();
92 | private ViewTransformer viewTransformer;
93 | private boolean shouldDimContentView = true;
94 | private boolean useHardwareLayerWhileAnimating = true;
95 | private Animator currentAnimator;
96 | private CopyOnWriteArraySet onSheetDismissedListeners = new CopyOnWriteArraySet<>();
97 | private CopyOnWriteArraySet onSheetStateChangeListeners = new CopyOnWriteArraySet<>();
98 | private OnLayoutChangeListener sheetViewOnLayoutChangeListener;
99 | private View dimView;
100 | private boolean interceptContentTouch = true;
101 | private int currentSheetViewHeight;
102 | private boolean hasIntercepted;
103 | private float peekKeyline;
104 | private float peek;
105 |
106 | /** Some values we need to manage width on tablets */
107 | private int screenWidth = 0;
108 | private final boolean isTablet = getResources().getBoolean(R.bool.bottomsheet_is_tablet);
109 | private final int defaultSheetWidth = getResources().getDimensionPixelSize(R.dimen.bottomsheet_default_sheet_width);
110 | private int sheetStartX = 0;
111 | private int sheetEndX = 0;
112 |
113 | /** Snapshot of the touch's y position on a down event */
114 | private float downY;
115 |
116 | /** Snapshot of the touch's x position on a down event */
117 | private float downX;
118 |
119 | /** Snapshot of the sheet's translation at the time of the last down event */
120 | private float downSheetTranslation;
121 |
122 | /** Snapshot of the sheet's state at the time of the last down event */
123 | private State downState;
124 |
125 | public BottomSheetLayout(Context context) {
126 | super(context);
127 | init();
128 | }
129 |
130 | public BottomSheetLayout(Context context, AttributeSet attrs) {
131 | this(context, attrs, 0);
132 | }
133 |
134 | public BottomSheetLayout(Context context, AttributeSet attrs, int defStyleAttr) {
135 | super(context, attrs, defStyleAttr);
136 | init();
137 | }
138 |
139 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
140 | public BottomSheetLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
141 | super(context, attrs, defStyleAttr, defStyleRes);
142 | init();
143 | }
144 |
145 | private void init() {
146 | ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
147 | minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
148 | touchSlop = viewConfiguration.getScaledTouchSlop();
149 |
150 | dimView = new View(getContext());
151 | dimView.setBackgroundColor(Color.BLACK);
152 | dimView.setAlpha(0);
153 | dimView.setVisibility(INVISIBLE);
154 |
155 | setFocusableInTouchMode(true);
156 |
157 | Point point = new Point();
158 | ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(point);
159 | screenWidth = point.x;
160 | sheetEndX = screenWidth;
161 |
162 | peek = 0; //getHeight() return 0 at start!
163 | peekKeyline = point.y - (screenWidth / (16.0f / 9.0f));
164 | }
165 |
166 | /**
167 | * Don't call addView directly, use setContentView() and showWithSheetView()
168 | */
169 | @Override
170 | public void addView(@NonNull View child) {
171 | if (getChildCount() > 0) {
172 | throw new IllegalArgumentException("You may not declare more then one child of bottom sheet. The sheet view must be added dynamically with showWithSheetView()");
173 | }
174 | setContentView(child);
175 | }
176 |
177 | @Override
178 | public void addView(@NonNull View child, int index) {
179 | addView(child);
180 | }
181 |
182 | @Override
183 | public void addView(@NonNull View child, int index, @NonNull ViewGroup.LayoutParams params) {
184 | addView(child);
185 | }
186 |
187 | @Override
188 | public void addView(@NonNull View child, @NonNull ViewGroup.LayoutParams params) {
189 | addView(child);
190 | }
191 |
192 | @Override
193 | public void addView(@NonNull View child, int width, int height) {
194 | addView(child);
195 | }
196 |
197 | @Override
198 | protected void onAttachedToWindow() {
199 | super.onAttachedToWindow();
200 | velocityTracker = VelocityTracker.obtain();
201 | }
202 |
203 | @Override
204 | protected void onDetachedFromWindow() {
205 | super.onDetachedFromWindow();
206 | velocityTracker.clear();
207 | cancelCurrentAnimation();
208 | }
209 |
210 | @Override
211 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
212 | super.onLayout(changed, left, top, right, bottom);
213 | int bottomClip = (int) (getHeight() - Math.ceil(sheetTranslation));
214 | this.contentClipRect.set(0, 0, getWidth(), bottomClip);
215 | }
216 |
217 | @Override
218 | public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
219 | if (keyCode == KeyEvent.KEYCODE_BACK && isSheetShowing()) {
220 | if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
221 | KeyEvent.DispatcherState state = getKeyDispatcherState();
222 | if (state != null) {
223 | state.startTracking(event, this);
224 | }
225 | return true;
226 | } else if (event.getAction() == KeyEvent.ACTION_UP) {
227 | KeyEvent.DispatcherState dispatcherState = getKeyDispatcherState();
228 | if (dispatcherState != null) {
229 | dispatcherState.handleUpEvent(event);
230 | }
231 | if (isSheetShowing() && event.isTracking() && !event.isCanceled()) {
232 | if (state == State.EXPANDED && peekOnDismiss) {
233 | peekSheet();
234 | } else {
235 | dismissSheet();
236 | }
237 | return true;
238 | }
239 | }
240 | }
241 | return super.onKeyPreIme(keyCode, event);
242 | }
243 |
244 | private void setSheetTranslation(float newTranslation) {
245 | this.sheetTranslation = Math.min(newTranslation, getMaxSheetTranslation());
246 | int bottomClip = (int) (getHeight() - Math.ceil(sheetTranslation));
247 | this.contentClipRect.set(0, 0, getWidth(), bottomClip);
248 | getSheetView().setTranslationY(getHeight() - sheetTranslation);
249 | transformView(sheetTranslation);
250 | if (shouldDimContentView) {
251 | float dimAlpha = getDimAlpha(sheetTranslation);
252 | dimView.setAlpha(dimAlpha);
253 | dimView.setVisibility(dimAlpha > 0 ? VISIBLE : INVISIBLE);
254 | }
255 | }
256 |
257 | private void transformView(float sheetTranslation) {
258 | if (viewTransformer != null) {
259 | viewTransformer.transformView(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView());
260 | } else if (defaultViewTransformer != null) {
261 | defaultViewTransformer.transformView(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView());
262 | }
263 | }
264 |
265 | private float getDimAlpha(float sheetTranslation) {
266 | if (viewTransformer != null) {
267 | return viewTransformer.getDimAlpha(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView());
268 | } else if (defaultViewTransformer != null) {
269 | return defaultViewTransformer.getDimAlpha(sheetTranslation, getMaxSheetTranslation(), getPeekSheetTranslation(), this, getContentView());
270 | }
271 | return 0;
272 | }
273 |
274 | public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) {
275 | boolean downAction = ev.getActionMasked() == MotionEvent.ACTION_DOWN;
276 | if (downAction) {
277 | hasIntercepted = false;
278 | }
279 | if (interceptContentTouch || (ev.getY() > getHeight() - sheetTranslation && isXInSheet(ev.getX()))) {
280 | hasIntercepted = downAction && isSheetShowing();
281 | } else {
282 | hasIntercepted = false;
283 | }
284 | return hasIntercepted;
285 | }
286 |
287 | @Override
288 | public boolean onTouchEvent(@NonNull MotionEvent event) {
289 | if (!isSheetShowing()) {
290 | return false;
291 | }
292 | if (isAnimating()) {
293 | return false;
294 | }
295 | if (!hasIntercepted) {
296 | return onInterceptTouchEvent(event);
297 | }
298 | if (event.getAction() == MotionEvent.ACTION_DOWN) {
299 | // Snapshot the state of things when finger touches the screen.
300 | // This allows us to calculate deltas without losing precision which we would have if we calculated deltas based on the previous touch.
301 | bottomSheetOwnsTouch = false;
302 | sheetViewOwnsTouch = false;
303 | downY = event.getY();
304 | downX = event.getX();
305 | downSheetTranslation = sheetTranslation;
306 | downState = state;
307 | velocityTracker.clear();
308 | }
309 | velocityTracker.addMovement(event);
310 |
311 | // The max translation is a hard limit while the min translation is where we start dragging more slowly and allow the sheet to be dismissed.
312 | float maxSheetTranslation = getMaxSheetTranslation();
313 | float peekSheetTranslation = getPeekSheetTranslation();
314 |
315 | float deltaY = downY - event.getY();
316 | float deltaX = downX - event.getX();
317 |
318 | if (!bottomSheetOwnsTouch && !sheetViewOwnsTouch) {
319 | bottomSheetOwnsTouch = Math.abs(deltaY) > touchSlop;
320 | sheetViewOwnsTouch = Math.abs(deltaX) > touchSlop;
321 |
322 | if (bottomSheetOwnsTouch) {
323 | if (state == State.PEEKED) {
324 | MotionEvent cancelEvent = MotionEvent.obtain(event);
325 | cancelEvent.offsetLocation(0, sheetTranslation - getHeight());
326 | cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
327 | getSheetView().dispatchTouchEvent(cancelEvent);
328 | cancelEvent.recycle();
329 | }
330 |
331 | sheetViewOwnsTouch = false;
332 | downY = event.getY();
333 | downX = event.getX();
334 | deltaY = 0;
335 | deltaX = 0;
336 | }
337 | }
338 |
339 | // This is not the actual new sheet translation but a first approximation it will be adjusted to account for max and min translations etc.
340 | float newSheetTranslation = downSheetTranslation + deltaY;
341 |
342 | if (bottomSheetOwnsTouch) {
343 | // If we are scrolling down and the sheet cannot scroll further, go out of expanded mode.
344 | boolean scrollingDown = deltaY < 0;
345 | boolean canScrollUp = canScrollUp(getSheetView(), event.getX(), event.getY() + (sheetTranslation - getHeight()));
346 | if (state == State.EXPANDED && scrollingDown && !canScrollUp) {
347 | // Reset variables so deltas are correctly calculated from the point at which the sheet was 'detached' from the top.
348 | downY = event.getY();
349 | downSheetTranslation = sheetTranslation;
350 | velocityTracker.clear();
351 | setState(State.PEEKED);
352 | setSheetLayerTypeIfEnabled(LAYER_TYPE_HARDWARE);
353 | newSheetTranslation = sheetTranslation;
354 |
355 | // Dispatch a cancel event to the sheet to make sure its touch handling is cleaned up nicely.
356 | MotionEvent cancelEvent = MotionEvent.obtain(event);
357 | cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
358 | getSheetView().dispatchTouchEvent(cancelEvent);
359 | cancelEvent.recycle();
360 | }
361 |
362 | // If we are at the top of the view we should go into expanded mode.
363 | if (state == State.PEEKED && newSheetTranslation > maxSheetTranslation) {
364 | setSheetTranslation(maxSheetTranslation);
365 |
366 | // Dispatch a down event to the sheet to make sure its touch handling is initiated correctly.
367 | newSheetTranslation = Math.min(maxSheetTranslation, newSheetTranslation);
368 | MotionEvent downEvent = MotionEvent.obtain(event);
369 | downEvent.setAction(MotionEvent.ACTION_DOWN);
370 | getSheetView().dispatchTouchEvent(downEvent);
371 | downEvent.recycle();
372 | setState(State.EXPANDED);
373 | setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE);
374 | }
375 |
376 | if (state == State.EXPANDED) {
377 | // Dispatch the touch to the sheet if we are expanded so it can handle its own internal scrolling.
378 | event.offsetLocation(0, sheetTranslation - getHeight());
379 | getSheetView().dispatchTouchEvent(event);
380 | } else {
381 | // Make delta less effective when sheet is below the minimum translation.
382 | // This makes it feel like scrolling in jello which gives the user an indication that the sheet will be dismissed if they let go.
383 | if (newSheetTranslation < peekSheetTranslation) {
384 | newSheetTranslation = peekSheetTranslation - (peekSheetTranslation - newSheetTranslation) / 4f;
385 | }
386 |
387 | setSheetTranslation(newSheetTranslation);
388 |
389 | if (event.getAction() == MotionEvent.ACTION_CANCEL) {
390 | // If touch is canceled, go back to previous state, a canceled touch should never commit an action.
391 | if (downState == State.EXPANDED) {
392 | expandSheet();
393 | } else {
394 | peekSheet();
395 | }
396 | }
397 |
398 | if (event.getAction() == MotionEvent.ACTION_UP) {
399 | if (newSheetTranslation < peekSheetTranslation) {
400 | dismissSheet();
401 | } else {
402 | // If touch is released, go to a new state depending on velocity.
403 | // If the velocity is not high enough we use the position of the sheet to determine the new state.
404 | velocityTracker.computeCurrentVelocity(1000);
405 | float velocityY = velocityTracker.getYVelocity();
406 | if (Math.abs(velocityY) < minFlingVelocity) {
407 | if (sheetTranslation > getHeight() / 2) {
408 | expandSheet();
409 | } else {
410 | peekSheet();
411 | }
412 | } else {
413 | if (velocityY < 0) {
414 | expandSheet();
415 | } else {
416 | peekSheet();
417 | }
418 | }
419 | }
420 | }
421 | }
422 | } else {
423 | // If the user clicks outside of the bottom sheet area we should dismiss the bottom sheet.
424 | boolean touchOutsideBottomSheet = event.getY() < getHeight() - sheetTranslation || !isXInSheet(event.getX());
425 | if (event.getAction() == MotionEvent.ACTION_UP && touchOutsideBottomSheet && interceptContentTouch) {
426 | dismissSheet();
427 | return true;
428 | }
429 |
430 | event.offsetLocation(isTablet ? getX() - sheetStartX : 0, sheetTranslation - getHeight());
431 | getSheetView().dispatchTouchEvent(event);
432 | }
433 | return true;
434 | }
435 |
436 | private boolean isXInSheet(float x) {
437 | return !isTablet || x >= sheetStartX && x <= sheetEndX;
438 | }
439 |
440 | private boolean isAnimating() {
441 | return currentAnimator != null;
442 | }
443 |
444 | private void cancelCurrentAnimation() {
445 | if (currentAnimator != null) {
446 | currentAnimator.cancel();
447 | }
448 | }
449 |
450 | private boolean canScrollUp(View view, float x, float y) {
451 | if (view instanceof ViewGroup) {
452 | ViewGroup vg = (ViewGroup) view;
453 | for (int i = 0; i < vg.getChildCount(); i++) {
454 | View child = vg.getChildAt(i);
455 | int childLeft = child.getLeft() - view.getScrollX();
456 | int childTop = child.getTop() - view.getScrollY();
457 | int childRight = child.getRight() - view.getScrollX();
458 | int childBottom = child.getBottom() - view.getScrollY();
459 | boolean intersects = x > childLeft && x < childRight && y > childTop && y < childBottom;
460 | if (intersects && canScrollUp(child, x - childLeft, y - childTop)) {
461 | return true;
462 | }
463 | }
464 | }
465 | return view.canScrollVertically(-1);
466 | }
467 |
468 | private void setSheetLayerTypeIfEnabled(int layerType) {
469 | if (useHardwareLayerWhileAnimating) {
470 | getSheetView().setLayerType(layerType, null);
471 | }
472 | }
473 |
474 | private void setState(State state) {
475 | if (state != this.state) {
476 | this.state = state;
477 | for (OnSheetStateChangeListener onSheetStateChangeListener : onSheetStateChangeListeners) {
478 | onSheetStateChangeListener.onSheetStateChanged(state);
479 | }
480 | }
481 | }
482 |
483 | private boolean hasTallerKeylineHeightSheet() {
484 | return getSheetView() == null || getSheetView().getHeight() > peekKeyline;
485 | }
486 |
487 | private boolean hasFullHeightSheet() {
488 | return getSheetView() == null || getSheetView().getHeight() == getHeight();
489 | }
490 |
491 | /**
492 | * Set dim and translation to the initial state
493 | * */
494 | private void initializeSheetValues() {
495 | this.sheetTranslation = 0;
496 | this.contentClipRect.set(0, 0, getWidth(), getHeight());
497 | getSheetView().setTranslationY(getHeight());
498 | dimView.setAlpha(0);
499 | dimView.setVisibility(INVISIBLE);
500 | }
501 |
502 | /**
503 | * Set the presented sheet to be in an expanded state.
504 | */
505 | public void expandSheet() {
506 | cancelCurrentAnimation();
507 | setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE);
508 | ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, getMaxSheetTranslation());
509 | anim.setDuration(ANIMATION_DURATION);
510 | anim.setInterpolator(animationInterpolator);
511 | anim.addListener(new CancelDetectionAnimationListener() {
512 | @Override
513 | public void onAnimationEnd(@NonNull Animator animation) {
514 | if (!canceled) {
515 | currentAnimator = null;
516 | }
517 | }
518 | });
519 | anim.start();
520 | currentAnimator = anim;
521 | setState(State.EXPANDED);
522 | }
523 |
524 | /**
525 | * Set the presented sheet to be in a peeked state.
526 | */
527 | public void peekSheet() {
528 | cancelCurrentAnimation();
529 | setSheetLayerTypeIfEnabled(LAYER_TYPE_HARDWARE);
530 | ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, getPeekSheetTranslation());
531 | anim.setDuration(ANIMATION_DURATION);
532 | anim.setInterpolator(animationInterpolator);
533 | anim.addListener(new CancelDetectionAnimationListener() {
534 | @Override
535 | public void onAnimationEnd(@NonNull Animator animation) {
536 | if (!canceled) {
537 | currentAnimator = null;
538 | }
539 | }
540 | });
541 | anim.start();
542 | currentAnimator = anim;
543 | setState(State.PEEKED);
544 | }
545 |
546 | /**
547 | * @return The peeked state translation for the presented sheet view. Translation is counted from the bottom of the view.
548 | */
549 | public float getPeekSheetTranslation() {
550 | return peek == 0 ? getDefaultPeekTranslation() : peek;
551 | }
552 |
553 | private float getDefaultPeekTranslation() {
554 | return hasTallerKeylineHeightSheet() ? peekKeyline : getSheetView().getHeight();
555 | }
556 |
557 | /**
558 | * Set custom height for PEEKED state.
559 | *
560 | * @param peek Peek height in pixels
561 | */
562 | public void setPeekSheetTranslation(float peek) {
563 | this.peek = peek;
564 | }
565 |
566 | /**
567 | * @return The maximum translation for the presented sheet view. Translation is counted from the bottom of the view.
568 | */
569 | public float getMaxSheetTranslation() {
570 | return hasFullHeightSheet() ? getHeight() - getPaddingTop() : getSheetView().getHeight();
571 | }
572 |
573 | /**
574 | * @return The currently presented sheet view. If no sheet is currently presented null will returned.
575 | */
576 | public View getContentView() {
577 | return getChildCount() > 0 ? getChildAt(0) : null;
578 | }
579 |
580 | /**
581 | * @return The currently presented sheet view. If no sheet is currently presented null will returned.
582 | */
583 | public View getSheetView() {
584 | return getChildCount() > 2 ? getChildAt(2) : null;
585 | }
586 |
587 | /**
588 | * Set the content view of the bottom sheet. This is the view which is shown under the sheet
589 | * being presented. This is usually the root view of your application.
590 | *
591 | * @param contentView The content view of your application.
592 | */
593 | public void setContentView(View contentView) {
594 | super.addView(contentView, -1, generateDefaultLayoutParams());
595 | super.addView(dimView, -1, generateDefaultLayoutParams());
596 | }
597 |
598 | /**
599 | * Convenience for showWithSheetView(sheetView, null, null).
600 | *
601 | * @param sheetView The sheet to be presented.
602 | */
603 | public void showWithSheetView(View sheetView) {
604 | showWithSheetView(sheetView, null);
605 | }
606 |
607 | /**
608 | * Present a sheet view to the user.
609 | * If another sheet is currently presented, it will be dismissed, and the new sheet will be shown after that
610 | *
611 | * @param sheetView The sheet to be presented.
612 | * @param viewTransformer The view transformer to use when presenting the sheet.
613 | */
614 | public void showWithSheetView(final View sheetView, final ViewTransformer viewTransformer) {
615 | if (state != State.HIDDEN) {
616 | Runnable runAfterDismissThis = new Runnable() {
617 | @Override
618 | public void run() {
619 | showWithSheetView(sheetView, viewTransformer);
620 | }
621 | };
622 | dismissSheet(runAfterDismissThis);
623 | return;
624 | }
625 | setState(State.PREPARING);
626 |
627 | LayoutParams params = (LayoutParams) sheetView.getLayoutParams();
628 | if (params == null) {
629 | params = new LayoutParams(isTablet ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
630 | }
631 |
632 | if (isTablet && params.width == FrameLayout.LayoutParams.WRAP_CONTENT) {
633 |
634 | // Center by default if they didn't specify anything
635 | if (params.gravity == -1) {
636 | params.gravity = Gravity.CENTER_HORIZONTAL;
637 | }
638 |
639 | params.width = defaultSheetWidth;
640 |
641 | // Update start and end coordinates for touch reference
642 | int horizontalSpacing = screenWidth - defaultSheetWidth;
643 | sheetStartX = horizontalSpacing / 2;
644 | sheetEndX = screenWidth - sheetStartX;
645 | }
646 |
647 | super.addView(sheetView, -1, params);
648 | initializeSheetValues();
649 | this.viewTransformer = viewTransformer;
650 |
651 | // Don't start animating until the sheet has been drawn once. This ensures that we don't do layout while animating and that
652 | // the drawing cache for the view has been warmed up. tl;dr it reduces lag.
653 | getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
654 | @Override
655 | public boolean onPreDraw() {
656 | getViewTreeObserver().removeOnPreDrawListener(this);
657 | post(new Runnable() {
658 | @Override
659 | public void run() {
660 | // Make sure sheet view is still here when first draw happens.
661 | // In the case of a large lag it could be that the view is dismissed before it is drawn resulting in sheet view being null here.
662 | if (getSheetView() != null) {
663 | peekSheet();
664 | }
665 | }
666 | });
667 | return true;
668 | }
669 | });
670 |
671 | // sheetView should always be anchored to the bottom of the screen
672 | currentSheetViewHeight = sheetView.getMeasuredHeight();
673 | sheetViewOnLayoutChangeListener = new OnLayoutChangeListener() {
674 | @Override
675 | public void onLayoutChange(View sheetView, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
676 | int newSheetViewHeight = sheetView.getMeasuredHeight();
677 | if (state != State.HIDDEN) {
678 | // The sheet can no longer be in the expanded state if it has shrunk
679 | if (newSheetViewHeight < currentSheetViewHeight) {
680 | if (state == State.EXPANDED) {
681 | setState(State.PEEKED);
682 | }
683 | setSheetTranslation(newSheetViewHeight);
684 | } else if (currentSheetViewHeight > 0 && newSheetViewHeight > currentSheetViewHeight && state == State.PEEKED) {
685 | if (newSheetViewHeight == getMaxSheetTranslation()) {
686 | setState(State.EXPANDED);
687 | }
688 | setSheetTranslation(newSheetViewHeight);
689 | }
690 | }
691 | currentSheetViewHeight = newSheetViewHeight;
692 | }
693 | };
694 | sheetView.addOnLayoutChangeListener(sheetViewOnLayoutChangeListener);
695 | }
696 |
697 | /**
698 | * Dismiss the sheet currently being presented.
699 | */
700 | public void dismissSheet() {
701 | dismissSheet(null);
702 | }
703 |
704 | private void dismissSheet(Runnable runAfterDismissThis) {
705 | if (state == State.HIDDEN) {
706 | runAfterDismiss = null;
707 | return;
708 | }
709 | // This must be set every time, including if the parameter is null
710 | // Otherwise a new sheet might be shown when the caller called dismiss after a showWithSheet call, which would be
711 | runAfterDismiss = runAfterDismissThis;
712 | final View sheetView = getSheetView();
713 | sheetView.removeOnLayoutChangeListener(sheetViewOnLayoutChangeListener);
714 | cancelCurrentAnimation();
715 | ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHEET_TRANSLATION, 0);
716 | anim.setDuration(ANIMATION_DURATION);
717 | anim.setInterpolator(animationInterpolator);
718 | anim.addListener(new CancelDetectionAnimationListener() {
719 | @Override
720 | public void onAnimationEnd(Animator animation) {
721 | if (!canceled) {
722 | currentAnimator = null;
723 | setState(State.HIDDEN);
724 | setSheetLayerTypeIfEnabled(LAYER_TYPE_NONE);
725 | removeView(sheetView);
726 |
727 | for (OnSheetDismissedListener onSheetDismissedListener : onSheetDismissedListeners) {
728 | onSheetDismissedListener.onDismissed(BottomSheetLayout.this);
729 | }
730 |
731 | // Remove sheet specific properties
732 | viewTransformer = null;
733 | if (runAfterDismiss != null) {
734 | runAfterDismiss.run();
735 | runAfterDismiss = null;
736 | }
737 | }
738 | }
739 | });
740 | anim.start();
741 | currentAnimator = anim;
742 | sheetStartX = 0;
743 | sheetEndX = screenWidth;
744 | }
745 |
746 | /**
747 | * Controls the behavior on back button press when the state is {@link State#EXPANDED}.
748 | *
749 | * @param peekOnDismiss true to show the peeked state on back press or false to completely hide
750 | * the Bottom Sheet. Default is false.
751 | */
752 | public void setPeekOnDismiss(boolean peekOnDismiss) {
753 | this.peekOnDismiss = peekOnDismiss;
754 | }
755 |
756 | /**
757 | * Returns the current peekOnDismiss value, which controls the behavior response to back presses
758 | * when the current state is {@link State#EXPANDED}.
759 | *
760 | * @return the current peekOnDismiss value
761 | */
762 | public boolean getPeekOnDismiss() {
763 | return peekOnDismiss;
764 | }
765 |
766 | /**
767 | * Controls whether or not child view interaction is possible when the bottomsheet is open.
768 | *
769 | * @param interceptContentTouch true to intercept content view touches or false to allow
770 | * interaction with Bottom Sheet's content view
771 | */
772 | public void setInterceptContentTouch(boolean interceptContentTouch) {
773 | this.interceptContentTouch = interceptContentTouch;
774 | }
775 |
776 | /**
777 | * @return true if we are intercepting content view touches or false to allow interaction with
778 | * Bottom Sheet's content view. Default value is true.
779 | */
780 | public boolean getInterceptContentTouch() {
781 | return interceptContentTouch;
782 | }
783 |
784 | /**
785 | * @return The current state of the sheet.
786 | */
787 | public State getState() {
788 | return state;
789 | }
790 |
791 | /**
792 | * @return Whether or not a sheet is currently presented.
793 | */
794 | public boolean isSheetShowing() {
795 | return state != State.HIDDEN;
796 | }
797 |
798 | /**
799 | * Set the default view transformer to use for showing a sheet. Usually applications will use
800 | * a similar transformer for most use cases of bottom sheet so this is a convenience instead of
801 | * passing a new transformer each time a sheet is shown. This choice is overridden by any
802 | * view transformer passed to showWithSheetView().
803 | *
804 | * @param defaultViewTransformer The view transformer user by default.
805 | */
806 | public void setDefaultViewTransformer(ViewTransformer defaultViewTransformer) {
807 | this.defaultViewTransformer = defaultViewTransformer;
808 | }
809 |
810 | /**
811 | * Enable or disable dimming of the content view while a sheet is presented. If enabled a
812 | * transparent black dim is overlaid on top of the content view indicating that the sheet is the
813 | * foreground view. This dim is animated into place is coordination with the sheet view.
814 | * Defaults to true.
815 | *
816 | * @param shouldDimContentView whether or not to dim the content view.
817 | */
818 | public void setShouldDimContentView(boolean shouldDimContentView) {
819 | this.shouldDimContentView = shouldDimContentView;
820 | }
821 |
822 | /**
823 | * @return whether the content view is being dimmed while presenting a sheet or not.
824 | */
825 | public boolean shouldDimContentView() {
826 | return shouldDimContentView;
827 | }
828 |
829 | /**
830 | * Enable or disable the use of a hardware layer for the presented sheet while animating.
831 | * This settings defaults to true and should only be changed if you know that putting the
832 | * sheet in a layer will negatively effect performance. One such example is if the sheet contains
833 | * a view which needs to frequently be re-drawn.
834 | *
835 | * @param useHardwareLayerWhileAnimating whether or not to use a hardware layer.
836 | */
837 | public void setUseHardwareLayerWhileAnimating(boolean useHardwareLayerWhileAnimating) {
838 | this.useHardwareLayerWhileAnimating = useHardwareLayerWhileAnimating;
839 | }
840 |
841 | /**
842 | * Adds an {@link OnSheetStateChangeListener} which will be notified when the state of the presented sheet changes.
843 | * The listener will not be automatically removed, so remember to remove it when it's no longer needed
844 | * (probably when the sheet is HIDDEN)
845 | *
846 | * @param onSheetStateChangeListener the listener to be notified.
847 | */
848 | public void addOnSheetStateChangeListener(@NonNull OnSheetStateChangeListener onSheetStateChangeListener) {
849 | checkNotNull(onSheetStateChangeListener, "onSheetStateChangeListener == null");
850 | this.onSheetStateChangeListeners.add(onSheetStateChangeListener);
851 | }
852 |
853 | /**
854 | * Adds an {@link OnSheetDismissedListener} which will be notified when the state of the presented sheet changes.
855 | * The listener will not be automatically removed, so remember to remove it when it's no longer needed
856 | * (probably when the sheet is HIDDEN)
857 | *
858 | * @param onSheetDismissedListener the listener to be notified.
859 | */
860 | public void addOnSheetDismissedListener(@NonNull OnSheetDismissedListener onSheetDismissedListener) {
861 | checkNotNull(onSheetDismissedListener, "onSheetDismissedListener == null");
862 | this.onSheetDismissedListeners.add(onSheetDismissedListener);
863 | }
864 |
865 | /**
866 | * Removes a previously added {@link OnSheetStateChangeListener}.
867 | *
868 | * @param onSheetStateChangeListener the listener to be removed.
869 | */
870 | public void removeOnSheetStateChangeListener(@NonNull OnSheetStateChangeListener onSheetStateChangeListener) {
871 | checkNotNull(onSheetStateChangeListener, "onSheetStateChangeListener == null");
872 | this.onSheetStateChangeListeners.remove(onSheetStateChangeListener);
873 | }
874 |
875 | /**
876 | * Removes a previously added {@link OnSheetDismissedListener}.
877 | *
878 | * @param onSheetDismissedListener the listener to be removed.
879 | */
880 | public void removeOnSheetDismissedListener(@NonNull OnSheetDismissedListener onSheetDismissedListener) {
881 | checkNotNull(onSheetDismissedListener, "onSheetDismissedListener == null");
882 | this.onSheetDismissedListeners.remove(onSheetDismissedListener);
883 | }
884 |
885 | /**
886 | * Returns whether or not BottomSheetLayout will assume it's being shown on a tablet.
887 | *
888 | * @param context Context instance to retrieve resources
889 | * @return True if BottomSheetLayout will assume it's being shown on a tablet, false if not
890 | */
891 | public static boolean isTablet(Context context) {
892 | return context.getResources().getBoolean(R.bool.bottomsheet_is_tablet);
893 | }
894 |
895 | /**
896 | * Returns the predicted default width of the sheet if it were shown.
897 | *
898 | * @param context Context instance to retrieve resources and display metrics
899 | * @return Predicted width of the sheet if shown
900 | */
901 | public static int predictedDefaultWidth(Context context) {
902 | if (isTablet(context)) {
903 | return context.getResources().getDimensionPixelSize(R.dimen.bottomsheet_default_sheet_width);
904 | } else {
905 | return context.getResources().getDisplayMetrics().widthPixels;
906 | }
907 | }
908 |
909 | private static T checkNotNull(T value, String message) {
910 | if (value == null) {
911 | throw new NullPointerException(message);
912 | }
913 | return value;
914 | }
915 | }
916 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/java/com/flipboard/bottomsheet/OnSheetDismissedListener.java:
--------------------------------------------------------------------------------
1 | package com.flipboard.bottomsheet;
2 |
3 | public interface OnSheetDismissedListener {
4 |
5 | /**
6 | * Called when the presented sheet has been dismissed.
7 | *
8 | * @param bottomSheetLayout The bottom sheet which contained the presented sheet.
9 | */
10 | void onDismissed(BottomSheetLayout bottomSheetLayout);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/java/com/flipboard/bottomsheet/ViewTransformer.java:
--------------------------------------------------------------------------------
1 | package com.flipboard.bottomsheet;
2 |
3 | import android.view.View;
4 |
5 | public interface ViewTransformer {
6 |
7 | /**
8 | * Called on every frame while animating the presented sheet. This method allows you to coordinate
9 | * other animations (usually on the content view) with the sheet view's translation.
10 | *
11 | * @param translation The current translation of the presented sheet view.
12 | * @param maxTranslation The max translation of the presented sheet view.
13 | * @param peekedTranslation The peeked state translation of the presented sheet view.
14 | * @param parent The BottomSheet presenting the sheet view.
15 | * @param view The content view to transform.
16 | */
17 | void transformView(float translation, float maxTranslation, float peekedTranslation, BottomSheetLayout parent, View view);
18 |
19 | /**
20 | * Called on when the translation of the sheet view changes allowing you to customize the amount of dimming which
21 | * is applied to the content view.
22 | *
23 | * @param translation The current translation of the presented sheet view.
24 | * @param maxTranslation The max translation of the presented sheet view.
25 | * @param peekedTranslation The peeked state translation of the presented sheet view.
26 | * @param parent The BottomSheet presenting the sheet view.
27 | * @param view The content view to transform.
28 | *
29 | * @return The alpha to apply to the dim overlay.
30 | */
31 | float getDimAlpha(float translation, float maxTranslation, float peekedTranslation, BottomSheetLayout parent, View view);
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values-sw600dp/bool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values-sw600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 446dp
4 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values-sw720dp/bool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values-sw720dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 480dp
4 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values/bool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
--------------------------------------------------------------------------------
/bottomsheet/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0dp
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.1.0'
9 | classpath 'com.novoda:bintray-release:0.3.4'
10 | }
11 | }
12 |
13 | allprojects {
14 | repositories {
15 | jcenter()
16 | }
17 | }
18 |
19 | ext {
20 | VERSION = version()
21 | }
22 |
23 | task bumpMajor << {
24 | ant.propertyfile(file: 'version.properties') {
25 | entry(key: 'major', type: 'int', operation: '+', value: 1)
26 | entry(key: 'minor', type: 'int', operation: '=', value: 0)
27 | entry(key: 'patch', type: 'int', operation: '=', value: 0)
28 | }
29 | }
30 |
31 | task bumpMinor << {
32 | ant.propertyfile(file: 'version.properties') {
33 | entry(key: 'minor', type: 'int', operation: '+', value: 1)
34 | entry(key: 'patch', type: 'int', operation: '=', value: 0)
35 | }
36 | }
37 |
38 | task bumpPatch << {
39 | ant.propertyfile(file: 'version.properties') {
40 | entry(key: 'patch', type: 'int', operation: '+', value: 1)
41 | }
42 | }
43 |
44 | task genReadMe << {
45 | def template = file('README.md.template').text
46 | def result = template.replaceAll("%%version%%", version())
47 | file("README.md").withWriter{ it << result }
48 | }
49 |
50 | task version << {
51 | println version()
52 | }
53 |
54 | def String version() {
55 | def versionPropsFile = file('version.properties')
56 | def Properties versionProps = new Properties()
57 | versionProps.load(new FileInputStream(versionPropsFile))
58 |
59 | return versionProps['major'] + "." + versionProps['minor'] + "." + versionProps['patch']
60 | }
61 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Flipboard/bottomsheet/2792f24d60a19e0948ff0e9977ea6ccff4a6a35d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue May 17 09:31:40 PDT 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':bottomsheet', ':bottomsheet-commons', ':bottomsheet-sample'
2 |
--------------------------------------------------------------------------------
/version.properties:
--------------------------------------------------------------------------------
1 | #Tue, 29 Nov 2016 11:06:26 -0800
2 | #Tue Jun 02 09:57:59 PDT 2015
3 | major=1
4 | minor=5
5 | patch=3
6 |
--------------------------------------------------------------------------------