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 覆盖导航栏

BottomSheetDialogFragment - 如何包装内容并完全显示?

BottomSheetDialogFragment 不是模态的