摘要
使用最简单方式实现抽屉侧边栏,点击按钮打开抽屉侧边栏.
关键信息
- Android Studio:Iguana | 2023.2.1
- Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-8.4-bin.zip
- jvmTarget = '1.8'
- minSdk 24
- targetSdk 34
- compileSdk 34
- 开发语言:Kotlin,Java
- ndkVersion = '21.1.6352462'
- kotlin版本:1.9.20
- kotlinCompilerExtensionVersion '1.5.4'
- com.android.library:8.3
原理简介
抽屉式侧边栏
[https://pixso.cn/designskills/cechoutisheji/]
[https://juejin.cn/post/7109057588460797988]
[https://developer.android.google.cn/jetpack/androidx/releases/drawerlayout?hl=zh-cn]
[https://developer.android.google.cn/guide/navigation/integrations/ui?hl=zh-cn]
侧抽屉究竟有多强?合理设计让用户不迷路
当屏幕尺寸有限,产品功能越来越多时,侧抽屉可以 “收纳”功能,减少用户认知负担,使界面更加清爽好用.
移动用户在使用应用程序时需要随时知道去“哪里”,以及如何到“那里”。为了使UI导航既可发现又可用,UI设计师好的选择是侧抽屉。侧边抽屉减少了 UI 混乱并优先考虑重要的导航目的地。侧抽屉的设计非常简单,适合大多数移动应用程序布局。
侧边抽屉,也称为滑动菜单、导航抽屉或左侧导航,包含网站或移动应用程序的主要导航目的地。通常隐藏在视图之外,侧抽屉从屏幕的左边缘滑入。它可以用鼠标或手指滑动,也可以用汉堡图标打开或关闭。链接按优先级顺序显示为一个在另一个之下的多个行项目。
类型:
- 永久侧抽屉
始终可见并固定在屏幕的左边缘。此侧抽屉推荐用于桌面设计。 - 持久侧边抽屉
默认折叠,用户可以打开或关闭。它与屏幕内容的其余部分位于相同的表面高度。打开时,它会强制其他内容适应大小。建议将此侧抽屉用于任何大于移动设备的 UI 设计,例如台式机、平板电脑和横向模式。 - 迷你变体侧抽屉
类似于持久侧抽屉,该抽屉在其“静止位置”隐藏在视线之外。但是,与强制其他元素调整的持久侧抽屉不同,迷你变体抽屉根据 UI 的内容扩展宽度。对于需要快速选择访问的应用程序,建议使用此侧抽屉。 - 临时侧边抽屉
最常用于移动应用程序设计,临时侧边抽屉切换打开或关闭,并在所有其他内容之上打开。侧边抽屉导航从左侧滑出,占据整个屏幕高度,遵循普通列表的布局规则。手机端的侧边抽屉距离屏幕右侧56dp。
如果移动应用程序具有深度导航结构,则用户将需要一种在导航目的地之间移动的方法,从而不会被视觉混乱或复杂的导航路径分心。
侧抽屉将所有导航项分组在一个根视图下。通过整合项目,而不是将它们分散在 UI 中,提高可见性。还可以减少用户的认知负担,因为他们只需要关注单个UI组件。
此外,由于没有后退按钮,完成任务所需的用户操作数量保持在最低限度。作为一般经验法则,如果应用几乎没有导航选项,则应该避免使用侧边抽屉。默认情况下使用折叠视图,侧边抽屉降低了导航的可发现性。
事实上,侧边抽屉式导航可能会花费一半的用户参与度。所以请注意,如果没有可用性,简洁的设计就毫无意义。当导航选项较少时,最好设计一个导航栏或使用选项卡式导航。
添加侧边栏最简单方式:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--主页面布局-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<Button
android:id="@+id/btn_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打开滑动菜单"
android:textColor="#000"
android:textSize="18sp" />
</LinearLayout>
<!--滑动菜单布局-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/colorAccent"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="滑动菜单"
android:textColor="#000"
android:textSize="18sp" />
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
侧边栏的关键是android:layout_gravity="start"
,start
设置了从左侧划出,end
可设置从右侧划出.
Android导航图
[https://juejin.cn/post/7241184271318515773]
[https://developer.android.google.cn/guide/navigation?hl=zh-cn]
Navigation 作为 Android Jetpack 组件库中的一员,是一个通用的页面导航框架。为单 Activity 架构而生的端内路由导航,用来管理 Fragment 的切换,并且可以通过可视化的方式,看见 App 的交互流程。
导航图原理 |
---|
实现
核心代码
注意:侧边栏布局在xml中需要放置在主页布局下方,否则可以显示无法交互.
activity_main.xml
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main_drawerlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:openDrawer="start"
tools:context="cn.qsbye.grape_yolov5_detect_android.MainActivity" >
<!-- start 主界面 -->
<LinearLayout
android:id="@+id/activity_main"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingRight="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_vertical_margin"
android:orientation="vertical">
<!-- start 顶部标题栏 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="380dp"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:strokeWidth="0dp"
android:layout_gravity="center"
app:cardBackgroundColor="@color/macaron_lavender_maple">
<androidx.appcompat.widget.ActionMenuView
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<Button
android:id="@+id/title_drawer_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/macaron_lavender_maple"
android:text="爱(AI)数葡萄"
android:textColor="@color/white" />
</androidx.appcompat.widget.ActionMenuView>
</com.google.android.material.card.MaterialCardView>
<!-- end 顶部标题栏 -->
<ImageView
android:id="@+id/background_image"
android:layout_width="wrap_content"
android:layout_height="450dp"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:src="@drawable/grape_bg"/>
<!-- start 智慧视觉按钮 -->
<Button
android:id="@+id/smart_vision_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_vertical_margin"
android:text="智慧视觉" />
<!-- end 智慧视觉按钮 -->
<!-- start 图片识别选择按钮 -->
<Button
android:id="@+id/photo_detect_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_vertical_margin"
android:text="图片识别" />
<!-- end 图片识别选择按钮 -->
<!-- start 幸运按钮 -->
<Button
android:id="@+id/lucky_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_vertical_margin"
android:text="幸运按钮" />
<!-- end 幸运按钮 -->
</LinearLayout>
<!-- end 主界面 -->
<!-- start 抽屉导航(必须放在主界面以下,否则无法交互) -->
<!-- start 侧边栏按钮容器 -->
<LinearLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_gravity="start"
android:background="@color/white"
android:paddingBottom="0dp">
<!-- start 选择图片按钮 -->
<Button
android:id="@+id/select_from_album_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="选择相册图片" />
<!-- end 选择图片按钮 -->
<!-- start 拍照按钮 -->
<Button
android:id="@+id/camera_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="拍照" />
<!-- end 拍照按钮 -->
<!-- start 跳转关于页按钮 -->
<Button
android:id="@+id/about_activity_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="关于" />
<!-- end 跳转关于页按钮 -->
<!-- start 跳转设置页按钮 -->
<Button
android:id="@+id/settings_activity_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="设置" />
<!-- end 跳转设置页按钮 -->
<!-- start 跳转任意门按钮 -->
<Button
android:id="@+id/pattern_lock_activity_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="任意门" />
<!-- end 跳转任意门按钮 -->
</LinearLayout>
<!-- end 侧边栏按钮容器 -->
<!-- end 抽屉导航 -->
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt
package cn.qsbye.grape_yolov5_detect_android
import android.content.Context
import android.content.Intent
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import cn.qsbye.grape_yolov5_detect_android.databinding.ActivityMainBinding
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.withName
import com.mx.imgpicker.MXImagePicker
import com.mx.imgpicker.app.MXImgShowActivity
import com.mx.imgpicker.builder.MXCaptureBuilder
import com.mx.imgpicker.builder.MXPickerBuilder
import com.mx.imgpicker.models.MXPickerType
import github.leavesczy.matisse.*
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.random.Random
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
private lateinit var imagePickerLauncher: ActivityResultLauncher<Intent>
/* start 图片选择器相关 */
private val takePictureLauncher =
registerForActivityResult(MatisseCaptureContract()) { result: MediaResource? ->
if (result != null) {
val uri = result.uri
val path = result.path
val name = result.name
val mimeType = result.mimeType
// 启动ResultActivity
val imageUri = uri
val intent = Intent(this@MainActivity, ResultActivity::class.java)
intent.putExtra("original_grape_bitmap_uri", imageUri)
startActivity(intent)
}
}
val mediaPickerLauncher =
registerForActivityResult(MatisseContract()) { result: List<MediaResource>? ->
if (!result.isNullOrEmpty()) {
val mediaResource = result[0]
val uri = mediaResource.uri
val path = mediaResource.path
val name = mediaResource.name
val mimeType = mediaResource.mimeType
// 启动ResultActivity
val imageUri = uri
val intent = Intent(this@MainActivity, ResultActivity::class.java)
intent.putExtra("original_grape_bitmap_uri", imageUri)
startActivity(intent)
}
}
val matisse = Matisse(
maxSelectable = 1,
mediaFilter = DefaultMediaFilter(supportedMimeTypes = MimeType.ofImage(hasGif = false)),
imageEngine = GlideImageEngine(),
singleMediaType = true,
captureStrategy = null
)
/* end 图片选择器相关 */
// toast函数
fun Context.toast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
companion object {
@JvmStatic
external fun initAssetManager(assetManager: AssetManager)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 预加载图片可以提前搜索本机图片/视频资源,减少首次进入选择页面时空白时间
MXImagePicker.init(application)
lifecycleScope.launch{ MXImagePicker.preScan(this@MainActivity) }
// 注册ActivityResultLauncher处理图片选择成功事件
val imagePickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
// 处理图片选择结果
val data = result.data
val imageUri = data?.data
if (imageUri != null) {
Log.e("图片", "图片选择成功")
// 将图片传递到 ResultActivity
// this.startResultActivityWithSelectedImage(imageUri)
}else{
Log.e("图片", "图片选择失败!")
// 测试显示图片
MXImgShowActivity.open(
this, arrayListOf(
"https://gitee.com/zhangmengxiong/MXImagePicker/raw/master/imgs/screenshot3.png",
), "图片详情"
)
}
}
}
// 初始化assets文件夹实例
val assetManager: AssetManager = assets
initAssetManager(assetManager)
/* start 监听幸运按钮 */
val luckyButton = findViewById<Button>(R.id.lucky_btn)
luckyButton.setOnClickListener {
val s_grape_file_str = String.format("grape_img/G%04d.jpg", Random.nextInt(1, 10))
try {
// 从assets目录中读取位图数据
val inputStream = assets.open(s_grape_file_str)
val bitmap = BitmapFactory.decodeStream(inputStream)
Log.e("检测", "当前识别的图片为:$s_grape_file_str")
// 将位图保存到应用的缓存目录中
val file = File(cacheDir, "grape_image.jpg")
val outputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.flush()
outputStream.close()
// 创建一个 Intent 对象,指定当前活动(this)和目标活动(ResultActivity::class.java)
val intent = Intent(this@MainActivity, ResultActivity::class.java)
// 将位图的 URI 作为附加参数传递给 ResultActivity
val imageUri = FileProvider.getUriForFile(this, "cn.qsbye.grape_yolov5_detect_android.fileprovider", file)
intent.putExtra("original_grape_bitmap_uri", imageUri)
// 启动 ResultActivity
startActivity(intent)
// 不会执行:在解析位图完成后发送广播通知 ResultActivity
val broadcastIntent = Intent("cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE")
sendBroadcast(broadcastIntent)
} catch (e: IOException) {
e.printStackTrace()
}
} // end setOnClickListener
/* end 监听幸运按钮 */
/* start 监听拍照按钮 */
val cameraButton = findViewById<Button>(R.id.camera_btn)
cameraButton.setOnClickListener {
/* start 检查相机权限 */
XXPermissions.with(this)
// 申请单个权限
.permission(Permission.CAMERA)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (!allGranted) {
// toast("相机权限正常")
return
}
toast("获取相机权限成功")
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
if (doNotAskAgain) {
toast("被永久拒绝授权,请手动授予相机权限")
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
} else {
toast("获取相机权限失败")
}
}
})
/* end 检查相机权限 */
// 使用Matisse库
runOnUiThread{
takePictureLauncher.launch(MatisseCapture(captureStrategy = MediaStoreCaptureStrategy()))
}
} // end setOnClickListener
/* end 监听拍照按钮 */
/* start 监听相册按钮 */
val selectFromAlbumButton = findViewById<Button>(R.id.select_from_album_btn)
selectFromAlbumButton.setOnClickListener{
/* start 检查相册(存储)权限 */
XXPermissions.with(this)
// 申请多个权限
.permission(Permission.CAMERA)
.permission(Permission.READ_MEDIA_IMAGES)
.permission(Permission.READ_MEDIA_VIDEO)
.permission(Permission.READ_MEDIA_VISUAL_USER_SELECTED)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (!allGranted) {
toast("有权限没有获取成功")
return
}
toast("获取所需权限成功")
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
if (doNotAskAgain) {
toast("被永久拒绝授权,请手动授予相机、相册权限")
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
} else {
toast("获取相机、相册权限失败")
}
}
})
/* end 检查相册(存储)权限 */
Log.e("图片", "进入图片选择器!")
// 使用Matisse库
mediaPickerLauncher.launch(matisse)
}// end setOnClickListener
/* end 监听相册按钮 */
/* start 监听抽屉栏开启按钮 */
val drawerSlider = findViewById<DrawerLayout>(R.id.activity_main_drawerlayout)
// 找到按钮并设置点击监听器
val titleDrawerBtn: Button = findViewById(R.id.title_drawer_btn)
titleDrawerBtn.setOnClickListener {
//打开滑动菜单,左侧出现
drawerSlider.openDrawer(GravityCompat.START)
}
/* end 监听抽屉栏开启按钮 */
/* start 监听关于页按钮 */
val aboutActivityBtn: Button = findViewById(R.id.about_activity_btn)
aboutActivityBtn.setOnClickListener {
// 跳转关于页
val intent = Intent(this, AboutActivity::class.java)
startActivity(intent)
// finish()
}
/* end 监听关于页按钮 */
/* start 监听设置页按钮 */
val settingsActivityBtn: Button = findViewById(R.id.settings_activity_btn)
settingsActivityBtn.setOnClickListener {
// 跳转设置页
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
// finish()
}
/* end 监听设置页按钮 */
/* start 监听任意门按钮 */
val patternLockActivityBtn: Button = findViewById(R.id.pattern_lock_activity_btn)
patternLockActivityBtn.setOnClickListener {
// 跳转任意门页
val intent = Intent(this, PatternLockActivity::class.java)
startActivity(intent)
// finish()
}
/* end 监听任意门按钮 */
/* start 监听智慧视觉按钮 */
val smartVisionActivityBtn: Button = findViewById(R.id.smart_vision_btn)
smartVisionActivityBtn.setOnClickListener {
// pass
}
/* end 监听智慧视觉按钮 */
/* start 监听图片识别按钮 */
val photoDetectBtn: Button = findViewById(R.id.photo_detect_btn)
photoDetectBtn.setOnClickListener {
// pass
}
/* end 监听图片识别按钮 */
} // end onCreate
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
// 用选择的图片启动ResultActivity
private fun startResultActivityWithSelectedImage(imageUri: Uri) {
val intent = Intent(this, ResultActivity::class.java)
intent.putExtra("original_grape_bitmap_uri", imageUri)
startActivity(intent)
}
}
效果
开启抽屉侧边栏并跳转页面 |
---|