BottomSheetDialogFragment实现地图导航弹窗效果
Posted AnalyzeSystem
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BottomSheetDialogFragment实现地图导航弹窗效果相关的知识,希望对你有一定的参考价值。
需求如下:从服务器查询列表在地图上弹窗显示,首次只能显示两条Item,下滑隐藏,上滑动铺满位于titleBar下,点击标记物显示详情,详情内可以点击查看列表
看效果图
首先想到的是NetScrollView CoordinatorLayout Behavior,当然还有BottomSheet系列,个人比较倾向于BottomSheetDialogFragment,不用在layout 绑定behavior操作,就像一般的Fragment dialog使用一样即可
相关BottomSheetDialogFragment的使用操作先推荐一篇文章科普,很有必要看哟
https://www.jianshu.com/p/7fcec871ea36
根据上面这边博客我先搞一个BaseBottomSheetFragment基类,方便其他地方调用
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.*
import androidx.annotation.NonNull
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.zgph.xz.sitemap.R
import java.lang.reflect.Method
import kotlin.math.min
import kotlin.properties.Delegates
abstract class BaseBottomSheetFragment : BottomSheetDialogFragment() {
protected var rootView: View? = null
/**
* 默认显示item Count : 0
*/
protected var itemCount = 0
/**
* 设置默认弹出高度peekHeight
*/
protected var minPeekHeight by Delegates.notNull<Int>()
/**
* 设置默认弹出最大高度maxPeekHeight
*/
protected var maxPeekHeight by Delegates.notNull<Int>()
/**
* 控制显示高度、状态值
*/
private var mBottomSheetBehavior: BottomSheetBehavior<View>? = null
private lateinit var mBottomSheet: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//弹出BottomSheetDialog背景变暗
/**
*
<style name="TransBottomSheetDialogStyle" parent="Theme.Design.Light.BottomSheetDialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
*
*/
//弹出BottomSheetDialog背景变暗解决方案
setStyle(BottomSheetDialogFragment.STYLE_NORMAL, R.style.pr_TransBottomSheetDialogStyle)
//item显示几条
itemCount = getShowItemCount()
//初始化弹出初始高度和弹出最大高度
minPeekHeight = BottomSheetDelegate.getMinPeekHeight(context!!,getItemLayoutResId(), getHeaderLayoutResId(), itemCount)
maxPeekHeight = BottomSheetDelegate.getMaxHeight(activity!!,getTitleBarLayoutResId())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (null != rootView) {
val parent = rootView!!.parent as ViewGroup
parent?.removeView(rootView)
} else {
rootView = inflater.inflate(getContentViewResID(), null)
initialize()
}
initialize()
return rootView
}
override fun onStart() {
super.onStart()
initBottomSheet()
}
private fun initBottomSheet() {
val dialog: Dialog? = dialog
if (dialog != null) {
val bottomSheet: View = dialog.findViewById(R.id.design_bottom_sheet)
//bottomSheet.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
//点击外部能让dialog 消失
dialog.setCanceledOnTouchOutside(true)
//防止最大高度View视图位置显示不正确
dialog?.window?.setGravity(Gravity.BOTTOM);
}
val view = view
view!!.post {
val parent = view.parent as View
val params = parent.layoutParams as CoordinatorLayout.LayoutParams
mBottomSheetBehavior = params.behavior as BottomSheetBehavior<View>?
//禁止下拉隐藏
//mBottomSheetBehavior?.isHideable = false
//设置初始弹出最大高度
setPeekHeight(minPeekHeight)
//解决背景和弹出背景重叠
parent.setBackgroundColor(Color.TRANSPARENT)
//设置回调函数用于设置拖拽扩展view最大高度设置
setBottomSheetCallback()
}
}
private fun setBottomSheetCallback() {
if (mBottomSheetBehavior != null) {
mBottomSheetBehavior!!.addBottomSheetCallback(mBottomSheetCallback)
}
}
private val mBottomSheetCallback: BottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(@NonNull bottomSheet: View,
@BottomSheetBehavior.State newState: Int) {
mBottomSheet = bottomSheet
var layoutParams = bottomSheet.layoutParams
if (bottomSheet.height > maxPeekHeight) {
layoutParams.height = maxPeekHeight
bottomSheet.layoutParams = layoutParams
}
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
listener?.onCollapsed()
} else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
listener?.onExpand()
}
}
override fun onSlide(@NonNull bottomSheet: View, slideOffset: Float) {
}
}
/**
* 设置显示弹窗高度
*/
protected fun setPeekHeight(mPeekHeight: Int) {
if (mPeekHeight <= 0) {
return
}
minPeekHeight = mPeekHeight
mBottomSheetBehavior!!.peekHeight = minPeekHeight
}
override fun show(manager: FragmentManager, tag: String?) {
super.show(manager, tag)
dialog?.setOnShowListener {
var parentView = rootView?.parent as View
val params = parentView.layoutParams as CoordinatorLayout.LayoutParams
params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
parentView.layoutParams = params
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onBottomSheetDismiss()
}
//Dialog消失毁掉可以自己处理一些逻辑,如果不需要可以取消函数相关调用
protected abstract fun onBottomSheetDismiss()
//整体布局setContentView
protected abstract fun getContentViewResID(): Int
//设置初始弹出显示Item布局ID(adapte 单一type)
protected abstract fun getItemLayoutResId(): Int
//设置初始弹出显示位于某个Layout布局之下(例如自定义的titleBar)
protected abstract fun getTitleBarLayoutResId(): Int
//设置初始弹出显示RecyclerView的HeaderView布局,如果没有HeaderView默认返回-1
protected abstract fun getHeaderLayoutResId(): Int
//设置初始弹出显示Item条数
protected abstract fun getShowItemCount(): Int
protected abstract fun initialize()
interface OnBottomSheetSiteListener {
//展开状态毁掉
fun onExpand()
//收缩状态毁掉
fun onCollapsed()
}
private var listener: OnBottomSheetSiteListener? = null
protected fun addOnBottomSheetSiteListener(onBottomSheetSiteListener: OnBottomSheetSiteListener) {
listener = onBottomSheetSiteListener
}
}
使用时注意拷贝注释掉的style,关于一些view高度测量相关的函数提取到BottomSheetDelegate类代理
import android.app.Activity
import android.content.Context
import android.util.DisplayMetrics
import android.view.Display
import android.view.LayoutInflater
import android.view.View
import java.lang.reflect.Method
class BottomSheetDelegate {
companion object{
/**
* @param titleBarLayoutResId: NoActionBar Theme》屏幕高度-虚拟按键高度-指定的top view 部分高度,这里id=topView部分id
* 默认展示两个Item,为了测量方便这里布局子view高度用精确值
*/
fun getMinPeekHeight(context: Context,itemLayoutResId: Int, headerLayoutResId: Int,itemCount:Int):Int {
var minPeekHeight = 0
if (headerLayoutResId == -1) {
minPeekHeight = getMeasuredHeightWithResId(context!!,itemLayoutResId) * itemCount
} else {
minPeekHeight = getMeasuredHeightWithResId(context!!,itemLayoutResId) * itemCount + getMeasuredHeightWithResId(context!!,headerLayoutResId)
}
return minPeekHeight
}
/**
* 测量view 高度,布局使用精确值
*/
fun getMaxHeight(activity: Activity,titleBarLayoutResId: Int): Int {
//系统屏幕高度
val outMetrics = DisplayMetrics()
activity?.windowManager?.defaultDisplay?.getMetrics(outMetrics)
val heightPixels = outMetrics.heightPixels
//自定义标题栏背景向上填充statusBar 高度
val resourceId: Int =
activity!!.resources.getIdentifier("status_bar_height", "dimen", "android")
val statusBarHeight: Int = activity!!.resources.getDimensionPixelSize(resourceId)
//计算自定义标题栏高度
var itemView = LayoutInflater.from(activity).inflate(titleBarLayoutResId, null, false)
var w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
var h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
itemView.measure(w, h);
//兼容凹凸屏幕
var max = 0
var notchSize = activity!!.let { NotchScreenUtil.getNotchHeight(it) }
if (notchSize != 0) {
max = heightPixels - itemView.measuredHeight - statusBarHeight + notchSize
} else {
max = heightPixels - itemView.measuredHeight - statusBarHeight
}
return max
}
/**
* 获取虚拟按键高度
*/
fun getVirtualBarHeight(activity:Activity): Int {
var vh = 0
val display: Display = activity!!.windowManager.defaultDisplay
val dm = DisplayMetrics()
try {
val c = Class.forName("android.view.Display")
val method: Method = c.getMethod("getRealMetrics", DisplayMetrics::class.java)
method.invoke(display, dm)
vh = dm.heightPixels - display.height
} catch (e: Exception) {
e.printStackTrace()
}
return vh
}
/**
* 测量view 高度,布局使用精确值
*/
fun getMeasuredHeightWithResId(context:Context,layoutResId: Int): Int {
var itemView = LayoutInflater.from(context).inflate(layoutResId, null, false)
var w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
var h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
itemView.measure(w, h);
return itemView.measuredHeight
}
fun setBottomSheetCallback(){
}
}
}
Tip: 这里的测量模式是根据自己布局写死的,具体使用具体修改,maxHeight的计算是根据自己UI实现的,也是具体界面具体修改,虚拟按键高度获取提供了暂未使用
这里面还做了关于华为、vivo、oppo手机凹凸屏幕高度获取适配NotchScreenUtil
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.Log
import android.util.TypedValue
import java.lang.reflect.Method
class NotchScreenUtil {
companion object{
/**
* 华为start
*/
// 判断是否是华为刘海屏
fun hasNotchInScreenAtHuawei(context: Context): Boolean {
var ret = false
try {
val cl: ClassLoader = context.getClassLoader()
val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
val get: Method = HwNotchSizeUtil.getMethod("hasNotchInScreen")
ret = get.invoke(HwNotchSizeUtil) as Boolean
Log.d("NotchScreenUtil", "this Huawei device has notch in screen?$ret")
} catch (e: ClassNotFoundException) {
Log.e("NotchScreenUtil", "hasNotchInScreen ClassNotFoundException", e)
} catch (e: NoSuchMethodException) {
Log.e("NotchScreenUtil", "hasNotchInScreen NoSuchMethodException", e)
} catch (e: Exception) {
Log.e("NotchScreenUtil", "hasNotchInScreen Exception", e)
}
return ret
}
/**
* 获取华为刘海的高
* @param context
* @return
*/
fun getNotchSizeAtHuawei(context: Context): Int {
var ret = intArrayOf(0, 0)
try {
val cl: ClassLoader = context.getClassLoader()
val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
val get: Method = HwNotchSizeUtil.getMethod("getNotchSize")
ret = get.invoke(HwNotchSizeUtil) as IntArray
} catch (e: ClassNotFoundException) {
Log.e("NotchScreenUtil", "getNotchSize ClassNotFoundException")
} catch (e: NoSuchMethodException) {
Log.e("NotchScreenUtil", "getNotchSize NoSuchMethodException")
} catch (e: Exception) {
Log.e("NotchScreenUtil", "getNotchSize Exception")
}
return ret[1]
}
/**
* Oppo start
*/
fun hasNotchInScreenAtOppo(context: Context): Boolean {
val hasNotch: Boolean =
context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism")
Log.d("NotchScreenUtil", "this OPPO device has notch in screen?$hasNotch")
return hasNotch
}
val notchSizeAtOppo: Int
get() = 80
val NOTCH_IN_SCREEN_VOIO = 0x00000020 // 是否有凹槽
fun hasNotchInScreenAtVivo(context: Context): Boolean {
var ret = false
try {
val cl: ClassLoader = context.getClassLoader()
val FtFeature = cl.loadClass("com.util.FtFeature")
val get: Method = FtFeature.getMethod("isFeatureSupport", Int::class.javaPrimitiveType)
ret = get.invoke(FtFeature, NOTCH_IN_SCREEN_VOIO) as Boolean
Log.d("NotchScreenUtil", "this VIVO device has notch in screen?$ret")
} catch (e: ClassNotFoundException) {
Log.e("NotchScreenUtil", "hasNotchInScreen ClassNotFoundException", e)
} catch (e: NoSuchMethodException) {
Log.e("NotchScreenUtil", "hasNotchInScreen NoSuchMethodException", e)
} catch (e: Exception) {
Log.e("NotchScreenUtil", "hasNotchInScreen Exception", e)
}
return ret
}
fun getNotchSizeAtVivo(context: Context): Int {
return dp2px(context, 32)
}
/**
* vivo end
*/
/**
* dp转px
* @param context
* @param dpValue
* @return
*/
private fun dp2px(context: Context, dpValue: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dpValue.toFloat(), context.getResources()
.getDisplayMetrics()
)
.toInt()
}
/**
* 获取手机厂商
*
* @return 手机厂商
*/
val DEVICE_BRAND_OPPO = 0x0001
val DEVICE_BRAND_HUAWEI = 0x0002
val DEVICE_BRAND_VIVO = 0x0003
@get:SuppressLint("DefaultLocale")
val deviceBrand: Int
get() {
val brand = Build.BRAND.trim { it <= ' ' }.toUpperCase()
if (brand.contains("HUAWEI")) {
return DEVICE_BRAND_HUAWEI
} else if (brand.contains("OPPO")) {
return DEVICE_BRAND_OPPO
} else if (brand.contains("VIVO")) {
return DEVICE_BRAND_VIVO
}
return 0
}
fun getNotchHeight(context: Context):Int{
var height = 0
when (deviceBrand) {
DEVICE_BRAND_HUAWEI -> {
if (hasNotchInScreenAtHuawei(context)) {
height = getNotchSizeAtHuawei(context)
}
}
DEVICE_BRAND_OPPO -> {
if (hasNotchInScreenAtOppo(context)) {
height = notchSizeAtOppo
}
}
DEVICE_BRAND_VIVO -> {
if (hasNotchInScreenAtVivo(context)) {
height = getNotchSizeAtVivo(context)
}
}
}
return height
}
}
}
最后再提供一个函数关于上图效果三角形旋转的代码块(如果使用两张图就可以忽略了)
private fun rotateBitmap(bm: Bitmap, orientationDegree: Float): Bitmap? {
//方便判断,角度都转换为正值
var degree = orientationDegree
if (degree < 0) {
degree = 360 + orientationDegree
}
val srcW = bm.width
val srcH = bm.height
val m = Matrix()
m.setRotate(degree, srcW.toFloat() / 2, srcH.toFloat() / 2)
val targetX: Float
val targetY: Float
var destH = srcH
var destW = srcW
//根据角度计算偏移量,原理不明
if (degree == 90f) {
targetX = srcH.toFloat()
targetY = 0f
destH = srcW
destW = srcH
} else if (degree == 270f) {
targetX = 0f
targetY = srcW.toFloat()
destH = srcW
destW = srcH
} else if (degree == 180f) {
targetX = srcW.toFloat()
targetY = srcH.toFloat()
} else {
return bm
}
val values = FloatArray(9)
m.getValues(values)
val x1 = values[Matrix.MTRANS_X]
val y1 = values[Matrix.MTRANS_Y]
m.postTranslate(targetX - x1, targetY - y1)
//注意destW 与 destH 不同角度会有不同
val bm1 = Bitmap.createBitmap(destW, destH, Bitmap.Config.ARGB_8888)
val paint = Paint()
val canvas = Canvas(bm1)
canvas.drawBitmap(bm, m, paint)
return bm1
}
有以上代码我相信你也可以轻松实现上图效果,源码暂不上传了
以上是关于BottomSheetDialogFragment实现地图导航弹窗效果的主要内容,如果未能解决你的问题,请参考以下文章
如果键盘可见,则防止关闭 BottomSheetDialogFragment
赶上BottomSheetDialogFragment的解雇
Android BottomSheetDialogFragment 闪烁
防止 BottomSheetDialogFragment 覆盖导航栏