如何在 Android 中实现 Material Design 文档中的底部工具栏

81

你如何实现底部工作表规范?http://www.google.com/design/spec/components/bottom-sheets.html

Google Drive的新更新通过Floating Action Button按下来显示此内容 ->

enter image description here

虽然规格说明中没有提到圆角,但是这是可以实现的,只是不确定如何去做。目前正在使用AppCompat库并将目标设置为21。

谢谢。


链接已经失效。新链接为https://material.io/design/components/sheets-bottom.html#usage。 - Ray Li
9个回答

67

编辑

BottomSheet 现在是 android-support-library 的一部分。请参见约翰·谢利的答案


不幸的是,目前没有官方的方法可以做到这一点(至少我不知道有)。
幸运的是,有一个名为"BottomSheet"(点击)的库,它模仿了BottomSheet的外观和感觉,并支持 Android 2.1 及以上版本。

对于Drive应用程序,在这个库中的代码如下所示:

    new BottomSheet.Builder(this, R.style.BottomSheet_Dialog)
            .title("New")
            .grid() // <-- important part
            .sheet(R.menu.menu_bottom_sheet)
            .listener(new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            // TODO
        }
    }).show();

menu_bottom_sheet(基本上是一个标准的/res/menu/*.xml资源)

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/folder"
        android:title="Folder"
        android:icon="@drawable/ic_action_folder" />
    <item
        android:id="@+id/upload"
        android:title="Upload"
        android:icon="@drawable/ic_action_file_upload" />
    <item
        android:id="@+id/scan"
        android:title="Scan"
        android:icon="@drawable/ic_action_camera_alt" />
</menu>

输出如下:

底部工作表的图片

我认为这已经非常接近原版了。如果您对颜色不满意,您可以自定义它-点击此处(单击).


1
这太棒了,谢谢分享!如果想显示出在共享意图中显示的所有选项怎么办?它们将根据设备上安装的应用程序动态生成。对于如何实现这一点有任何想法吗? - Cat
1
@Cat 基本上,您只需要获取所有可以处理意图的应用程序列表(例如,请参见此StackO-Question),并将它们添加到“喂”BottomSheet的适配器中。 您还可以查看该库的官方示例,该示例正是您所请求的内容。 (点击 - 第153至179行)。 - reVerse
这个库太棒了! - kuldeep
@reVerse 我想要实现Android Material Design规范中所提供的带有FAB图标的底部菜单(http://bit.ly/1O8EQrK)。这个库能实现吗? - Koustuv Sinha
1
@JohnShelley 没问题,绝对没问题。我也建议采用“官方解决方案”,尽管目前非常有限,但至少谷歌已经打下了基础。 ;) - reVerse
显示剩余6条评论

66

回答自己的问题,让开发人员知道新的支持库终于提供了这个功能!让我们向强大的谷歌致敬!

Android Developer's Blog中的一个例子:

// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);  
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);  
behavior.setBottomSheetCallback(new BottomSheetCallback() {  
   @Override  
   public void onStateChanged(@NonNull View bottomSheet, int newState) {  
     // React to state change  
   }  

  @Override  
  public void onSlide(@NonNull View bottomSheet, float slideOffset) {  
     // React to dragging events  
  }  
});

@reVerse的上面的答案仍然是一个有效的选项,但知道Google支持的标准也很好。


感谢您的发布。我认为现在这应该是“正确”的答案 :) - vine'th
@androiddeveloper 完成了。考虑到这是一个特定于支持库的事情,我觉得用户在寻找这些示例时会有更好的运气。当我最初提出问题时,它没有包含在支持库中。如果问题是“如何实现支持库底部表视图”,那么我也会提供更详细的答案 :) - John Shelley
@JohnShelley,你不需要使用一些XML相关的东西吗?是否可以将底部工作表的peek/collapsed(或者当它在底部时被称为什么)设置为wrap-content视图,并且当它展开时,变成全屏? - android developer
4
我认为有很多人想知道如何使用Google的新支持API,Vipul Shah有一个很棒的YouTube链接,可以帮助你了解如何使用它,请参考https://dev59.com/8V8d5IYBdhLWcg3wiSjI#35731959中的YouTube链接。 - minh truong
问题仍然存在:如何实现底部菜单。在上面的示例中,菜单项在哪里?????你知道,那些你点击的菜单项... - ekashking

8

根据博客文章:http://android-developers.blogspot.com/2016/02/android-support-library-232.html

我的 XML 最终看起来像这样:

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator_layout"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:orientation="horizontal"
        app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
        <ImageView
            android:src="@android:drawable/ic_input_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

在我的片段的onCreateView方法中:
    coordinatorLayout = (CoordinatorLayout)v.findViewById(R.id.coordinator_layout);
    View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
    BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
    behavior.setPeekHeight(100);
    behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            // React to state change
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            // React to dragging events
        }
    });

setPeekHeight的默认值为0,因此如果您没有设置它,则无法看到您的视图。


是的,XML文件与我的片段完全一样。它对你不起作用吗? - mcorrado
默认的最大高度是-1(自动!),而不是0。 - Zordid
1
如果您将使用AndroidX,请记住包名称的更改:com.google.android.material.bottomsheet.BottomSheetBehavior - Ultimo_m
底部菜单的目的就在于显示菜单项,你在哪里呢??????????? - ekashking

7

现在您可以从Android支持库23.2中使用官方的BottomSheetBehavior API。

以下是示例代码片段:

bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheet));

case R.id.expandBottomSheetButton:
 bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
 break;
case R.id.collapseBottomSheetButton:
 bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
 break;
case R.id.hideBottomSheetButton:
 bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
 break;
case R.id.showBottomSheetDialogButton:
 new MyBottomSheetDialogFragment().show(getSupportFragmentManager(), "sample");

嗨Vipul,感谢提供链接。我有一个问题。是否可以为底部表设置锚点? - koraxis
太棒了,这个Youtube链接。你有源代码存放在公共仓库里吗? - John Shelley

5

5
以下是其他几个选项:
  • Flipboard提供了一个可用的选项,但嵌入的活动需要修改才能使用底部工作表。
  • tutti-ch的bottomsheet :这是从Android Repo的ResolverActivity中提取出来的,启动活动不需要进行修改。

3

Bottom Sheet image

如果您想实现像这样的底部菜单,可以按照以下设计模式完成几个简单步骤:
  1. 创建bottom_sheet_layout.xml布局文件
  2. 创建bottom_sheet_background.xml可绘制文件
请将您的bottom_sheet_background.xml可绘制文件设置为以下内容:
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
    
        <solid android:color="@color/bottom_sheet_background"/>
        <corners
            android:topRightRadius="20dp"
            android:topLeftRadius="20dp"/>
    
    </shape>
    
    

你的bottom_sheet_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        android:id="@+id/bottom_Sheet"
        android:background="@drawable/bottom_sheet_background"
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="24dp"
        android:paddingEnd="24dp"
        android:paddingTop="16dp"
        android:paddingBottom="42dp"
        android:orientation="vertical"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <ImageView
            android:id="@+id/rectangle_3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:background="@drawable/rectangle_39"
            />
    
    
        //add your design code here
    
    </LinearLayout>
    
    

以及您的activity_main.xml或片段

    <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
    
       //design your code here
    
    
        //this is your bottom sheet layout included here
        <include
            android:id="@+id/bottom_sheet_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_behavior="com.google.android.material.
            bottomsheet.BottomSheetBehavior"
            layout="@layout/bottom_sheet_layout"/>
    
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

最后,在您的 MainActivityFragment 类中添加代码。这里我将在您的 onCreateonCreateView 中添加 Kotlin 代码。

BottomSheetBehavior.from(binding.bottomSheetLayout.bottomSheet).apply {
    //peek height is default visible height
    peekHeight = 200
    this.state = BottomSheetBehavior.STATE_COLLAPSED
}

就是这样!


0

2
有没有这方面的样例或教程? - android developer

-1

现在随着Android Jetpack Compose的发布,这是Android的现代UI工具包,底部工作表可以更轻松地创建,而无需使用任何xml代码:

1.创建持久性底部工作表,用户可以在底部工作表范围之外访问内容:

enter image description here

val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
    bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
MaterialTheme {
    Column {
        BottomSheetScaffold(
            modifier = Modifier.fillMaxSize(),
            topBar = { TopAppBar(viewModel, onNavigateToRecipeListScreen, hideKeyBoard) },
            content = {
                CreateRecipeContent(
                    viewModel,
                    context,
                    readExternalStorage,
                    bottomSheetScaffoldState,
                    coroutineScope
                )
            },
            scaffoldState = bottomSheetScaffoldState,
            sheetContent = {
                Column(
                    Modifier
                        .fillMaxWidth()
                        .height(200.dp)
                        .background(color = colorResource(id = R.color.colorPrimaryLight))
                )
                {
                    Text(
                        text = "SELECT PICTURE",
                        style = TextStyle(fontSize = 26.sp),
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier
                            .padding(8.dp)
                            .align(Alignment.Start),
                        color = Color.Black
                    )
                    Spacer(modifier = Modifier.height(16.dp))
                    IconButton(onClick = {
                        when {
                            context.let { it1 ->
                                ContextCompat.checkSelfPermission(
                                    it1,
                                    Manifest.permission.READ_EXTERNAL_STORAGE
                                )
                            } == PackageManager.PERMISSION_GRANTED -> {
                                val takePictureIntent =
                                    Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                                launchCamera(takePictureIntent)
                                coroutineScope.launch {
                                    bottomSheetScaffoldState.bottomSheetState.collapse()
                                }
                            }
                            else -> {
                                // You can directly ask for the permission.
                                // The registered ActivityResultCallback gets the result of this request.
                                viewModel.isCameraPermissionAsked = true
                                readExternalStorage()
                                coroutineScope.launch {
                                    bottomSheetScaffoldState.bottomSheetState.collapse()
                                }
                            }
                        }

                    }, modifier = Modifier.fillMaxWidth()) {
                        Text(
                            text = "TAKE PHOTO",
                            style = TextStyle(fontSize = 20.sp),
                            fontWeight = FontWeight.Bold,
                            modifier = Modifier
                                .padding(8.dp)
                                .align(Alignment.Start),
                            textAlign = TextAlign.Left,
                            color = Color.Black
                        )
                    }
                    Spacer(modifier = Modifier.height(16.dp))
                    IconButton(onClick = {
                        when {
                            context.let { it1 ->
                                ContextCompat.checkSelfPermission(
                                    it1,
                                    Manifest.permission.READ_EXTERNAL_STORAGE
                                )
                            } == PackageManager.PERMISSION_GRANTED -> {
                                val galleryIntent = Intent(
                                    Intent.ACTION_PICK,
                                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                                )
                                galleryIntent.type = "image/*"
                                launchGalley(galleryIntent)
                                coroutineScope.launch {
                                    bottomSheetScaffoldState.bottomSheetState.collapse()
                                }
                            }
                            else -> {
                                // You can directly ask for the permission.
                                // The registered ActivityResultCallback gets the result of this request.
                                viewModel.isCameraPermissionAsked = false
                                readExternalStorage()
                                coroutineScope.launch {
                                    bottomSheetScaffoldState.bottomSheetState.collapse()
                                }
                            }
                        }

                    }, modifier = Modifier.fillMaxWidth()) {
                        Text(
                            text = "CHOOSE FROM GALLERY",
                            style = TextStyle(fontSize = 20.sp),
                            fontWeight = FontWeight.Bold,
                            modifier = Modifier
                                .padding(8.dp)
                                .align(Alignment.Start),
                            textAlign = TextAlign.Left,
                            color = Color.Black
                        )
                    }

                }
            }, sheetPeekHeight = 0.dp
        )


    }
}

上述代码片段和屏幕截图来自该应用程序:

https://play.google.com/store/apps/details?id=com.bhuvnesh.diary

这个应用完全由我使用Jetpack Compose创建

  • 创建模态底部表单,使用户无法访问底部表单范围之外的内容:

     ModalBottomSheetLayout(
         sheetState = modalBottomSheetState,
         sheetElevation = 8.dp,
         sheetContent = {
             //表单内容
         }
     ) {
         ...
         //主要内容
     }
    

  • 网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接