├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ ├── clipsub │ │ └── rnbottomsheet │ │ │ ├── RNBottomSheet.java │ │ │ └── RNBottomSheetPackage.java │ │ └── cocosw │ │ └── bottomsheet │ │ ├── ActionMenu.java │ │ ├── ActionMenuItem.java │ │ ├── BottomSheet.java │ │ ├── BottomSheetHelper.java │ │ ├── ClosableSlidingLayout.java │ │ ├── FillerView.java │ │ ├── HeaderLayout.java │ │ ├── PinnedSectionGridView.java │ │ ├── SimpleSectionedGridAdapter.java │ │ └── TranslucentHelper.java │ └── res │ ├── anim-v21 │ ├── bs_list_item_in.xml │ └── bs_list_layout_anim_in.xml │ ├── anim │ ├── bs_list_item_in.xml │ ├── bs_list_layout_anim_in.xml │ ├── dock_bottom_enter.xml │ └── dock_bottom_exit.xml │ ├── drawable-hdpi │ ├── bs_ic_clear.png │ ├── bs_ic_clear_light.png │ ├── bs_ic_more.png │ └── bs_ic_more_light.png │ ├── drawable-mdpi │ ├── bs_ic_clear.png │ ├── bs_ic_clear_light.png │ ├── bs_ic_more.png │ └── bs_ic_more_light.png │ ├── drawable-v21 │ ├── bs_list_dark_selector.xml │ └── bs_list_selector.xml │ ├── drawable-xhdpi │ ├── bs_ic_clear.png │ ├── bs_ic_clear_light.png │ ├── bs_ic_more.png │ └── bs_ic_more_light.png │ ├── drawable-xxhdpi │ ├── bs_ic_clear.png │ ├── bs_ic_clear_light.png │ ├── bs_ic_more.png │ └── bs_ic_more_light.png │ ├── drawable │ ├── bs_list_dark_selector.xml │ └── bs_list_selector.xml │ ├── layout │ ├── bottom_sheet_dialog.xml │ ├── bs_grid_entry.xml │ ├── bs_header.xml │ ├── bs_list_divider.xml │ └── bs_list_entry.xml │ ├── values-land │ └── integer.xml │ ├── values-sw600dp-land │ └── integer.xml │ ├── values-sw600dp │ └── integer.xml │ ├── values-zh │ └── string.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ids.xml │ ├── integer.xml │ ├── string.xml │ └── styles.xml ├── babel.config.js ├── image-demo.png ├── index.d.ts ├── index.js ├── module.ios.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": false, 6 | "node": true 7 | }, 8 | "rules": { 9 | "no-param-reassign": ["error", { "props": false }], 10 | "no-underscore-dangle": "off", 11 | "import/extensions": "off", 12 | "import/no-extraneous-dependencies": ["error", { 13 | "optionalDependencies": false, 14 | "peerDependencies": true 15 | }], 16 | "import/no-unresolved": "off", 17 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }] 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | 43 | .cache 44 | .rts2_cache_cjs 45 | .rts2_cache_esm 46 | .rts2_cache_umd 47 | dist 48 | yarn.lock 49 | package-lock.json 50 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | example 3 | .rts* 4 | todo.txt 5 | image-demo.png 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Clip-sub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-bottomsheet 2 | 3 | `react-native-bottomsheet` is a cross-platform ActionSheet for both Android and iOS. It uses original ActionSheet on iOS and [soarcn BottomSheet](https://github.com/soarcn/BottomSheet) on Android with some minor fixes, such as title and list item margins. 4 | 5 | *Update*: The library now uses a copy of original lib (source copied) so you can modify it whenever you need to. 6 | 7 | ![bottomsheet](https://raw.githubusercontent.com/acaziasoftcom/react-native-bottomsheet/master/image-demo.png) 8 | 9 | Note: On Android, `message` property is not available. Instead, there's a `dark` option to turn on Dark Mode like so: 10 | 11 | ```js 12 | BottomSheet.showBottomSheetWithOptions({ 13 | options: ['Option 1', 'Option 2', 'Option 3'], 14 | title: 'Demo Bottom Sheet', 15 | dark: true, 16 | cancelButtonIndex: 3, 17 | }, (value) => { 18 | alert(value); 19 | }); 20 | ``` 21 | 22 | ## Installation 23 | 24 | First, install the npm package: 25 | ```bash 26 | npm install --save react-native-bottomsheet 27 | ``` 28 | Then: 29 | 30 | ### - If you are using React Native _0.60+_ 31 | 32 | You don't have to do anything, since it will be linked automatically for you. 33 | 34 | ### - If you are using React Native 0.59 and below 35 | 36 | Then link the native module, since we are using native bottom sheet on Android: 37 | ```bash 38 | react-native link react-native-bottomsheet 39 | ``` 40 | 41 | Or you can link it manually in `MainApplication.java` 42 | 43 | ```java 44 | import com.clipsub.rnbottomsheet.RNBottomSheetPackage; // Import this 45 | 46 | .... 47 | @Override 48 | protected List getPackages() { 49 | return Arrays.asList( 50 | new RNBottomSheetPackage() // Add this 51 | ); 52 | } 53 | ``` 54 | 55 | ## Usage 56 | 57 | ### Import the package 58 | ```js 59 | import BottomSheet from 'react-native-bottomsheet'; 60 | ``` 61 | 62 | ### Use it like how you do with ActionSheet. 63 | ````js 64 | BottomSheet.showBottomSheetWithOptions(options: Object, callback: Function) 65 | BottomSheet.showShareBottomSheetWithOptions(options: Object, failureCallback: Function, successCallback: Function) 66 | ```` 67 | 68 | Example: 69 | 70 | ```js 71 | import BottomSheet from 'react-native-bottomsheet'; 72 | ... 73 | BottomSheet.showBottomSheetWithOptions({ 74 | options: ['Option 1', 'Option 2', 'Option 3'], 75 | title: 'Demo Bottom Sheet', 76 | dark: true, 77 | cancelButtonIndex: 3, 78 | }, (value) => { 79 | alert(value); 80 | }); 81 | ``` 82 | 83 | ```js 84 | BottomSheet.showShareBottomSheetWithOptions({ 85 | url: 'https://google.com', 86 | subject: 'Share', 87 | message: 'Simple share', 88 | }, (value) => { 89 | alert(value); 90 | }, (resultcode, path) => { 91 | alert(resultcode); 92 | alert(path); 93 | }) 94 | ``` 95 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.library" 2 | 3 | def extGetOrDefault(prop, fallback) { 4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 5 | } 6 | 7 | android { 8 | compileSdkVersion extGetOrDefault('compileSdkVersion', 30) 9 | buildToolsVersion extGetOrDefault('buildToolsVersion', "30.0.2") 10 | 11 | defaultConfig { 12 | minSdkVersion extGetOrDefault('minSdkVersion', 16) 13 | targetSdkVersion extGetOrDefault('targetSdkVersion', 30) 14 | versionCode 1 15 | versionName "1.0.0" 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenLocal() 22 | jcenter() 23 | maven { 24 | // For developing the library outside the context of the example app, expect `react-native` 25 | // to be installed at `./node_modules`. 26 | url "$projectDir/../node_modules/react-native/android" 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation "com.facebook.react:react-native:+" 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/clipsub/rnbottomsheet/RNBottomSheet.java: -------------------------------------------------------------------------------- 1 | package com.clipsub.rnbottomsheet; 2 | 3 | import android.content.DialogInterface; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | import com.cocosw.bottomsheet.BottomSheet; 8 | import com.facebook.react.bridge.Callback; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 11 | import com.facebook.react.bridge.ReactMethod; 12 | import com.facebook.react.bridge.ReadableArray; 13 | import com.facebook.react.bridge.ReadableMap; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class RNBottomSheet extends ReactContextBaseJavaModule { 19 | private boolean isOpened; 20 | 21 | public RNBottomSheet(ReactApplicationContext reactContext) { 22 | super(reactContext); 23 | } 24 | 25 | @Override 26 | public String getName() { 27 | return "RNBottomSheet"; 28 | } 29 | 30 | @ReactMethod 31 | public void showBottomSheetWithOptions(ReadableMap options, final Callback onSelect) { 32 | if (this.isOpened) return; 33 | 34 | this.isOpened = true; 35 | 36 | ReadableArray optionArray = options.getArray("options"); 37 | final Integer cancelButtonIndex = options.getInt("cancelButtonIndex"); 38 | String title; 39 | boolean dark; 40 | BottomSheet.Builder builder; 41 | 42 | // Title. 43 | try { 44 | title = options.getString("title"); 45 | builder = new BottomSheet.Builder(this.getCurrentActivity()).title(title); 46 | } catch (Exception e) { 47 | builder = new BottomSheet.Builder(this.getCurrentActivity()); 48 | } 49 | 50 | // Dark theme. 51 | try { 52 | dark = options.getBoolean("dark"); 53 | if (dark) { 54 | builder.darkTheme(); 55 | } 56 | } catch (Exception e) { 57 | // Code... 58 | } 59 | 60 | // Options. 61 | int size = optionArray.size(); 62 | for (int i = 0; i < size; i++) { 63 | builder.sheet(i, optionArray.getString(i)); 64 | } 65 | 66 | builder.listener(new DialogInterface.OnClickListener() { 67 | @Override 68 | public void onClick(DialogInterface dialog, int which) { 69 | dialog.dismiss(); 70 | onSelect.invoke(which); 71 | } 72 | }); 73 | 74 | builder.setOnDismissListener(new DialogInterface.OnDismissListener() { 75 | @Override 76 | public void onDismiss(DialogInterface dialog) { 77 | RNBottomSheet.this.isOpened = false; 78 | } 79 | }); 80 | 81 | builder.build().show(); 82 | } 83 | 84 | @ReactMethod 85 | public void showShareBottomSheetWithOptions(ReadableMap options, Callback failureCallback, Callback successCallback) { 86 | String url = options.getString("url"); 87 | String message = options.getString("message"); 88 | String subject = options.getString("subject"); 89 | String title = options.getString("subject"); 90 | if (title == null) { 91 | title = ""; 92 | } 93 | 94 | List items = new ArrayList<>(); 95 | if (message != null && !message.isEmpty()) { 96 | items.add(message); 97 | } 98 | 99 | final Intent shareIntent = new Intent(); 100 | shareIntent.setAction(Intent.ACTION_SEND); 101 | Uri uri = Uri.parse(url); 102 | if (uri != null) { 103 | if (uri.getScheme() != null && "data".equals(uri.getScheme().toLowerCase())) { 104 | shareIntent.setType("*/*"); 105 | shareIntent.putExtra(Intent.EXTRA_STREAM, uri); 106 | } else { 107 | shareIntent.setType("text/plain"); 108 | shareIntent.putExtra(Intent.EXTRA_EMAIL, url); 109 | shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 110 | shareIntent.putExtra(Intent.EXTRA_TEXT, message); 111 | } 112 | } 113 | 114 | if (shareIntent.resolveActivity(this.getCurrentActivity().getPackageManager()) != null) { 115 | this.getCurrentActivity().startActivity(Intent.createChooser(shareIntent, title)); 116 | } else { 117 | failureCallback.invoke(new Exception("The app you want to share is not installed.")); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /android/src/main/java/com/clipsub/rnbottomsheet/RNBottomSheetPackage.java: -------------------------------------------------------------------------------- 1 | package com.clipsub.rnbottomsheet; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import javax.annotation.Nonnull; 15 | 16 | public class RNBottomSheetPackage implements ReactPackage { 17 | public RNBottomSheetPackage() { 18 | super(); 19 | } 20 | 21 | @Override 22 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) { 23 | List modules = new ArrayList<>(); 24 | modules.add(new RNBottomSheet(reactContext)); 25 | 26 | return modules; 27 | } 28 | 29 | @Override 30 | public List createViewManagers(@Nonnull ReactApplicationContext reactContext) { 31 | return Collections.emptyList(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/ActionMenu.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.cocosw.bottomsheet; 18 | 19 | import android.content.ComponentName; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.pm.PackageManager; 23 | import android.content.pm.ResolveInfo; 24 | import android.view.KeyEvent; 25 | import android.view.MenuItem; 26 | import android.view.SubMenu; 27 | 28 | import java.util.ArrayList; 29 | import java.util.Iterator; 30 | import java.util.List; 31 | 32 | class ActionMenu implements android.view.Menu { 33 | private static final int[] sCategoryToOrder = new int[]{ 34 | 1, /* No category */ 35 | 4, /* CONTAINER */ 36 | 5, /* SYSTEM */ 37 | 3, /* SECONDARY */ 38 | 2, /* ALTERNATIVE */ 39 | 0, /* SELECTED_ALTERNATIVE */ 40 | }; 41 | /** 42 | * This is the part of an order integer that the user can provide. 43 | */ 44 | private static final int USER_MASK = 0x0000ffff; 45 | /** 46 | * Bit shift of the user portion of the order integer. 47 | */ 48 | private static final int USER_SHIFT = 0; 49 | /** 50 | * This is the part of an order integer that supplies the category of the item. 51 | */ 52 | private static final int CATEGORY_MASK = 0xffff0000; 53 | /** 54 | * Bit shift of the category portion of the order integer. 55 | */ 56 | private static final int CATEGORY_SHIFT = 16; 57 | private final Context mContext; 58 | private boolean mIsQwerty; 59 | private ArrayList mItems; 60 | 61 | ActionMenu(Context context) { 62 | mContext = context; 63 | mItems = new ArrayList<>(); 64 | } 65 | 66 | private static int findInsertIndex(ArrayList items, int ordering) { 67 | for (int i = items.size() - 1; i >= 0; i--) { 68 | ActionMenuItem item = items.get(i); 69 | if (item.getOrder() <= ordering) { 70 | return i + 1; 71 | } 72 | } 73 | return 0; 74 | } 75 | 76 | /** 77 | * Returns the ordering across all items. This will grab the category from 78 | * the upper bits, find out how to order the category with respect to other 79 | * categories, and combine it with the lower bits. 80 | * 81 | * @param categoryOrder The category order for a particular item (if it has 82 | * not been or/add with a category, the default category is 83 | * assumed). 84 | * @return An ordering integer that can be used to order this item across 85 | * all the items (even from other categories). 86 | */ 87 | private static int getOrdering(int categoryOrder) { 88 | final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; 89 | 90 | if (index < 0 || index >= sCategoryToOrder.length) { 91 | throw new IllegalArgumentException("order does not contain a valid category."); 92 | } 93 | 94 | return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); 95 | } 96 | 97 | public Context getContext() { 98 | return mContext; 99 | } 100 | 101 | public MenuItem add(CharSequence title) { 102 | return add(0, 0, 0, title); 103 | } 104 | 105 | public MenuItem add(int titleRes) { 106 | return add(0, 0, 0, titleRes); 107 | } 108 | 109 | public MenuItem add(int groupId, int itemId, int order, int titleRes) { 110 | return add(groupId, itemId, order, mContext.getResources().getString(titleRes)); 111 | } 112 | 113 | public MenuItem add(int groupId, int itemId, int order, CharSequence title) { 114 | ActionMenuItem item = new ActionMenuItem(getContext(), 115 | groupId, itemId, 0, order, title); 116 | mItems.add(findInsertIndex(mItems, getOrdering(order)), item); 117 | return item; 118 | } 119 | 120 | MenuItem add(ActionMenuItem item) { 121 | mItems.add(findInsertIndex(mItems, getOrdering(item.getOrder())), item); 122 | return item; 123 | } 124 | 125 | public int addIntentOptions(int groupId, int itemId, int order, 126 | ComponentName caller, Intent[] specifics, Intent intent, int flags, 127 | MenuItem[] outSpecificItems) { 128 | PackageManager pm = mContext.getPackageManager(); 129 | final List lri = 130 | pm.queryIntentActivityOptions(caller, specifics, intent, 0); 131 | final int N = lri != null ? lri.size() : 0; 132 | 133 | if ((flags & FLAG_APPEND_TO_GROUP) == 0) { 134 | removeGroup(groupId); 135 | } 136 | 137 | for (int i = 0; i < N; i++) { 138 | final ResolveInfo ri = lri.get(i); 139 | Intent rintent = new Intent( 140 | ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); 141 | rintent.setComponent(new ComponentName( 142 | ri.activityInfo.applicationInfo.packageName, 143 | ri.activityInfo.name)); 144 | final MenuItem item = add(groupId, itemId, order, ri.loadLabel(pm)) 145 | .setIcon(ri.loadIcon(pm)) 146 | .setIntent(rintent); 147 | if (outSpecificItems != null && ri.specificIndex >= 0) { 148 | outSpecificItems[ri.specificIndex] = item; 149 | } 150 | } 151 | 152 | return N; 153 | } 154 | 155 | public SubMenu addSubMenu(CharSequence title) { 156 | // TODO Implement submenus 157 | return null; 158 | } 159 | 160 | public SubMenu addSubMenu(int titleRes) { 161 | // TODO Implement submenus 162 | return null; 163 | } 164 | 165 | public SubMenu addSubMenu(int groupId, int itemId, int order, 166 | CharSequence title) { 167 | // TODO Implement submenus 168 | return null; 169 | } 170 | 171 | public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) { 172 | // TODO Implement submenus 173 | return null; 174 | } 175 | 176 | public void clear() { 177 | mItems.clear(); 178 | } 179 | 180 | public void close() { 181 | } 182 | 183 | private int findItemIndex(int id) { 184 | final ArrayList items = mItems; 185 | final int itemCount = items.size(); 186 | for (int i = 0; i < itemCount; i++) { 187 | if (items.get(i).getItemId() == id) { 188 | return i; 189 | } 190 | } 191 | return -1; 192 | } 193 | 194 | public MenuItem findItem(int id) { 195 | final int index = findItemIndex(id); 196 | if (index < 0) { 197 | return null; 198 | } 199 | 200 | return mItems.get(index); 201 | } 202 | 203 | public MenuItem getItem(int index) { 204 | return mItems.get(index); 205 | } 206 | 207 | public boolean hasVisibleItems() { 208 | final ArrayList items = mItems; 209 | final int itemCount = items.size(); 210 | 211 | for (int i = 0; i < itemCount; i++) { 212 | if (items.get(i).isVisible()) { 213 | return true; 214 | } 215 | } 216 | 217 | return false; 218 | } 219 | 220 | private ActionMenuItem findItemWithShortcut(int keyCode, KeyEvent event) { 221 | // TODO Make this smarter. 222 | final boolean qwerty = mIsQwerty; 223 | final ArrayList items = mItems; 224 | final int itemCount = items.size(); 225 | 226 | for (int i = 0; i < itemCount; i++) { 227 | ActionMenuItem item = items.get(i); 228 | final char shortcut = qwerty ? item.getAlphabeticShortcut() : 229 | item.getNumericShortcut(); 230 | if (keyCode == shortcut) { 231 | return item; 232 | } 233 | } 234 | return null; 235 | } 236 | 237 | public boolean isShortcutKey(int keyCode, KeyEvent event) { 238 | return findItemWithShortcut(keyCode, event) != null; 239 | } 240 | 241 | public boolean performIdentifierAction(int id, int flags) { 242 | final int index = findItemIndex(id); 243 | if (index < 0) { 244 | return false; 245 | } 246 | 247 | return mItems.get(index).invoke(); 248 | } 249 | 250 | public boolean performShortcut(int keyCode, KeyEvent event, int flags) { 251 | ActionMenuItem item = findItemWithShortcut(keyCode, event); 252 | if (item == null) { 253 | return false; 254 | } 255 | 256 | return item.invoke(); 257 | } 258 | 259 | public void removeGroup(int groupId) { 260 | final ArrayList items = mItems; 261 | int itemCount = items.size(); 262 | int i = 0; 263 | while (i < itemCount) { 264 | if (items.get(i).getGroupId() == groupId) { 265 | items.remove(i); 266 | itemCount--; 267 | } else { 268 | i++; 269 | } 270 | } 271 | } 272 | 273 | public void removeItem(int id) { 274 | final int index = findItemIndex(id); 275 | if (index < 0) { 276 | return; 277 | } 278 | 279 | mItems.remove(index); 280 | } 281 | 282 | public void setGroupCheckable(int group, boolean checkable, 283 | boolean exclusive) { 284 | final ArrayList items = mItems; 285 | final int itemCount = items.size(); 286 | 287 | for (int i = 0; i < itemCount; i++) { 288 | ActionMenuItem item = items.get(i); 289 | if (item.getGroupId() == group) { 290 | item.setCheckable(checkable); 291 | item.setExclusiveCheckable(exclusive); 292 | } 293 | } 294 | } 295 | 296 | public void setGroupEnabled(int group, boolean enabled) { 297 | final ArrayList items = mItems; 298 | final int itemCount = items.size(); 299 | 300 | for (int i = 0; i < itemCount; i++) { 301 | ActionMenuItem item = items.get(i); 302 | if (item.getGroupId() == group) { 303 | item.setEnabled(enabled); 304 | } 305 | } 306 | } 307 | 308 | public void setGroupVisible(int group, boolean visible) { 309 | final ArrayList items = mItems; 310 | final int itemCount = items.size(); 311 | 312 | for (int i = 0; i < itemCount; i++) { 313 | ActionMenuItem item = items.get(i); 314 | if (item.getGroupId() == group) { 315 | item.setVisible(visible); 316 | } 317 | } 318 | } 319 | 320 | public void setQwertyMode(boolean isQwerty) { 321 | mIsQwerty = isQwerty; 322 | } 323 | 324 | public int size() { 325 | return mItems.size(); 326 | } 327 | 328 | ActionMenu clone(int size) { 329 | ActionMenu out = new ActionMenu(getContext()); 330 | out.mItems = new ArrayList<>(this.mItems.subList(0, size)); 331 | return out; 332 | } 333 | 334 | void removeInvisible() { 335 | Iterator iter = mItems.iterator(); 336 | while (iter.hasNext()) { 337 | ActionMenuItem item = iter.next(); 338 | if (!item.isVisible()) iter.remove(); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/ActionMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | /* 4 | * Copyright (C) 2010 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.content.res.ColorStateList; 22 | import android.graphics.PorterDuff; 23 | import android.graphics.drawable.Drawable; 24 | import android.view.ContextMenu; 25 | import android.view.MenuItem; 26 | import android.view.SubMenu; 27 | import android.view.View; 28 | 29 | import androidx.core.content.ContextCompat; 30 | 31 | 32 | class ActionMenuItem implements MenuItem { 33 | 34 | 35 | private static final int NO_ICON = 0; 36 | private static final int CHECKABLE = 0x00000001; 37 | private static final int CHECKED = 0x00000002; 38 | private static final int EXCLUSIVE = 0x00000004; 39 | private static final int HIDDEN = 0x00000008; 40 | private static final int ENABLED = 0x00000010; 41 | private final int mId; 42 | private final int mGroup; 43 | private final int mCategoryOrder; 44 | private final int mOrdering; 45 | private CharSequence mTitle; 46 | private CharSequence mTitleCondensed; 47 | private Intent mIntent; 48 | private char mShortcutNumericChar; 49 | private char mShortcutAlphabeticChar; 50 | private Drawable mIconDrawable; 51 | private int mIconResId = NO_ICON; 52 | private Context mContext; 53 | private MenuItem.OnMenuItemClickListener mClickListener; 54 | private int mFlags = ENABLED; 55 | 56 | public ActionMenuItem(Context context, int group, int id, int categoryOrder, int ordering, 57 | CharSequence title) { 58 | mContext = context; 59 | mId = id; 60 | mGroup = group; 61 | mCategoryOrder = categoryOrder; 62 | mOrdering = ordering; 63 | mTitle = title; 64 | } 65 | 66 | public char getAlphabeticShortcut() { 67 | return mShortcutAlphabeticChar; 68 | } 69 | 70 | public int getGroupId() { 71 | return mGroup; 72 | } 73 | 74 | public Drawable getIcon() { 75 | return mIconDrawable; 76 | } 77 | 78 | public Intent getIntent() { 79 | return mIntent; 80 | } 81 | 82 | public int getItemId() { 83 | return mId; 84 | } 85 | 86 | public ContextMenu.ContextMenuInfo getMenuInfo() { 87 | return null; 88 | } 89 | 90 | public char getNumericShortcut() { 91 | return mShortcutNumericChar; 92 | } 93 | 94 | public int getOrder() { 95 | return mOrdering; 96 | } 97 | 98 | public SubMenu getSubMenu() { 99 | return null; 100 | } 101 | 102 | public CharSequence getTitle() { 103 | return mTitle; 104 | } 105 | 106 | public CharSequence getTitleCondensed() { 107 | return mTitleCondensed != null ? mTitleCondensed : mTitle; 108 | } 109 | 110 | public boolean hasSubMenu() { 111 | return false; 112 | } 113 | 114 | public boolean isCheckable() { 115 | return (mFlags & CHECKABLE) != 0; 116 | } 117 | 118 | public boolean isChecked() { 119 | return (mFlags & CHECKED) != 0; 120 | } 121 | 122 | public boolean isEnabled() { 123 | return (mFlags & ENABLED) != 0; 124 | } 125 | 126 | public boolean isVisible() { 127 | return (mFlags & HIDDEN) == 0; 128 | } 129 | 130 | public MenuItem setAlphabeticShortcut(char alphaChar) { 131 | mShortcutAlphabeticChar = alphaChar; 132 | return this; 133 | } 134 | 135 | public MenuItem setCheckable(boolean checkable) { 136 | mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); 137 | return this; 138 | } 139 | 140 | public ActionMenuItem setExclusiveCheckable(boolean exclusive) { 141 | mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); 142 | return this; 143 | } 144 | 145 | public MenuItem setChecked(boolean checked) { 146 | mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); 147 | return this; 148 | } 149 | 150 | public MenuItem setEnabled(boolean enabled) { 151 | mFlags = (mFlags & ~ENABLED) | (enabled ? ENABLED : 0); 152 | return this; 153 | } 154 | 155 | public MenuItem setIcon(Drawable icon) { 156 | mIconDrawable = icon; 157 | mIconResId = NO_ICON; 158 | return this; 159 | } 160 | 161 | public MenuItem setIcon(int iconRes) { 162 | mIconResId = iconRes; 163 | if (iconRes>0) 164 | mIconDrawable = ContextCompat.getDrawable(mContext, iconRes); 165 | return this; 166 | } 167 | 168 | public MenuItem setIntent(Intent intent) { 169 | mIntent = intent; 170 | return this; 171 | } 172 | 173 | public MenuItem setNumericShortcut(char numericChar) { 174 | mShortcutNumericChar = numericChar; 175 | return this; 176 | } 177 | 178 | public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) { 179 | mClickListener = menuItemClickListener; 180 | return this; 181 | } 182 | 183 | public MenuItem setShortcut(char numericChar, char alphaChar) { 184 | mShortcutNumericChar = numericChar; 185 | mShortcutAlphabeticChar = alphaChar; 186 | return this; 187 | } 188 | 189 | public MenuItem setTitle(CharSequence title) { 190 | mTitle = title; 191 | return this; 192 | } 193 | 194 | public MenuItem setTitle(int title) { 195 | mTitle = mContext.getResources().getString(title); 196 | return this; 197 | } 198 | 199 | public MenuItem setTitleCondensed(CharSequence title) { 200 | mTitleCondensed = title; 201 | return this; 202 | } 203 | 204 | public MenuItem setVisible(boolean visible) { 205 | mFlags = (mFlags & ~HIDDEN) | (visible ? 0 : HIDDEN); 206 | return this; 207 | } 208 | 209 | public boolean invoke() { 210 | if (mClickListener != null && mClickListener.onMenuItemClick(this)) { 211 | return true; 212 | } 213 | 214 | if (mIntent != null) { 215 | mContext.startActivity(mIntent); 216 | return true; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | public void setShowAsAction(int show) { 223 | // Do nothing. ActionMenuItems always show as action buttons. 224 | } 225 | 226 | public MenuItem setActionView(View actionView) { 227 | throw new UnsupportedOperationException(); 228 | } 229 | 230 | public View getActionView() { 231 | return null; 232 | } 233 | 234 | @Override 235 | public MenuItem setActionProvider(android.view.ActionProvider actionProvider) { 236 | throw new UnsupportedOperationException(); 237 | } 238 | 239 | @Override 240 | public android.view.ActionProvider getActionProvider() { 241 | throw new UnsupportedOperationException(); 242 | } 243 | 244 | @Override 245 | public MenuItem setActionView(int resId) { 246 | throw new UnsupportedOperationException(); 247 | } 248 | 249 | 250 | @Override 251 | public MenuItem setShowAsActionFlags(int actionEnum) { 252 | setShowAsAction(actionEnum); 253 | return this; 254 | } 255 | 256 | @Override 257 | public boolean expandActionView() { 258 | return false; 259 | } 260 | 261 | @Override 262 | public boolean collapseActionView() { 263 | return false; 264 | } 265 | 266 | @Override 267 | public boolean isActionViewExpanded() { 268 | return false; 269 | } 270 | 271 | @Override 272 | public MenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) { 273 | throw new UnsupportedOperationException(); 274 | } 275 | 276 | @Override 277 | public MenuItem setContentDescription(CharSequence contentDescription) { 278 | return this; 279 | } 280 | 281 | @Override 282 | public CharSequence getContentDescription() { 283 | return null; 284 | } 285 | 286 | @Override 287 | public MenuItem setTooltipText(CharSequence tooltipText) { 288 | return this; 289 | } 290 | 291 | @Override 292 | public CharSequence getTooltipText() { 293 | return null; 294 | } 295 | 296 | @Override 297 | public MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers, int alphaModifiers) { 298 | return this; 299 | } 300 | 301 | @Override 302 | public MenuItem setNumericShortcut(char numericChar, int numericModifiers) { 303 | return this; 304 | } 305 | 306 | @Override 307 | public int getNumericModifiers() { 308 | return 0; 309 | } 310 | 311 | @Override 312 | public MenuItem setAlphabeticShortcut(char alphaChar, int alphaModifiers) { 313 | return this; 314 | } 315 | 316 | @Override 317 | public int getAlphabeticModifiers() { 318 | return 0; 319 | } 320 | 321 | @Override 322 | public MenuItem setIconTintList(ColorStateList tint) { 323 | return this; 324 | } 325 | 326 | @Override 327 | public ColorStateList getIconTintList() { 328 | return null; 329 | } 330 | 331 | @Override 332 | public MenuItem setIconTintMode(PorterDuff.Mode tintMode) { 333 | return this; 334 | } 335 | 336 | @Override 337 | public PorterDuff.Mode getIconTintMode() { 338 | return null; 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/BottomSheet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011, 2015 Kai Liao 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.cocosw.bottomsheet; 18 | 19 | import android.annotation.SuppressLint; 20 | import android.app.Activity; 21 | import android.app.Dialog; 22 | import android.content.Context; 23 | import android.content.DialogInterface; 24 | import android.content.res.TypedArray; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import android.transition.ChangeBounds; 29 | import android.transition.Transition; 30 | import android.transition.TransitionManager; 31 | import android.util.SparseIntArray; 32 | import android.view.Gravity; 33 | import android.view.LayoutInflater; 34 | import android.view.Menu; 35 | import android.view.MenuInflater; 36 | import android.view.MenuItem; 37 | import android.view.View; 38 | import android.view.ViewGroup; 39 | import android.view.ViewTreeObserver; 40 | import android.view.WindowManager; 41 | import android.widget.AdapterView; 42 | import android.widget.BaseAdapter; 43 | import android.widget.GridView; 44 | import android.widget.ImageView; 45 | import android.widget.LinearLayout; 46 | import android.widget.TextView; 47 | 48 | import androidx.annotation.DrawableRes; 49 | import androidx.annotation.IntegerRes; 50 | import androidx.annotation.MenuRes; 51 | import androidx.annotation.NonNull; 52 | import androidx.annotation.StringRes; 53 | import androidx.annotation.StyleRes; 54 | 55 | import com.clipsub.rnbottomsheet.R; 56 | 57 | import java.lang.reflect.Field; 58 | import java.util.ArrayList; 59 | 60 | 61 | /** 62 | * One way to present a set of actions to a user is with bottom sheets, a sheet of paper that slides up from the bottom edge of the screen. Bottom sheets offer flexibility in the display of clear and simple actions that do not need explanation. 63 | * https://www.google.com/design/spec/components/bottom-sheets.html 64 | * 65 | * Project: BottomSheet 66 | * Created by Kai Liao on 2014/9/21. 67 | */ 68 | @SuppressWarnings("unused") 69 | public class BottomSheet extends Dialog implements DialogInterface { 70 | 71 | private final SparseIntArray hidden = new SparseIntArray(); 72 | 73 | private TranslucentHelper helper; 74 | private String moreText; 75 | private Drawable close; 76 | private Drawable more; 77 | private int mHeaderLayoutId; 78 | private int mListItemLayoutId; 79 | private int mGridItemLayoutId; 80 | 81 | private boolean collapseListIcons; 82 | private GridView list; 83 | private SimpleSectionedGridAdapter adapter; 84 | private Builder builder; 85 | private ImageView icon; 86 | 87 | private int limit = -1; 88 | private boolean cancelOnTouchOutside = true; 89 | private boolean cancelOnSwipeDown = true; 90 | private ActionMenu fullMenuItem; 91 | private ActionMenu menuItem; 92 | private ActionMenu actions; 93 | private OnDismissListener dismissListener; 94 | private OnShowListener showListener; 95 | 96 | BottomSheet(Context context) { 97 | super(context, R.style.BottomSheet_Dialog); 98 | } 99 | 100 | @SuppressWarnings("WeakerAccess") 101 | BottomSheet(Context context, int theme) { 102 | super(context, theme); 103 | 104 | TypedArray a = getContext() 105 | .obtainStyledAttributes(null, R.styleable.BottomSheet, R.attr.bs_bottomSheetStyle, 0); 106 | try { 107 | more = a.getDrawable(R.styleable.BottomSheet_bs_moreDrawable); 108 | close = a.getDrawable(R.styleable.BottomSheet_bs_closeDrawable); 109 | moreText = a.getString(R.styleable.BottomSheet_bs_moreText); 110 | collapseListIcons = a.getBoolean(R.styleable.BottomSheet_bs_collapseListIcons, true); 111 | mHeaderLayoutId = a.getResourceId(R.styleable.BottomSheet_bs_headerLayout, R.layout.bs_header); 112 | mListItemLayoutId = a.getResourceId(R.styleable.BottomSheet_bs_listItemLayout, R.layout.bs_list_entry); 113 | mGridItemLayoutId = a.getResourceId(R.styleable.BottomSheet_bs_gridItemLayout, R.layout.bs_grid_entry); 114 | } finally { 115 | a.recycle(); 116 | } 117 | 118 | // https://github.com/jgilfelt/SystemBarTint/blob/master/library/src/com/readystatesoftware/systembartint/SystemBarTintManager.java 119 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 120 | helper = new TranslucentHelper(this, context); 121 | } 122 | } 123 | 124 | /** 125 | * Hacky way to get gridview's column number 126 | */ 127 | private int getNumColumns() { 128 | try { 129 | Field numColumns = GridView.class.getDeclaredField("mRequestedNumColumns"); 130 | numColumns.setAccessible(true); 131 | return numColumns.getInt(list); 132 | } catch (Exception e) { 133 | return 1; 134 | } 135 | } 136 | 137 | @Override 138 | public void setCanceledOnTouchOutside(boolean cancel) { 139 | super.setCanceledOnTouchOutside(cancel); 140 | cancelOnTouchOutside = cancel; 141 | } 142 | 143 | /** 144 | * Sets whether this dialog is canceled when swipe it down 145 | * 146 | * @param cancel whether this dialog is canceled when swipe it down 147 | */ 148 | public void setCanceledOnSwipeDown(boolean cancel) { 149 | cancelOnSwipeDown = cancel; 150 | } 151 | 152 | @Override 153 | public void setOnShowListener(OnShowListener listener) { 154 | this.showListener = listener; 155 | } 156 | 157 | private void init(final Context context) { 158 | setCanceledOnTouchOutside(cancelOnTouchOutside); 159 | final ClosableSlidingLayout mDialogView = (ClosableSlidingLayout) View.inflate(context, R.layout.bottom_sheet_dialog, null); 160 | LinearLayout mainLayout = mDialogView.findViewById(R.id.bs_main); 161 | mainLayout.addView(View.inflate(context, mHeaderLayoutId, null), 0); 162 | setContentView(mDialogView); 163 | if (!cancelOnSwipeDown) 164 | mDialogView.swipeable = cancelOnSwipeDown; 165 | mDialogView.setSlideListener(new ClosableSlidingLayout.SlideListener() { 166 | @Override 167 | public void onClosed() { 168 | BottomSheet.this.dismiss(); 169 | } 170 | 171 | @Override 172 | public void onOpened() { 173 | showFullItems(); 174 | } 175 | }); 176 | 177 | super.setOnShowListener(new OnShowListener() { 178 | @Override 179 | public void onShow(DialogInterface dialogInterface) { 180 | if (showListener != null) 181 | showListener.onShow(dialogInterface); 182 | list.setAdapter(adapter); 183 | list.startLayoutAnimation(); 184 | if (builder.icon == null) 185 | icon.setVisibility(View.GONE); 186 | else { 187 | icon.setVisibility(View.VISIBLE); 188 | icon.setImageDrawable(builder.icon); 189 | } 190 | } 191 | }); 192 | int[] location = new int[2]; 193 | mDialogView.getLocationOnScreen(location); 194 | 195 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 196 | mDialogView.setPadding(0, location[0] == 0 ? helper.mStatusBarHeight : 0, 0, 0); 197 | mDialogView.getChildAt(0).setPadding(0, 0, 0, helper.mNavBarAvailable ? helper.getNavigationBarHeight(getContext()) + mDialogView.getPaddingBottom() : 0); 198 | } 199 | 200 | final TextView title = mDialogView.findViewById(R.id.bottom_sheet_title); 201 | if (builder.title != null) { 202 | title.setVisibility(View.VISIBLE); 203 | title.setText(builder.title); 204 | } 205 | 206 | icon = mDialogView.findViewById(R.id.bottom_sheet_title_image); 207 | list = mDialogView.findViewById(R.id.bottom_sheet_gridview); 208 | mDialogView.mTarget = list; 209 | if (!builder.grid) { 210 | list.setNumColumns(1); 211 | } 212 | 213 | if (builder.grid) { 214 | for (int i = 0; i < getMenu().size(); i++) { 215 | if (getMenu().getItem(i).getIcon() == null) 216 | throw new IllegalArgumentException("You must set icon for each items in grid style"); 217 | } 218 | } 219 | 220 | if (builder.limit > 0) 221 | limit = builder.limit * getNumColumns(); 222 | else 223 | limit = Integer.MAX_VALUE; 224 | 225 | mDialogView.setCollapsible(false); 226 | 227 | actions = builder.menu; 228 | menuItem = actions; 229 | // over the initial numbers 230 | if (getMenu().size() > limit) { 231 | fullMenuItem = builder.menu; 232 | menuItem = builder.menu.clone(limit - 1); 233 | ActionMenuItem item = new ActionMenuItem(context, 0, R.id.bs_more, 0, limit - 1, moreText); 234 | item.setIcon(more); 235 | menuItem.add(item); 236 | actions = menuItem; 237 | mDialogView.setCollapsible(true); 238 | } 239 | 240 | BaseAdapter baseAdapter = new BaseAdapter() { 241 | 242 | @Override 243 | public int getCount() { 244 | return actions.size() - hidden.size(); 245 | } 246 | 247 | @Override 248 | public MenuItem getItem(int position) { 249 | return actions.getItem(position); 250 | } 251 | 252 | @Override 253 | public long getItemId(int position) { 254 | return position; 255 | } 256 | 257 | @Override 258 | public int getViewTypeCount() { 259 | return 1; 260 | } 261 | 262 | @Override 263 | public boolean isEnabled(int position) { 264 | return getItem(position).isEnabled(); 265 | } 266 | 267 | @Override 268 | public boolean areAllItemsEnabled() { 269 | return false; 270 | } 271 | 272 | @Override 273 | public View getView(int position, View convertView, ViewGroup parent) { 274 | ViewHolder holder; 275 | if (convertView == null) { 276 | LayoutInflater inflater = (LayoutInflater) getContext() 277 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 278 | if (builder.grid) 279 | convertView = inflater.inflate(mGridItemLayoutId, parent, false); 280 | else 281 | convertView = inflater.inflate(mListItemLayoutId, parent, false); 282 | holder = new ViewHolder(); 283 | holder.title = convertView.findViewById(R.id.bs_list_title); 284 | holder.image = convertView.findViewById(R.id.bs_list_image); 285 | convertView.setTag(holder); 286 | } else { 287 | holder = (ViewHolder) convertView.getTag(); 288 | } 289 | 290 | for (int i = 0; i < hidden.size(); i++) { 291 | if (hidden.valueAt(i) <= position) 292 | position++; 293 | } 294 | 295 | MenuItem item = getItem(position); 296 | 297 | holder.title.setText(item.getTitle()); 298 | if (item.getIcon() == null) 299 | holder.image.setVisibility(collapseListIcons ? View.GONE : View.INVISIBLE); 300 | else { 301 | holder.image.setVisibility(View.VISIBLE); 302 | holder.image.setImageDrawable(item.getIcon()); 303 | } 304 | 305 | holder.image.setEnabled(item.isEnabled()); 306 | holder.title.setEnabled(item.isEnabled()); 307 | 308 | return convertView; 309 | } 310 | 311 | class ViewHolder { 312 | private TextView title; 313 | private ImageView image; 314 | } 315 | }; 316 | 317 | adapter = new SimpleSectionedGridAdapter(context, baseAdapter, R.layout.bs_list_divider, R.id.headerlayout, R.id.header); 318 | list.setAdapter(adapter); 319 | adapter.setGridView(list); 320 | 321 | list.setOnItemClickListener(new AdapterView.OnItemClickListener() { 322 | @Override 323 | public void onItemClick(AdapterView parent, View view, int position, long id) { 324 | if (((MenuItem) adapter.getItem(position)).getItemId() == R.id.bs_more) { 325 | showFullItems(); 326 | mDialogView.setCollapsible(false); 327 | return; 328 | } 329 | 330 | if (!((ActionMenuItem) adapter.getItem(position)).invoke()) { 331 | if (builder.menulistener != null) 332 | builder.menulistener.onMenuItemClick((MenuItem) adapter.getItem(position)); 333 | else if (builder.listener != null) 334 | builder.listener.onClick(BottomSheet.this, ((MenuItem) adapter.getItem(position)).getItemId()); 335 | } 336 | dismiss(); 337 | } 338 | }); 339 | 340 | if (builder.dismissListener != null) { 341 | setOnDismissListener(builder.dismissListener); 342 | } 343 | setListLayout(); 344 | } 345 | 346 | 347 | private void updateSection() { 348 | actions.removeInvisible(); 349 | 350 | if (!builder.grid && actions.size() > 0) { 351 | int groupId = actions.getItem(0).getGroupId(); 352 | ArrayList sections = new ArrayList<>(); 353 | for (int i = 0; i < actions.size(); i++) { 354 | if (actions.getItem(i).getGroupId() != groupId) { 355 | groupId = actions.getItem(i).getGroupId(); 356 | sections.add(new SimpleSectionedGridAdapter.Section(i, null)); 357 | } 358 | } 359 | if (sections.size() > 0) { 360 | SimpleSectionedGridAdapter.Section[] s = new SimpleSectionedGridAdapter.Section[sections.size()]; 361 | sections.toArray(s); 362 | adapter.setSections(s); 363 | } else { 364 | adapter.mSections.clear(); 365 | } 366 | } 367 | } 368 | 369 | private void showFullItems() { 370 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 371 | Transition changeBounds = new ChangeBounds(); 372 | changeBounds.setDuration(300); 373 | TransitionManager.beginDelayedTransition(list, changeBounds); 374 | } 375 | actions = fullMenuItem; 376 | updateSection(); 377 | adapter.notifyDataSetChanged(); 378 | list.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); 379 | icon.setVisibility(View.VISIBLE); 380 | icon.setImageDrawable(close); 381 | icon.setOnClickListener(new View.OnClickListener() { 382 | @Override 383 | public void onClick(View v) { 384 | showShortItems(); 385 | } 386 | }); 387 | setListLayout(); 388 | } 389 | 390 | private void showShortItems() { 391 | actions = menuItem; 392 | updateSection(); 393 | adapter.notifyDataSetChanged(); 394 | setListLayout(); 395 | 396 | if (builder.icon == null) 397 | icon.setVisibility(View.GONE); 398 | else { 399 | icon.setVisibility(View.VISIBLE); 400 | icon.setImageDrawable(builder.icon); 401 | } 402 | } 403 | 404 | @Override 405 | protected void onStart() { 406 | super.onStart(); 407 | showShortItems(); 408 | } 409 | 410 | private boolean hasDivider() { 411 | return adapter.mSections.size() > 0; 412 | } 413 | 414 | private void setListLayout() { 415 | // without divider, the height of gridview is correct 416 | if (!hasDivider()) 417 | return; 418 | list.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 419 | @Override 420 | public void onGlobalLayout() { 421 | if (Build.VERSION.SDK_INT < 16) { 422 | //noinspection deprecation 423 | list.getViewTreeObserver().removeGlobalOnLayoutListener(this); 424 | } else { 425 | list.getViewTreeObserver().removeOnGlobalLayoutListener(this); 426 | } 427 | View lastChild = list.getChildAt(list.getChildCount() - 1); 428 | if (lastChild != null) 429 | list.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, lastChild.getBottom() + lastChild.getPaddingBottom() + list.getPaddingBottom())); 430 | } 431 | }); 432 | } 433 | 434 | 435 | @Override 436 | protected void onCreate(Bundle savedInstanceState) { 437 | super.onCreate(savedInstanceState); 438 | init(getContext()); 439 | 440 | WindowManager.LayoutParams params = getWindow().getAttributes(); 441 | params.height = ViewGroup.LayoutParams.WRAP_CONTENT; 442 | params.gravity = Gravity.BOTTOM; 443 | 444 | TypedArray a = getContext().obtainStyledAttributes(new int[]{android.R.attr.layout_width}); 445 | try { 446 | params.width = a.getLayoutDimension(0, ViewGroup.LayoutParams.MATCH_PARENT); 447 | } finally { 448 | a.recycle(); 449 | } 450 | super.setOnDismissListener(new OnDismissListener() { 451 | @Override 452 | public void onDismiss(DialogInterface dialog) { 453 | if (dismissListener != null) 454 | dismissListener.onDismiss(dialog); 455 | if (limit != Integer.MAX_VALUE) 456 | showShortItems(); 457 | } 458 | }); 459 | getWindow().setAttributes(params); 460 | } 461 | 462 | 463 | 464 | public Menu getMenu() { 465 | return builder.menu; 466 | } 467 | 468 | /** 469 | * If you make any changes to menu and try to apply it immediately to your bottomsheet, you should call this. 470 | */ 471 | public void invalidate() { 472 | updateSection(); 473 | adapter.notifyDataSetChanged(); 474 | setListLayout(); 475 | } 476 | 477 | @Override 478 | public void setOnDismissListener(OnDismissListener listener) { 479 | this.dismissListener = listener; 480 | } 481 | 482 | /** 483 | * Constructor using a context for this builder and the {@link com.cocosw.bottomsheet.BottomSheet} it creates. 484 | */ 485 | public static class Builder { 486 | 487 | private final Context context; 488 | private final ActionMenu menu; 489 | private int theme; 490 | private CharSequence title; 491 | private boolean grid; 492 | private OnClickListener listener; 493 | private OnDismissListener dismissListener; 494 | private Drawable icon; 495 | private int limit = -1; 496 | private MenuItem.OnMenuItemClickListener menulistener; 497 | 498 | 499 | /** 500 | * Constructor using a context for this builder and the {@link com.cocosw.bottomsheet.BottomSheet} it creates. 501 | * 502 | * @param context A Context for built BottomSheet. 503 | */ 504 | public Builder(@NonNull Activity context) { 505 | this(context, R.style.BottomSheet_Dialog); 506 | TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{R.attr.bs_bottomSheetStyle}); 507 | try { 508 | theme = ta.getResourceId(0, R.style.BottomSheet_Dialog); 509 | } finally { 510 | ta.recycle(); 511 | } 512 | } 513 | 514 | /** 515 | * Constructor using a context for this builder and the {@link com.cocosw.bottomsheet.BottomSheet} it creates with given style 516 | * 517 | * @param context A Context for built BottomSheet. 518 | * @param theme The theme id will be apply to BottomSheet 519 | */ 520 | public Builder(Context context, @StyleRes int theme) { 521 | this.context = context; 522 | this.theme = theme; 523 | this.menu = new ActionMenu(context); 524 | } 525 | 526 | /** 527 | * Set menu resources as list item to display in BottomSheet 528 | * 529 | * @param xmlRes menu resource id 530 | * @return This Builder object to allow for chaining of calls to set methods 531 | */ 532 | public Builder sheet(@MenuRes int xmlRes) { 533 | new MenuInflater(context).inflate(xmlRes, menu); 534 | return this; 535 | } 536 | 537 | 538 | /** 539 | * Add one item into BottomSheet 540 | * 541 | * @param id ID of item 542 | * @param iconRes icon resource 543 | * @param textRes text resource 544 | * @return This Builder object to allow for chaining of calls to set methods 545 | */ 546 | public Builder sheet(int id, @DrawableRes int iconRes, @StringRes int textRes) { 547 | ActionMenuItem item = new ActionMenuItem(context, 0, id, 0, 0, context.getText(textRes)); 548 | item.setIcon(iconRes); 549 | menu.add(item); 550 | return this; 551 | } 552 | 553 | /** 554 | * Add one item into BottomSheet 555 | * 556 | * @param id ID of item 557 | * @param icon icon 558 | * @param text text 559 | * @return This Builder object to allow for chaining of calls to set methods 560 | */ 561 | public Builder sheet(int id, @NonNull Drawable icon, @NonNull CharSequence text) { 562 | ActionMenuItem item = new ActionMenuItem(context, 0, id, 0, 0, text); 563 | item.setIcon(icon); 564 | menu.add(item); 565 | return this; 566 | } 567 | 568 | /** 569 | * Add one item without icon into BottomSheet 570 | * 571 | * @param id ID of item 572 | * @param textRes text resource 573 | * @return This Builder object to allow for chaining of calls to set methods 574 | */ 575 | public Builder sheet(int id, @StringRes int textRes) { 576 | menu.add(0, id, 0, textRes); 577 | return this; 578 | } 579 | 580 | /** 581 | * Add one item without icon into BottomSheet 582 | * 583 | * @param id ID of item 584 | * @param text text 585 | * @return This Builder object to allow for chaining of calls to set methods 586 | */ 587 | public Builder sheet(int id, @NonNull CharSequence text) { 588 | menu.add(0, id, 0, text); 589 | return this; 590 | } 591 | 592 | /** 593 | * Set title for BottomSheet 594 | * 595 | * @param titleRes title for BottomSheet 596 | * @return This Builder object to allow for chaining of calls to set methods 597 | */ 598 | public Builder title(@StringRes int titleRes) { 599 | title = context.getText(titleRes); 600 | return this; 601 | } 602 | 603 | /** 604 | * Remove an item from BottomSheet 605 | * 606 | * @param id ID of item 607 | * @return This Builder object to allow for chaining of calls to set methods 608 | */ 609 | @Deprecated 610 | public Builder remove(int id) { 611 | menu.removeItem(id); 612 | return this; 613 | } 614 | 615 | /** 616 | * Set title for BottomSheet 617 | * 618 | * @param icon icon for BottomSheet 619 | * @return This Builder object to allow for chaining of calls to set methods 620 | */ 621 | public Builder icon(Drawable icon) { 622 | this.icon = icon; 623 | return this; 624 | } 625 | 626 | /** 627 | * Set title for BottomSheet 628 | * 629 | * @param iconRes icon resource id for BottomSheet 630 | * @return This Builder object to allow for chaining of calls to set methods 631 | */ 632 | public Builder icon(@DrawableRes int iconRes) { 633 | this.icon = context.getResources().getDrawable(iconRes); 634 | return this; 635 | } 636 | 637 | /** 638 | * Set OnclickListener for BottomSheet 639 | * 640 | * @param listener OnclickListener for BottomSheet 641 | * @return This Builder object to allow for chaining of calls to set methods 642 | */ 643 | public Builder listener(@NonNull OnClickListener listener) { 644 | this.listener = listener; 645 | return this; 646 | } 647 | 648 | /** 649 | * Set OnMenuItemClickListener for BottomSheet 650 | * 651 | * @param listener OnMenuItemClickListener for BottomSheet 652 | * @return This Builder object to allow for chaining of calls to set methods 653 | */ 654 | public Builder listener(@NonNull MenuItem.OnMenuItemClickListener listener) { 655 | this.menulistener = listener; 656 | return this; 657 | } 658 | 659 | 660 | /** 661 | * Show BottomSheet in dark color theme looking 662 | * 663 | * @return This Builder object to allow for chaining of calls to set methods 664 | */ 665 | public Builder darkTheme() { 666 | theme = R.style.BottomSheet_Dialog_Dark; 667 | return this; 668 | } 669 | 670 | 671 | /** 672 | * Show BottomSheet 673 | * 674 | * @return Instance of bottomsheet 675 | */ 676 | public BottomSheet show() { 677 | BottomSheet dialog = build(); 678 | dialog.show(); 679 | return dialog; 680 | } 681 | 682 | /** 683 | * Show items in grid instead of list 684 | * 685 | * @return This Builder object to allow for chaining of calls to set methods 686 | */ 687 | public Builder grid() { 688 | this.grid = true; 689 | return this; 690 | } 691 | 692 | /** 693 | * Set initial number of actions which will be shown in current sheet. 694 | * If more actions need to be shown, a "more" action will be displayed in the last position. 695 | * 696 | * @param limitRes resource id for initial number of actions 697 | * @return This Builder object to allow for chaining of calls to set methods 698 | */ 699 | public Builder limit(@IntegerRes int limitRes) { 700 | limit = context.getResources().getInteger(limitRes); 701 | return this; 702 | } 703 | 704 | 705 | /** 706 | * Create a BottomSheet but not show it 707 | * 708 | * @return Instance of bottomsheet 709 | */ 710 | @SuppressLint("Override") 711 | public BottomSheet build() { 712 | BottomSheet dialog = new BottomSheet(context, theme); 713 | dialog.builder = this; 714 | return dialog; 715 | } 716 | 717 | /** 718 | * Set title for BottomSheet 719 | * 720 | * @param title title for BottomSheet 721 | * @return This Builder object to allow for chaining of calls to set methods 722 | */ 723 | public Builder title(CharSequence title) { 724 | this.title = title; 725 | return this; 726 | } 727 | 728 | /** 729 | * Set the OnDismissListener for BottomSheet 730 | * 731 | * @param listener OnDismissListener for Bottom 732 | * @return This Builder object to allow for chaining of calls to set methods 733 | */ 734 | public Builder setOnDismissListener(@NonNull OnDismissListener listener) { 735 | this.dismissListener = listener; 736 | return this; 737 | } 738 | } 739 | 740 | 741 | } 742 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/BottomSheetHelper.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.content.pm.ActivityInfo; 8 | import android.content.pm.PackageManager; 9 | import android.content.pm.ResolveInfo; 10 | 11 | import androidx.annotation.NonNull; 12 | 13 | import com.clipsub.rnbottomsheet.R; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * A helper class, 19 | * 20 | * Project: BottomSheet 21 | * Created by LiaoKai(soarcn) on 2015/7/18. 22 | */ 23 | public class BottomSheetHelper { 24 | 25 | 26 | /** 27 | * Create a BottomSheet Builder for creating share intent chooser. 28 | * You still need to call show() to display it like: 29 | * 30 | * Intent sharingIntent = new Intent(Intent.ACTION_SEND); 31 | * shareIntent.setType("text/plain"); 32 | * shareIntent.putExtra(Intent.EXTRA_TEXT, "hello"); 33 | * BottomSheetHelper.shareAction(activity,sharingIntent).show(); 34 | * 35 | * @param activity Activity instance 36 | * @param intent shareIntent 37 | * @return BottomSheet builder 38 | */ 39 | public static BottomSheet.Builder shareAction(@NonNull final Activity activity, @NonNull final Intent intent) { 40 | BottomSheet.Builder builder = new BottomSheet.Builder(activity).grid(); 41 | PackageManager pm = activity.getPackageManager(); 42 | 43 | final List list = pm.queryIntentActivities(intent, 0); 44 | 45 | for (int i = 0; i < list.size(); i++) { 46 | builder.sheet(i, list.get(i).loadIcon(pm), list.get(i).loadLabel(pm)); 47 | } 48 | 49 | builder.listener(new DialogInterface.OnClickListener() { 50 | @Override 51 | public void onClick(@NonNull DialogInterface dialog, int which) { 52 | ActivityInfo activityInfo = list.get(which).activityInfo; 53 | ComponentName name = new ComponentName(activityInfo.applicationInfo.packageName, 54 | activityInfo.name); 55 | Intent newIntent = (Intent) intent.clone(); 56 | newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 57 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 58 | newIntent.setComponent(name); 59 | activity.startActivity(newIntent); 60 | } 61 | }); 62 | builder.limit(R.integer.bs_initial_grid_row); 63 | return builder; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/ClosableSlidingLayout.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.widget.FrameLayout; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.core.view.ViewCompat; 13 | import androidx.customview.widget.ViewDragHelper; 14 | 15 | /** 16 | * Project: gradle 17 | * Created by LiaoKai(soarcn) on 2014/11/25. 18 | */ 19 | class ClosableSlidingLayout extends FrameLayout { 20 | 21 | private static final int INVALID_POINTER = -1; 22 | private final float MINVEL; 23 | View mTarget; 24 | boolean swipeable = true; 25 | private ViewDragHelper mDragHelper; 26 | private SlideListener mListener; 27 | private int height; 28 | private int top; 29 | private int mActivePointerId; 30 | private boolean mIsBeingDragged; 31 | private float mInitialMotionY; 32 | private boolean collapsible = false; 33 | private float yDiff; 34 | 35 | public ClosableSlidingLayout(Context context) { 36 | this(context, null); 37 | } 38 | 39 | public ClosableSlidingLayout(Context context, AttributeSet attrs) { 40 | this(context, attrs, 0); 41 | } 42 | 43 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 44 | public ClosableSlidingLayout(Context context, AttributeSet attrs, int defStyle) { 45 | super(context, attrs, defStyle); 46 | mDragHelper = ViewDragHelper.create(this, 0.8f, new ViewDragCallback()); 47 | MINVEL = getResources().getDisplayMetrics().density * 400; 48 | } 49 | 50 | @Override 51 | public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { 52 | final int action = event.getActionMasked(); 53 | 54 | if (!isEnabled() || canChildScrollUp()) { 55 | // Fail fast if we're not in a state where a swipe is possible 56 | return false; 57 | } 58 | 59 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 60 | mActivePointerId = INVALID_POINTER; 61 | mIsBeingDragged = false; 62 | if (collapsible && -yDiff > mDragHelper.getTouchSlop()) { 63 | expand(mDragHelper.getCapturedView(), 0); 64 | } 65 | mDragHelper.cancel(); 66 | return false; 67 | } 68 | 69 | switch (action) { 70 | case MotionEvent.ACTION_DOWN: 71 | height = getChildAt(0).getHeight(); 72 | top = getChildAt(0).getTop(); 73 | mActivePointerId = event.getPointerId(0); 74 | mIsBeingDragged = false; 75 | final float initialMotionY = getMotionEventY(event, mActivePointerId); 76 | if (initialMotionY == -1) { 77 | return false; 78 | } 79 | mInitialMotionY = initialMotionY; 80 | yDiff = 0; 81 | break; 82 | case MotionEvent.ACTION_MOVE: 83 | if (mActivePointerId == INVALID_POINTER) { 84 | return false; 85 | } 86 | final float y = getMotionEventY(event, mActivePointerId); 87 | if (y == -1) { 88 | return false; 89 | } 90 | yDiff = y - mInitialMotionY; 91 | if (swipeable && yDiff > mDragHelper.getTouchSlop() && !mIsBeingDragged) { 92 | mIsBeingDragged = true; 93 | mDragHelper.captureChildView(getChildAt(0), 0); 94 | } 95 | break; 96 | } 97 | mDragHelper.shouldInterceptTouchEvent(event); 98 | return mIsBeingDragged; 99 | } 100 | 101 | @Override 102 | public void requestDisallowInterceptTouchEvent(boolean b) { 103 | // Nope. 104 | } 105 | 106 | /** 107 | * @return Whether it is possible for the child view of this layout to 108 | * scroll up. Override this if the child view is a custom view. 109 | */ 110 | private boolean canChildScrollUp() { 111 | return mTarget.canScrollVertically(-1); 112 | } 113 | 114 | private float getMotionEventY(MotionEvent ev, int activePointerId) { 115 | final int index = ev.findPointerIndex(activePointerId); 116 | if (index < 0) { 117 | return -1; 118 | } 119 | return ev.getY(index); 120 | } 121 | 122 | @Override 123 | public boolean onTouchEvent(MotionEvent ev) { 124 | if (!isEnabled() || canChildScrollUp()) { 125 | return super.onTouchEvent(ev); 126 | } 127 | 128 | try { 129 | if (swipeable) 130 | mDragHelper.processTouchEvent(ev); 131 | } catch (Exception ignored) { 132 | } 133 | return true; 134 | } 135 | 136 | @Override 137 | public void computeScroll() { 138 | if (mDragHelper.continueSettling(true)) { 139 | ViewCompat.postInvalidateOnAnimation(this); 140 | } 141 | } 142 | 143 | void setSlideListener(SlideListener listener) { 144 | mListener = listener; 145 | } 146 | 147 | void setCollapsible(boolean collapsible) { 148 | this.collapsible = collapsible; 149 | } 150 | 151 | private void expand(View releasedChild, float yvel) { 152 | if (mListener != null) { 153 | mListener.onOpened(); 154 | } 155 | } 156 | 157 | private void dismiss(View view, float yvel) { 158 | mDragHelper.smoothSlideViewTo(view, 0, top + height); 159 | ViewCompat.postInvalidateOnAnimation(ClosableSlidingLayout.this); 160 | } 161 | 162 | /** 163 | * set listener 164 | */ 165 | interface SlideListener { 166 | void onClosed(); 167 | 168 | void onOpened(); 169 | } 170 | 171 | /** 172 | * Callback 173 | */ 174 | private class ViewDragCallback extends ViewDragHelper.Callback { 175 | 176 | 177 | @Override 178 | public boolean tryCaptureView(View child, int pointerId) { 179 | return true; 180 | } 181 | 182 | @Override 183 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 184 | if (yvel > MINVEL) { 185 | dismiss(releasedChild, yvel); 186 | } else { 187 | if (releasedChild.getTop() >= top + height / 2) { 188 | dismiss(releasedChild, yvel); 189 | } else { 190 | mDragHelper.smoothSlideViewTo(releasedChild, 0, top); 191 | ViewCompat.postInvalidateOnAnimation(ClosableSlidingLayout.this); 192 | } 193 | } 194 | } 195 | 196 | @Override 197 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 198 | if (height - top < 1 && mListener != null) { 199 | mDragHelper.cancel(); 200 | mListener.onClosed(); 201 | mDragHelper.smoothSlideViewTo(changedView, 0, top); 202 | } 203 | } 204 | 205 | @Override 206 | public int clampViewPositionVertical(View child, int top, int dy) { 207 | return Math.max(top, ClosableSlidingLayout.this.top); 208 | } 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/FillerView.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.LinearLayout; 7 | 8 | class FillerView extends LinearLayout { 9 | private View mMeasureTarget; 10 | 11 | 12 | public FillerView(Context context) { 13 | super(context); 14 | } 15 | 16 | 17 | public void setMeasureTarget(View lastViewSeen) { 18 | mMeasureTarget = lastViewSeen; 19 | } 20 | 21 | 22 | public FillerView(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | 27 | @Override 28 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 29 | if(null != mMeasureTarget) 30 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 31 | mMeasureTarget.getMeasuredHeight(), MeasureSpec.EXACTLY); 32 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 33 | } 34 | } -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/HeaderLayout.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | 4 | 5 | import android.content.Context; 6 | import android.util.AttributeSet; 7 | import android.widget.FrameLayout; 8 | 9 | 10 | class HeaderLayout extends FrameLayout { 11 | private int mHeaderWidth = 1; 12 | 13 | 14 | public HeaderLayout(Context context) { 15 | super(context); 16 | } 17 | 18 | 19 | public HeaderLayout(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | 24 | public HeaderLayout(Context context, AttributeSet attrs, int defStyle) { 25 | super(context, attrs, defStyle); 26 | } 27 | 28 | public void setHeaderWidth(int width) { 29 | mHeaderWidth = width; 30 | } 31 | 32 | 33 | @Override 34 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 35 | int widthMeasureSpecNew = mHeaderWidth == 1 36 | ? widthMeasureSpec 37 | : MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.getMode(widthMeasureSpec)); 38 | super.onMeasure(widthMeasureSpecNew, heightMeasureSpec); 39 | } 40 | } -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/PinnedSectionGridView.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | 4 | /* 5 | * Copyright 2013 Hari Krishna Dulipudi 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | 21 | import android.content.Context; 22 | import android.util.AttributeSet; 23 | import android.widget.GridView; 24 | 25 | /** 26 | * ListView capable to pin views at its top while the rest is still scrolled. 27 | */ 28 | class PinnedSectionGridView extends GridView { 29 | 30 | 31 | // -- class fields 32 | 33 | private int mNumColumns; 34 | private int mHorizontalSpacing; 35 | private int mColumnWidth; 36 | private int mAvailableWidth; 37 | 38 | public PinnedSectionGridView(Context context) { 39 | super(context); 40 | } 41 | 42 | public PinnedSectionGridView(Context context, AttributeSet attrs) { 43 | super(context, attrs); 44 | } 45 | 46 | public PinnedSectionGridView(Context context, AttributeSet attrs, int defStyleAttr) { 47 | super(context, attrs, defStyleAttr); 48 | } 49 | 50 | public int getNumColumns() { 51 | return mNumColumns; 52 | } 53 | 54 | @Override 55 | public void setNumColumns(int numColumns) { 56 | mNumColumns = numColumns; 57 | super.setNumColumns(numColumns); 58 | } 59 | 60 | public int getHorizontalSpacing() { 61 | return mHorizontalSpacing; 62 | } 63 | 64 | @Override 65 | public void setHorizontalSpacing(int horizontalSpacing) { 66 | mHorizontalSpacing = horizontalSpacing; 67 | super.setHorizontalSpacing(horizontalSpacing); 68 | } 69 | 70 | public int getColumnWidth() { 71 | return mColumnWidth; 72 | } 73 | 74 | @Override 75 | public void setColumnWidth(int columnWidth) { 76 | mColumnWidth = columnWidth; 77 | super.setColumnWidth(columnWidth); 78 | } 79 | 80 | public int getAvailableWidth() { 81 | return mAvailableWidth != 0 ? mAvailableWidth : getWidth(); 82 | } 83 | 84 | // @Override 85 | // protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 86 | // if (mNumColumns == GridView.AUTO_FIT) { 87 | // mAvailableWidth = MeasureSpec.getSize(widthMeasureSpec); 88 | // if (mColumnWidth > 0) { 89 | // int availableSpace = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); 90 | // // Client told us to pick the number of columns 91 | // mNumColumns = (availableSpace + mHorizontalSpacing) / 92 | // (mColumnWidth + mHorizontalSpacing); 93 | // } else { 94 | // // Just make up a number if we don't have enough info 95 | // mNumColumns = 2; 96 | // } 97 | // if(null != getAdapter()){ 98 | // if(getAdapter() instanceof SimpleSectionedGridAdapter){ 99 | // ((SimpleSectionedGridAdapter)getAdapter()).setSections(); 100 | // } 101 | // } 102 | // } 103 | // super.onMeasure(widthMeasureSpec, heightMeasureSpec); 104 | // } 105 | } -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/SimpleSectionedGridAdapter.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | 4 | import java.util.Arrays; 5 | import java.util.Comparator; 6 | 7 | import android.content.Context; 8 | import android.database.DataSetObserver; 9 | import android.text.TextUtils; 10 | import android.util.SparseArray; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.BaseAdapter; 15 | import android.widget.GridView; 16 | import android.widget.ListAdapter; 17 | import android.widget.ListView; 18 | import android.widget.TextView; 19 | 20 | class SimpleSectionedGridAdapter extends BaseAdapter{ 21 | protected static final int TYPE_FILLER = 0; 22 | protected static final int TYPE_HEADER = 1; 23 | protected static final int TYPE_HEADER_FILLER = 2; 24 | private boolean mValid = true; 25 | private int mSectionResourceId; 26 | private LayoutInflater mLayoutInflater; 27 | private ListAdapter mBaseAdapter; 28 | SparseArray
mSections = new SparseArray
(); 29 | private Section[] mInitialSections = new Section[0]; 30 | private Context mContext; 31 | private View mLastViewSeen; 32 | private int mHeaderWidth; 33 | private int mNumColumns; 34 | private int mWidth; 35 | private int mColumnWidth; 36 | private int mHorizontalSpacing; 37 | private int mStretchMode; 38 | private int requestedColumnWidth; 39 | private int requestedHorizontalSpacing; 40 | private GridView mGridView; 41 | private int mHeaderLayoutResId; 42 | private int mHeaderTextViewResId; 43 | 44 | public static class Section { 45 | int firstPosition; 46 | int sectionedPosition; 47 | CharSequence title; 48 | int type = 0; 49 | 50 | public Section(int firstPosition, CharSequence title) { 51 | this.firstPosition = firstPosition; 52 | this.title =title; 53 | } 54 | 55 | public CharSequence getTitle() { 56 | return title; 57 | } 58 | } 59 | 60 | public SimpleSectionedGridAdapter(Context context, BaseAdapter baseAdapter, int sectionResourceId, int headerLayoutResId, 61 | int headerTextViewResId) { 62 | mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 63 | mSectionResourceId = sectionResourceId; 64 | mHeaderLayoutResId = headerLayoutResId; 65 | mHeaderTextViewResId = headerTextViewResId; 66 | mBaseAdapter = baseAdapter; 67 | mContext = context; 68 | mBaseAdapter.registerDataSetObserver(new DataSetObserver() { 69 | @Override 70 | public void onChanged() { 71 | mValid = !mBaseAdapter.isEmpty(); 72 | notifyDataSetChanged(); 73 | } 74 | 75 | @Override 76 | public void onInvalidated() { 77 | mValid = false; 78 | notifyDataSetInvalidated(); 79 | } 80 | }); 81 | } 82 | 83 | public void setGridView(GridView gridView){ 84 | if(!(gridView instanceof PinnedSectionGridView)){ 85 | throw new IllegalArgumentException("Does your grid view extends PinnedSectionGridView?"); 86 | } 87 | mGridView = gridView; 88 | mStretchMode = gridView.getStretchMode(); 89 | mWidth = gridView.getWidth() - (mGridView.getPaddingLeft() + mGridView.getPaddingRight()); 90 | mNumColumns = ((PinnedSectionGridView)gridView).getNumColumns(); 91 | requestedColumnWidth = ((PinnedSectionGridView)gridView).getColumnWidth(); 92 | requestedHorizontalSpacing = ((PinnedSectionGridView)gridView).getHorizontalSpacing(); 93 | } 94 | 95 | private int getHeaderSize(){ 96 | if(mHeaderWidth > 0){ 97 | return mHeaderWidth; 98 | } 99 | if(mWidth != mGridView.getWidth()){ 100 | mStretchMode = mGridView.getStretchMode(); 101 | mWidth = ((PinnedSectionGridView)mGridView).getAvailableWidth() - (mGridView.getPaddingLeft() + mGridView.getPaddingRight()); 102 | mNumColumns = ((PinnedSectionGridView)mGridView).getNumColumns(); 103 | requestedColumnWidth = ((PinnedSectionGridView)mGridView).getColumnWidth(); 104 | requestedHorizontalSpacing = ((PinnedSectionGridView)mGridView).getHorizontalSpacing(); 105 | } 106 | 107 | int spaceLeftOver = mWidth - (mNumColumns * requestedColumnWidth) - 108 | ((mNumColumns - 1) * requestedHorizontalSpacing); 109 | switch (mStretchMode) { 110 | case GridView.NO_STRETCH: // Nobody stretches 111 | mWidth -= spaceLeftOver; 112 | mColumnWidth = requestedColumnWidth; 113 | mHorizontalSpacing = requestedHorizontalSpacing; 114 | break; 115 | 116 | case GridView.STRETCH_COLUMN_WIDTH: 117 | mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; 118 | mHorizontalSpacing = requestedHorizontalSpacing; 119 | break; 120 | 121 | case GridView.STRETCH_SPACING: 122 | mColumnWidth = requestedColumnWidth; 123 | if (mNumColumns > 1) { 124 | mHorizontalSpacing = requestedHorizontalSpacing + 125 | spaceLeftOver / (mNumColumns - 1); 126 | } else { 127 | mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 128 | } 129 | break; 130 | 131 | case GridView.STRETCH_SPACING_UNIFORM: 132 | mColumnWidth = requestedColumnWidth; 133 | mHorizontalSpacing = requestedHorizontalSpacing; 134 | mWidth = mWidth - spaceLeftOver + (2 * mHorizontalSpacing); 135 | break; 136 | } 137 | mHeaderWidth = mWidth + ((mNumColumns - 1) * (mColumnWidth + mHorizontalSpacing)) ; 138 | return mHeaderWidth; 139 | } 140 | 141 | 142 | public void setSections(Section... sections) { 143 | mInitialSections = sections; 144 | setSections(); 145 | } 146 | 147 | public void setSections() { 148 | mSections.clear(); 149 | 150 | getHeaderSize(); 151 | Arrays.sort(mInitialSections, new Comparator
() { 152 | @Override 153 | public int compare(Section o, Section o1) { 154 | return (o.firstPosition == o1.firstPosition) 155 | ? 0 156 | : ((o.firstPosition < o1.firstPosition) ? -1 : 1); 157 | } 158 | }); 159 | 160 | int offset = 0; // offset positions for the headers we're adding 161 | for (int i = 0; i < mInitialSections.length; i++) { 162 | Section section = mInitialSections[i]; 163 | Section sectionAdd; 164 | 165 | for (int j = 0; j < mNumColumns - 1; j++) { 166 | sectionAdd = new Section(section.firstPosition, section.title); 167 | sectionAdd.type = TYPE_HEADER_FILLER; 168 | sectionAdd.sectionedPosition = sectionAdd.firstPosition + offset; 169 | mSections.append(sectionAdd.sectionedPosition, sectionAdd); 170 | ++offset; 171 | } 172 | 173 | sectionAdd = new Section(section.firstPosition, section.title); 174 | sectionAdd.type = TYPE_HEADER; 175 | sectionAdd.sectionedPosition = sectionAdd.firstPosition + offset; 176 | mSections.append(sectionAdd.sectionedPosition, sectionAdd); 177 | ++offset; 178 | 179 | if(i < mInitialSections.length - 1){ 180 | int nextPos = mInitialSections[i+1].firstPosition; 181 | int itemsCount = nextPos - section.firstPosition; 182 | int dummyCount = mNumColumns - (itemsCount % mNumColumns); 183 | if(mNumColumns != dummyCount){ 184 | for (int k = 0 ;k < dummyCount; k++) { 185 | sectionAdd = new Section(section.firstPosition, section.title); 186 | sectionAdd.type = TYPE_FILLER; 187 | sectionAdd.sectionedPosition = nextPos + offset; 188 | mSections.append(sectionAdd.sectionedPosition, sectionAdd); 189 | ++offset; 190 | } 191 | } 192 | } 193 | } 194 | 195 | notifyDataSetChanged(); 196 | } 197 | 198 | public int positionToSectionedPosition(int position) { 199 | int offset = 0; 200 | for (int i = 0; i < mSections.size(); i++) { 201 | if (mSections.valueAt(i).firstPosition > position) { 202 | break; 203 | } 204 | ++offset; 205 | } 206 | return position + offset; 207 | } 208 | 209 | public int sectionedPositionToPosition(int sectionedPosition) { 210 | if (isSectionHeaderPosition(sectionedPosition)) { 211 | return ListView.INVALID_POSITION; 212 | } 213 | 214 | int offset = 0; 215 | for (int i = 0; i < mSections.size(); i++) { 216 | if (mSections.valueAt(i).sectionedPosition > sectionedPosition) { 217 | break; 218 | } 219 | --offset; 220 | } 221 | return sectionedPosition + offset; 222 | } 223 | 224 | public boolean isSectionHeaderPosition(int position) { 225 | return mSections.get(position) != null; 226 | } 227 | 228 | @Override 229 | public int getCount() { 230 | return (mValid ? mBaseAdapter.getCount() + mSections.size() : 0); 231 | } 232 | 233 | @Override 234 | public Object getItem(int position) { 235 | return isSectionHeaderPosition(position) 236 | ? mSections.get(position) 237 | : mBaseAdapter.getItem(sectionedPositionToPosition(position)); 238 | } 239 | 240 | @Override 241 | public long getItemId(int position) { 242 | return isSectionHeaderPosition(position) 243 | ? Integer.MAX_VALUE - mSections.indexOfKey(position) 244 | : mBaseAdapter.getItemId(sectionedPositionToPosition(position)); 245 | } 246 | 247 | @Override 248 | public int getItemViewType(int position) { 249 | return isSectionHeaderPosition(position) 250 | ? getViewTypeCount() - 1 251 | : mBaseAdapter.getItemViewType(sectionedPositionToPosition(position)); 252 | } 253 | 254 | @Override 255 | public boolean isEnabled(int position) { 256 | //noinspection SimplifiableConditionalExpression 257 | return isSectionHeaderPosition(position) 258 | ? false 259 | : mBaseAdapter.isEnabled(sectionedPositionToPosition(position)); 260 | } 261 | 262 | @Override 263 | public int getViewTypeCount() { 264 | return mBaseAdapter.getViewTypeCount() + 1; // the section headings 265 | } 266 | 267 | @Override 268 | public boolean areAllItemsEnabled() { 269 | return mBaseAdapter.areAllItemsEnabled(); 270 | } 271 | 272 | @Override 273 | public boolean hasStableIds() { 274 | return mBaseAdapter.hasStableIds(); 275 | } 276 | 277 | @Override 278 | public boolean isEmpty() { 279 | return mBaseAdapter.isEmpty(); 280 | } 281 | 282 | @Override 283 | public View getView(int position, View convertView, ViewGroup parent) { 284 | if (isSectionHeaderPosition(position)) { 285 | HeaderLayout header; 286 | TextView view; 287 | if (null == convertView) { 288 | convertView = mLayoutInflater.inflate(mSectionResourceId, parent, false); 289 | } else { 290 | if (null == convertView.findViewById(mHeaderLayoutResId)) { 291 | convertView = mLayoutInflater.inflate(mSectionResourceId, parent, false); 292 | } 293 | } 294 | switch (mSections.get(position).type) { 295 | case TYPE_HEADER: 296 | header = (HeaderLayout) convertView.findViewById(mHeaderLayoutResId); 297 | if (!TextUtils.isEmpty(mSections.get(position).title)) { 298 | view = (TextView) convertView.findViewById(mHeaderTextViewResId); 299 | view.setText(mSections.get(position).title); 300 | } 301 | header.setHeaderWidth(getHeaderSize()); 302 | break; 303 | case TYPE_HEADER_FILLER: 304 | header = (HeaderLayout) convertView.findViewById(mHeaderLayoutResId); 305 | if (!TextUtils.isEmpty(mSections.get(position).title)) { 306 | view = (TextView) convertView.findViewById(mHeaderTextViewResId); 307 | view.setText(mSections.get(position).title); 308 | } 309 | header.setHeaderWidth(0); 310 | break; 311 | default: 312 | convertView = getFillerView(mLastViewSeen); 313 | } 314 | } else { 315 | convertView = mBaseAdapter.getView(sectionedPositionToPosition(position), convertView, parent); 316 | mLastViewSeen = convertView; 317 | } 318 | return convertView; 319 | } 320 | 321 | private FillerView getFillerView(final View lastViewSeen) { 322 | final FillerView fillerView = new FillerView(mContext); 323 | fillerView.setMeasureTarget(lastViewSeen); 324 | return fillerView; 325 | } 326 | 327 | public int getHeaderLayoutResId() { 328 | return mHeaderLayoutResId; 329 | } 330 | 331 | public static class ViewHolder { 332 | @SuppressWarnings("unchecked") 333 | public static T get(View view, int id) { 334 | SparseArray viewHolder = (SparseArray) view.getTag(); 335 | if (viewHolder == null) { 336 | viewHolder = new SparseArray(); 337 | view.setTag(viewHolder); 338 | } 339 | View childView = viewHolder.get(id); 340 | if (childView == null) { 341 | childView = view.findViewById(id); 342 | viewHolder.put(id, childView); 343 | } 344 | return (T) childView; 345 | } 346 | } 347 | 348 | } 349 | -------------------------------------------------------------------------------- /android/src/main/java/com/cocosw/bottomsheet/TranslucentHelper.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.bottomsheet; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.res.Configuration; 8 | import android.content.res.Resources; 9 | import android.content.res.TypedArray; 10 | import android.os.Build; 11 | import android.util.DisplayMetrics; 12 | import android.view.ViewConfiguration; 13 | import android.view.Window; 14 | import android.view.WindowManager; 15 | 16 | import java.lang.reflect.Method; 17 | 18 | /** 19 | * Project: BottomSheet 20 | * Created by LiaoKai(soarcn) on 2015/8/29. 21 | */ 22 | 23 | @TargetApi(19) 24 | class TranslucentHelper { 25 | 26 | // translucent support 27 | private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; 28 | private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height"; 29 | private static final String NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME = "navigation_bar_height_landscape"; 30 | private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar"; 31 | private final Dialog dialog; 32 | boolean mNavBarAvailable; 33 | int mStatusBarHeight; 34 | private boolean mInPortrait; 35 | private String sNavBarOverride; 36 | private float mSmallestWidthDp; 37 | 38 | TranslucentHelper(Dialog dialog, Context context) { 39 | this.dialog = dialog; 40 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 41 | mInPortrait = (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT); 42 | try { 43 | Class c = Class.forName("android.os.SystemProperties"); 44 | @SuppressWarnings("unchecked") Method m = c.getDeclaredMethod("get", String.class); 45 | m.setAccessible(true); 46 | sNavBarOverride = (String) m.invoke(null, "qemu.hw.mainkeys"); 47 | } catch (Throwable e) { 48 | sNavBarOverride = null; 49 | } 50 | 51 | // check theme attrs 52 | int[] as = {android.R.attr.windowTranslucentNavigation}; 53 | TypedArray a = context.obtainStyledAttributes(as); 54 | try { 55 | mNavBarAvailable = a.getBoolean(0, false); 56 | } finally { 57 | a.recycle(); 58 | } 59 | 60 | // check window flags 61 | WindowManager.LayoutParams winParams = ((Activity) context).getWindow().getAttributes(); 62 | 63 | int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; 64 | if ((winParams.flags & bits) != 0) { 65 | mNavBarAvailable = true; 66 | } 67 | 68 | mSmallestWidthDp = getSmallestWidthDp(wm); 69 | if (mNavBarAvailable) 70 | setTranslucentStatus(true); 71 | mStatusBarHeight = getInternalDimensionSize(context.getResources(), STATUS_BAR_HEIGHT_RES_NAME); 72 | } 73 | 74 | @SuppressWarnings("SameParameterValue") 75 | private void setTranslucentStatus(boolean on) { 76 | Window win = dialog.getWindow(); 77 | WindowManager.LayoutParams winParams = win.getAttributes(); 78 | final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; 79 | if (on) { 80 | winParams.flags |= bits; 81 | } else { 82 | winParams.flags &= ~bits; 83 | } 84 | 85 | win.setAttributes(winParams); 86 | // instance 87 | win.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 88 | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 89 | } 90 | 91 | private float getSmallestWidthDp(WindowManager wm) { 92 | DisplayMetrics metrics = new DisplayMetrics(); 93 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 94 | wm.getDefaultDisplay().getRealMetrics(metrics); 95 | } else { 96 | //this is not correct, but we don't really care pre-kitkat 97 | wm.getDefaultDisplay().getMetrics(metrics); 98 | } 99 | float widthDp = metrics.widthPixels / metrics.density; 100 | float heightDp = metrics.heightPixels / metrics.density; 101 | return Math.min(widthDp, heightDp); 102 | } 103 | 104 | int getNavigationBarHeight(Context context) { 105 | Resources res = context.getResources(); 106 | int result = 0; 107 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 108 | if (hasNavBar(context)) { 109 | String key; 110 | if (mInPortrait) { 111 | key = NAV_BAR_HEIGHT_RES_NAME; 112 | } else { 113 | if (!isNavigationAtBottom()) 114 | return 0; 115 | key = NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME; 116 | } 117 | return getInternalDimensionSize(res, key); 118 | } 119 | } 120 | return result; 121 | } 122 | 123 | private boolean hasNavBar(Context context) { 124 | Resources res = context.getResources(); 125 | int resourceId = res.getIdentifier(SHOW_NAV_BAR_RES_NAME, "bool", "android"); 126 | if (resourceId != 0) { 127 | boolean hasNav = res.getBoolean(resourceId); 128 | // check override flag (see static block) 129 | if ("1".equals(sNavBarOverride)) { 130 | hasNav = false; 131 | } else if ("0".equals(sNavBarOverride)) { 132 | hasNav = true; 133 | } 134 | return hasNav; 135 | } else { // fallback 136 | return !ViewConfiguration.get(context).hasPermanentMenuKey(); 137 | } 138 | } 139 | 140 | private int getInternalDimensionSize(Resources res, String key) { 141 | int result = 0; 142 | int resourceId = res.getIdentifier(key, "dimen", "android"); 143 | if (resourceId > 0) { 144 | result = res.getDimensionPixelSize(resourceId); 145 | } 146 | return result; 147 | } 148 | 149 | /** 150 | * Should a navigation bar appear at the bottom of the screen in the current 151 | * device configuration? A navigation bar may appear on the right side of 152 | * the screen in certain configurations. 153 | * 154 | * @return True if navigation should appear at the bottom of the screen, False otherwise. 155 | */ 156 | private boolean isNavigationAtBottom() { 157 | return (mSmallestWidthDp >= 600 || mInPortrait); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /android/src/main/res/anim-v21/bs_list_item_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/anim-v21/bs_list_layout_anim_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/res/anim/bs_list_item_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/anim/bs_list_layout_anim_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/res/anim/dock_bottom_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /android/src/main/res/anim/dock_bottom_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/bs_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-hdpi/bs_ic_clear.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/bs_ic_clear_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-hdpi/bs_ic_clear_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/bs_ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-hdpi/bs_ic_more.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/bs_ic_more_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-hdpi/bs_ic_more_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-mdpi/bs_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-mdpi/bs_ic_clear.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-mdpi/bs_ic_clear_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-mdpi/bs_ic_clear_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-mdpi/bs_ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-mdpi/bs_ic_more.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-mdpi/bs_ic_more_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-mdpi/bs_ic_more_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-v21/bs_list_dark_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-v21/bs_list_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/bs_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xhdpi/bs_ic_clear.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/bs_ic_clear_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xhdpi/bs_ic_clear_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/bs_ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xhdpi/bs_ic_more.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/bs_ic_more_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xhdpi/bs_ic_more_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xxhdpi/bs_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xxhdpi/bs_ic_clear.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xxhdpi/bs_ic_clear_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xxhdpi/bs_ic_clear_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xxhdpi/bs_ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xxhdpi/bs_ic_more.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xxhdpi/bs_ic_more_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/android/src/main/res/drawable-xxhdpi/bs_ic_more_light.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/bs_list_dark_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/bs_list_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/main/res/layout/bottom_sheet_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /android/src/main/res/layout/bs_grid_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /android/src/main/res/layout/bs_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 16 | 17 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/src/main/res/layout/bs_list_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/layout/bs_list_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /android/src/main/res/values-land/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 4 | 2 5 | 3 6 | -------------------------------------------------------------------------------- /android/src/main/res/values-sw600dp-land/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 4 | 2 5 | 4 6 | -------------------------------------------------------------------------------- /android/src/main/res/values-sw600dp/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 4 | 3 5 | 5 6 | -------------------------------------------------------------------------------- /android/src/main/res/values-zh/string.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 更多 4 | -------------------------------------------------------------------------------- /android/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1f000000 4 | #1fffffff 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 0dp 5 | 8dp 6 | 8dp 7 | -------------------------------------------------------------------------------- /android/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0 4 | 3 5 | 3 6 | 5 7 | -------------------------------------------------------------------------------- /android/src/main/res/values/string.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | More 4 | -------------------------------------------------------------------------------- /android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 | 47 | 48 | 56 | 57 | 63 | 64 | 70 | 71 | 77 | 78 | 89 | 90 | 95 | 96 | 103 | 104 | 107 | 108 | 114 | 115 | 121 | 122 | 130 | 131 | 139 | 140 | 146 | 147 | 152 | 153 | 160 | 161 | 165 | 166 | 172 | 173 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /image-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clip-sub/react-native-bottomsheet/ee0c177678fe36b46e7cfd00ab4f530e9e30c0e9/image-demo.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface OptionsType { 2 | options?: string[]; 3 | title?: string; 4 | dark?: boolean; 5 | cancelButtonIndex?: number; 6 | } 7 | 8 | export type ShareWithOptionsType = { 9 | url: string; 10 | subject: string; 11 | message: string; 12 | } 13 | 14 | export default class BottomSheet { 15 | static showBottomSheetWithOptions: ( 16 | options?: OptionsType, 17 | callback?: (value: number) => void | undefined, 18 | ) => void 19 | static showShareBottomSheetWithOptions: ( 20 | options: ShareWithOptionsType, 21 | callback: (value: any) => void | undefined, 22 | ) => void 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { NativeModules, Platform } from 'react-native'; 2 | import BottomSheetIOS from "./module.ios"; 3 | 4 | export default Platform.OS === 'ios' ? BottomSheetIOS : NativeModules.RNBottomSheet; 5 | -------------------------------------------------------------------------------- /module.ios.js: -------------------------------------------------------------------------------- 1 | import { ActionSheetIOS } from 'react-native'; 2 | 3 | const BottomSheetIOS = { 4 | showBottomSheetWithOptions: (options, callback) => { 5 | return ActionSheetIOS.showActionSheetWithOptions(options, callback); 6 | }, 7 | 8 | showShareBottomSheetWithOptions: (options, failureCallback, successCallback) => { 9 | return ActionSheetIOS.showShareActionSheetWithOptions(options, failureCallback, successCallback); 10 | } 11 | } 12 | 13 | export default BottomSheetIOS; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-bottomsheet", 3 | "version": "2.9.0", 4 | "description": "True Cross-platform ActionSheet for Android and iOS", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "keywords": [ 8 | "react", 9 | "react-native", 10 | "bottomsheet", 11 | "android", 12 | "ios" 13 | ], 14 | "author": "clip-sub", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https@github.com:Clip-sub/react-native-bottomsheet.git" 19 | }, 20 | "scripts": { 21 | "deploy": "yarn build && npm publish", 22 | "clean": "rm -rf dist .rts2_*" 23 | }, 24 | "devDependencies": { 25 | "metro-react-native-babel-preset": "^0.71.0" 26 | }, 27 | "peerDependencies": { 28 | "react": ">=16", 29 | "react-native": "^0.71.0" 30 | } 31 | } 32 | --------------------------------------------------------------------------------