仿Telegram媒体功能

先上一个Telegram的媒体样式, 后面是自定义是实现效果

        

 

大概说一下实现的方式,目前用的是DialogFragment全屏实现,自定义滚动视图,利用pictureselector图片库 获取数据源,列表第一个是打开的相机,根据官方提供的相机库 实现绑定生命周期显示相机。
先看滚动视图,首先你得知道你的滚动视图分为几个view,占几部分,我这里写的固定view,如果满足不了你的需求请自行修改

DialogFragment中滚动View
 class ChatPhotoAlertNestedScrollingParent @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr), NestedScrollingParent3 {

    // 标题栏
    private var mFlTitle: View? = null
    // 滚动的间距
    private var mViewSpace: View? = null
    // 滚动底部的view
    private var mScrollBottomView: View? = null

    // 滚动的背景布局头
    private var mClBottomLayoutTitle: ConstraintLayout? = null
    private var mTabBgStartColor = Color.WHITE
    private var mTabBgEndColor = Color.WHITE
    private var mTabBgColor = 0
    // 背景 圆角
    private var mTabBg: GradientDrawable = GradientDrawable()
    private var mTabBgRadius: FloatArray = FloatArray(8)
    private var mMaxTabBgRadius = 0f

    private val mHelper: NestedScrollingParentHelper = NestedScrollingParentHelper(this)
    private var mSnapAnimator: ValueAnimator? = null
    private var mLastStartedType = 0
    private var mScrollRangeTop = 0
    private var mScrollRangeBottom = 0
    private var mCurrentScrollRange = 0
    private var mTitleChange = false
    private var isScrollTopShowTitle = false
    private var mArgbEvaluator: ArgbEvaluator = ArgbEvaluator()
    private var isDoPerformAnyCallbacks = true
    private var mOnOffsetScrollRangeListener: OnOffsetScrollRangeListener? = null

    init {
        mArgbEvaluator = ArgbEvaluator()
        mTabBgColor = mTabBgStartColor
        mMaxTabBgRadius = getDefaultTabBgRadius().toFloat()
        mTabBg.setColor(mTabBgStartColor)
        mTabBgRadius = FloatArray(8)
        mTabBgRadius[0] = mMaxTabBgRadius.also { mTabBgRadius[3] = it }
            .also { mTabBgRadius[2] = it }.also { mTabBgRadius[1] = it }
        mTabBgRadius[4] = 0.also { mTabBgRadius[7] = it.toFloat() }.also {
            mTabBgRadius[6] =
                it.toFloat()
        }.also { mTabBgRadius[5] = it.toFloat() }.toFloat()
        mTabBg.cornerRadii = mTabBgRadius
    }

    override fun onFinishInflate() {
        super.onFinishInflate()
        mFlTitle = findViewById(R.id.fl_title)
        mViewSpace = findViewById(R.id.view_space)
        mScrollBottomView = findViewById(R.id.ll_scroll_bottom_layout)
        mClBottomLayoutTitle = findViewById(R.id.cl_bottom_top_title)
        mClBottomLayoutTitle?.background = mTabBg
    }

    private fun getDefaultTabBgRadius(): Int {
        return getDimen(R.dimen.dp_16)
    }

    private fun getDimen(@DimenRes dimenId: Int): Int {
        return resources.getDimensionPixelSize(dimenId)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val mViewSpaceHeight = mViewSpace!!.measuredHeight
        val mFlTitleHeight = mFlTitle!!.measuredHeight
        val mViewSpaceMarginTop = (mViewSpace!!.layoutParams as MarginLayoutParams).topMargin
        // 获取向上滑动的距离
        mScrollRangeTop = mViewSpaceHeight + mViewSpaceMarginTop + mFlTitleHeight
        val update = mCurrentScrollRange != 0 && mScrollRangeBottom == mCurrentScrollRange
        // 屏幕总高度 - 上方空间高度 - 状态栏高度
        val mViewSpaceBottom = ScreenUtils.getScreenHeight() - mViewSpaceHeight
        // 获取向下滑动的距离  布局距离下方的3分之1
        mScrollRangeBottom = -(mViewSpaceBottom - mViewSpaceBottom / 3)
        if (update) {
            mCurrentScrollRange = mScrollRangeBottom
        }
        val scrollWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
        val scrollHeightSpec =
            MeasureSpec.makeMeasureSpec(measuredHeight - mFlTitleHeight, MeasureSpec.EXACTLY)
//        val scrollHeightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
        mScrollBottomView!!.measure(scrollWidthSpec, scrollHeightSpec)
        if (mOnOffsetScrollRangeListener != null) {
            mOnOffsetScrollRangeListener!!.offsetScroll(0f,
                mCurrentScrollRange,
                mScrollRangeTop,
                mScrollRangeBottom,
                isScrollTopShowTitle)
        }
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        val mViewSpaceHeight =
            ScreenUtils.getScreenHeight() - mScrollBottomView!!.measuredHeight + mViewSpace!!.measuredHeight
        mViewSpace!!.layout(left, mFlTitle!!.measuredHeight, right, mViewSpaceHeight)
        val t = mScrollRangeTop - mCurrentScrollRange + mFlTitle!!.measuredHeight
        mScrollBottomView!!.layout(left, t, right, t + mScrollBottomView!!.measuredHeight)
    }

    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        val started = axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
        if (started) {
            if (mSnapAnimator != null) {
                mSnapAnimator!!.cancel()
            }
        }
        mLastStartedType = type
        return started
    }

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        mHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        if (mLastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
            if (mSnapAnimator == null || !mSnapAnimator!!.isRunning) {
                snap()
            }
        }
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray,
    ) {
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
        if (dyUnconsumed < 0) {
            val bottom: Int
            if (type == ViewCompat.TYPE_TOUCH) {
                bottom = mScrollRangeBottom
            } else if (mCurrentScrollRange < 0) {
                bottom = mScrollRangeBottom
                if (dyUnconsumed > -10 && mCurrentScrollRange < mScrollRangeBottom * 0.25f) {
                    ViewCompat.stopNestedScroll(target, type)
                }
            } else {
                bottom = 0
            }
            consumed[1] = offsetScrollView(dyUnconsumed, mScrollRangeTop, bottom)
        }
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
    ) {
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (dy > 0) {
            if (type == ViewCompat.TYPE_TOUCH || mCurrentScrollRange > 0) {
                consumed[1] = offsetScrollView(dy, mScrollRangeTop, mScrollRangeBottom)
            } else {
                offsetScrollView(dy, 0, mScrollRangeBottom)
                consumed[1] = dy
            }
        }
    }

    private fun offsetScrollView(dy: Int) {
        offsetScrollView(dy, mScrollRangeTop, mScrollRangeBottom, true)
    }

    private fun offsetScrollView(dy: Int, top: Int, bottom: Int): Int {
        return offsetScrollView(dy, top, bottom, false)
    }

    private fun offsetScrollView(dy: Int, top: Int, bottom: Int, anim: Boolean): Int {
        var tempDy = dy
        if (tempDy >= 0 && mCurrentScrollRange >= top) {
            return 0
        }
        if (tempDy <= 0 && mCurrentScrollRange <= bottom) {
            return 0
        }
        val result = tempDy
        if (mCurrentScrollRange <= 0 && !anim) {
            tempDy /= 1.5f.toInt()
        }
        mCurrentScrollRange += tempDy
        if (mCurrentScrollRange > top) {
            tempDy -= mCurrentScrollRange - top
            mCurrentScrollRange = top
        } else if (mCurrentScrollRange < bottom) {
            tempDy -= mCurrentScrollRange - bottom
            mCurrentScrollRange = bottom
        }
        changeTitle(mCurrentScrollRange.toFloat())
        ViewCompat.offsetTopAndBottom(mScrollBottomView!!, -tempDy)
        return result
    }

    private fun changeTitle(current: Float) {
        val fraction: Float = if (current < 0) {
            0f
        } else if (current >= mScrollRangeTop) {
            1f
        } else {
            current / mScrollRangeTop
        }
//        int titleColor = (int) mArgbEvaluator.evaluate(fraction, mTitleSearchBgStartColor, mTitleSearchBgEndColor);
//        mFlTitle.getBackground().mutate().setAlpha((int) (fraction * 0xFF));
        val radius = mMaxTabBgRadius * (1 - fraction)
        mTabBgRadius[3] = radius
        mTabBgRadius[2] = mTabBgRadius[3]
        mTabBgRadius[1] = mTabBgRadius[2]
        mTabBgRadius[0] = mTabBgRadius[1]
        mTabBg.cornerRadii = mTabBgRadius
        val tabColor = mArgbEvaluator.evaluate(fraction, mTabBgColor, mTabBgEndColor) as Int
        mTabBg.setColor(tabColor)
        mClBottomLayoutTitle!!.background = mTabBg
        dispatchTitleChange(fraction)
        if (mOnOffsetScrollRangeListener != null && isDoPerformAnyCallbacks) {
            mOnOffsetScrollRangeListener!!.offsetScroll(fraction,
                mCurrentScrollRange,
                mScrollRangeTop,
                mScrollRangeBottom,
                isScrollTopShowTitle)
            mOnOffsetScrollRangeListener!!.isScrollTopShowTitle(mCurrentScrollRange,
                mScrollRangeTop,
                mCurrentScrollRange == mScrollRangeTop)
        }
    }

    private fun dispatchTitleChange(fraction: Float) {
        val change = fraction > 0
        if (change != mTitleChange) {
            mTitleChange = change
        }
    }

    fun setExpand(expand: Boolean) {
        if (expand) {
            anim(mCurrentScrollRange, mScrollRangeBottom)
        } else {
            anim(mCurrentScrollRange, 0)
        }
    }

    private fun snap() {
        val start = mCurrentScrollRange
        if (start != mScrollRangeTop) {
            val end: Int = if (start < mScrollRangeBottom * 0.5f) {
                //                end = mScrollRangeBottom;
                0
            } else if (start > mScrollRangeTop * 0.5f) {
                mScrollRangeTop
            } else {
                0
            }
            anim(start, end)
        }
    }

    private fun anim(start: Int, end: Int) {
        if (start == end) return
        isScrollTopShowTitle = end == mScrollRangeTop
        if (mSnapAnimator == null) {
            mSnapAnimator = ValueAnimator.ofInt(start, end)
            mSnapAnimator?.addUpdateListener { animation: ValueAnimator ->
                val value = animation.animatedValue as Int
                offsetScrollView(value - mCurrentScrollRange)
            }
        } else {
            mSnapAnimator!!.cancel()
            mSnapAnimator!!.setIntValues(start, end)
        }
        mSnapAnimator!!.duration = 250
        mSnapAnimator!!.start()
    }

    fun setDoPerformAnyCallbacks(doPerformAnyCallbacks: Boolean) {
        isDoPerformAnyCallbacks = doPerformAnyCallbacks
    }

    fun setOnOffsetScrollRangeListener(listener: OnOffsetScrollRangeListener) {
        mOnOffsetScrollRangeListener = listener
    }

    // 滚动的距离
    interface OnOffsetScrollRangeListener {
        fun offsetScroll(
            fraction: Float,
            offset: Int,
            scrollRangeTop: Int,
            scrollRangeBottom: Int,
            isScrollTopShowTitle: Boolean,
        )

        fun isScrollTopShowTitle(offset: Int, scrollRangeTop: Int, isScrollTopShowTitle: Boolean)
    }
}

这里面主要就是测量view的大小,计算出滚动的距离,还有显示的位置等;(滚动距离还有显示位置根据自己需求自行修改)

下面才是媒体显示的页面,主要介绍一下逻辑部分(布局就不贴了,尾部给链接,自行查看)

在滚动的时候 上面的导航标题栏显示隐藏;并且可以点击切换相册媒体文件,点击图片带有选中的序号,按照顺序显示;下方带有说明的输入框;在列表的第一个是打开的相机,这里相机是需要绑定生命周期的,直接传入DialogFragment,点击之后进入自定义拍照相机页面, 拍照介绍返回图片添加进入列表显示

DialogFragment
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        initView()
        clickListener()
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(STYLE_NORMAL, R.style.TransparentDialog)
        onCreateConfigLoader()
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        mActivity = context as FragmentActivity
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState)
        binding = DialogChatAttachPhotoAlertBinding.inflate(layoutInflater, null, false)
        if (Build.VERSION.SDK_INT >= 30) {
            dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
        } else {
            dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
        }
        dialog.window?.statusBarColor = Color.TRANSPARENT
        dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        val params = dialog.window!!.attributes
        params.gravity = Gravity.BOTTOM
        params.height = ViewGroup.LayoutParams.MATCH_PARENT
        if (Build.VERSION.SDK_INT >= 28) {
            params.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
        }
        dialog.window?.attributes = params
        dialog.setContentView(binding.root)
        dialog.setOnKeyListener { _, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
                dismissWithDelayRun()
                true
            } else {
                false
            }
        }
        return dialog
    }

    // 初始化获取数据源的状态
    private fun onCreateConfigLoader() {
        PictureSelectionConfig.imageEngine = GlideEngine.createGlideEngine()
        config = PictureSelectionConfig.getInstance()
        config.chooseMode = SelectMimeType.ofAll()
        config.isDisplayCamera = true
        config.isPageSyncAsCount = true
        config.isPageStrategy = true
        config.isGif = true
        config.isBmp = true
        config.maxSelectNum = Int.MAX_VALUE
        mLoader = if (config.isPageStrategy) LocalMediaPageLoader() else LocalMediaLoader()
        mLoader.initConfig(mActivity, config)
    }

    @SuppressLint("ClickableViewAccessibility", "NotifyDataSetChanged")
    private fun initView() {
        // 弹出动画之后 设置背景半透明
        mHandler.postDelayed({
            // 在显示之后 设置没有动画   否则跳转页面在返回是会有弹出动画
            dialog?.window?.setWindowAnimations(R.style.DialogNoAnimation)
            binding.nestedScrolling.setDoPerformAnyCallbacks(true)
            val animator = ValueAnimator.ofInt(0, 128)
            animator.duration = 200
            animator.addUpdateListener {
                val animatedValue = it.animatedValue as Int
                dialog?.window!!.statusBarColor =
                    ColorUtils.setAlphaComponent(Color.parseColor("#80222229"), animatedValue)
                binding.nestedScrolling.setBackgroundColor(
                    ColorUtils.setAlphaComponent(Color.parseColor("#80222229"), animatedValue)
                )
            }
            animator.start()
        }, 400)
        binding.nestedScrolling.setOnOffsetScrollRangeListener(object :
            ChatPhotoAlertNestedScrollingParent.OnOffsetScrollRangeListener {
            override fun offsetScroll(
                fraction: Float,
                offset: Int,
                scrollRangeTop: Int,
                scrollRangeBottom: Int,
                isScrollTopShowTitle: Boolean,
            ) {
                this@ChatAttachPhotoAlert.onScrollOffset = offset
                this@ChatAttachPhotoAlert.scrollTop = scrollRangeTop
                this@ChatAttachPhotoAlert.scrollBottom = scrollRangeBottom
            }

            override fun isScrollTopShowTitle(
                offset: Int,
                scrollRangeTop: Int,
                isScrollTopShowTitle: Boolean,
            ) {
                if (offset == scrollTop) {
                    dialog?.window!!.statusBarColor = Color.parseColor("#0096f0")
                    binding.flTitle.visible()
                } else {
                    dialog?.window!!.statusBarColor = Color.parseColor("#80222229")
                    binding.flTitle.invisible()
                }
            }
        })
        initAlbumListPopWindow()
//        binding.recyclerViewPreview.setOnTouchListener { _, event ->
//            setOnTouchListenerScrollHandler(event)
//            return@setOnTouchListener false
//        }
        binding.recyclerView.setOnTouchListener { _, event ->
            setOnTouchListenerScrollHandler(event)
            return@setOnTouchListener false
        }
        binding.recyclerView.setHasFixedSize(true)
        binding.recyclerView.setReachBottomRow(RecyclerPreloadView.BOTTOM_PRELOAD)
        binding.recyclerView.setOnRecyclerViewPreloadListener(this)
        binding.recyclerView.layoutManager = mLayoutManager
        binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(3, 6.dp2px(), true))
        mAdapter = BasePictureAdapter(this, openCameraClick = {
            openCameraPermissions()
        }, onItemClickListener = { _, position ->
            onStartPreview(position)
        }, onItemLongClick = { _, position ->
            if (mDragSelectTouchListener != null) {
                val vibrator = activity?.getSystemService(Service.VIBRATOR_SERVICE) as Vibrator
                vibrator.vibrate(50)
                mDragSelectTouchListener?.startSlideSelection(if (mAdapter.isDisplayCamera()) position - 1 else position)
            }
        }, onSelectListener = { selectedView, ivPicture, data, position ->
            val selectResultCode = confirmSelect(data, selectedView.isSelected)
            selectedMedia(selectedView, ivPicture, isSelected(data))
            mAdapter.notifyItemChanged(position)
            if (selectResultCode == SelectedManager.ADD_SUCCESS) {
                val animation = AnimationUtils.loadAnimation(context, com.luck.picture.lib.R.anim.ps_anim_modal_in)
                selectedView.startAnimation(animation)
            }
        })
        mAdapter.setDisplayCamera(true)
        binding.recyclerView.adapter = mAdapter
        // 图片预览
//        binding.recyclerViewPreview.setHasFixedSize(true)
//        binding.recyclerViewPreview.layoutManager = LinearLayoutManager(mActivity)
//        binding.recyclerViewPreview.adapter =
//            mAdapterPreview.apply { addItemBinder(PreviewImageGroupAdapter()) }
        SelectedManager.addAllSelectResult(mAdapter.data)
        binding.recyclerView.setOnRecyclerViewScrollStateListener(object :
            OnRecyclerViewScrollStateListener {
            override fun onScrollFast() {
                if (PictureSelectionConfig.imageEngine != null) {
                    PictureSelectionConfig.imageEngine.pauseRequests(context)
                }
            }

            override fun onScrollSlow() {
                if (PictureSelectionConfig.imageEngine != null) {
                    PictureSelectionConfig.imageEngine.resumeRequests(context)
                }
            }
        })
        val selectedPosition = HashSet<Int>()
        val slideSelectionHandler =
            SlideSelectionHandler(object : SlideSelectionHandler.ISelectionHandler {
                override fun getSelection(): HashSet<Int> {
                    for (i in 0 until SelectedManager.getSelectCount()) {
                        val media = SelectedManager.getSelectedResult()[i]
                        selectedPosition.add(media.position)
                    }
                    return selectedPosition
                }

                override fun changeSelection(
                    start: Int,
                    end: Int,
                    isSelected: Boolean,
                    calledFromOnStart: Boolean,
                ) {
                    // 下标是0的时候不处理 因为是相机
//                    if (start == 0 || end == 0) return
                    val adapterData: ArrayList<LocalMedia> = mAdapter.data as ArrayList
                    if (adapterData.size == 0 || start > adapterData.size) return
                    val media = adapterData[start]
                    val selectResultCode: Int =
                        confirmSelect(media, SelectedManager.getSelectedResult().contains(media))
                    mDragSelectTouchListener?.setActive(selectResultCode != SelectedManager.INVALID)
                }
            })
        mDragSelectTouchListener = SlideSelectTouchListener()
            .setRecyclerViewHeaderCount(if (mAdapter.isDisplayCamera()) 1 else 0)
            .withSelectListener(slideSelectionHandler)
        mDragSelectTouchListener?.let { binding.recyclerView.addOnItemTouchListener(it) }
        val emojiTheming = EmojiTheming(
            ContextCompat.getColor(mActivity, R.color.color_F2F3F5),
            ContextCompat.getColor(mActivity, R.color.black),
            ContextCompat.getColor(mActivity, R.color.blue_color),
            ContextCompat.getColor(mActivity, R.color.color_ECEDF1),
            ContextCompat.getColor(mActivity, R.color.blue_color),
            ContextCompat.getColor(mActivity, R.color.blue_color)
        )
        emojiPopup = EmojiPopup(rootView = binding.emotionContainerFrameLayout,
            editText = binding.layoutBottomEdit.editText,
            onEmojiPopupShownListener = {
                binding.layoutBottomEdit.emotionImageView.setImageResource(R.drawable.icon_chat_key)
            },
            onEmojiPopupDismissListener = {
                binding.layoutBottomEdit.emotionImageView.setImageResource(R.drawable.icon_chat_emo)
            },
            onSoftKeyboardCloseListener = {},
            onSoftKeyboardOpenListener = {},
            onEmojiBackspaceClickListener = {},
            onEmojiClickListener = {}, theming = emojiTheming
        )
        requestLoadData()
    }

  private fun clickListener() {
        binding.layoutBottomEdit.editText.doAfterTextChanged {
            //文字或换行导致输入框超过两行更换输入框圆角背景
            if (binding.layoutBottomEdit.editText.lineCount > 1) {
                if (!is6dpRound) {
                    binding.layoutBottomEdit.llEditSpeak.setBackgroundResource(R.drawable.shape_white_radius_6)
                    is6dpRound = true
                }
            } else {
                binding.layoutBottomEdit.llEditSpeak.setBackgroundResource(R.drawable.shape_white_radius_20)
                is6dpRound = false
            }
        }
        binding.ivBack.setOnClickListener { dismissWithDelayRun() }
        binding.llTitle.setOnClickListener {
            albumListPopWindow.showAsDropDown(binding.tvTitle)
        }
        binding.viewSpace.setOnClickListener { dismissWithDelayRun() }
        binding.layoutBottomEdit.llEditSpeak.setOnClickListener { KeyboardUtils.showSoftInput(binding.layoutBottomEdit.editText) }
        binding.layoutBottomEdit.clBottomSendEdit.setOnLongClickListener { true }
        binding.layoutBottomEdit.emotionImageView.setOnClickListener {
            emojiPopup?.toggle()
        }
        binding.tvSelectNum.setOnClickListener {
//            switchAlbumPreviewAnimation()
        }
        binding.sendButton.setOnClickListener { dispatchTransformResult() }
    }

    /**
     * initAlbumListPopWindow
     */
    private fun initAlbumListPopWindow() {
        albumListPopWindow = AlbumListPopWindow.buildPopWindow(mActivity)
        albumListPopWindow.setOnPopupWindowStatusListener(object :
            AlbumListPopWindow.OnPopupWindowStatusListener {
            override fun onShowPopupWindow() {
                if (!config.isOnlySandboxDir) {
                    AnimUtils.rotateArrow(binding.ivArrow, true)
                }
            }

            override fun onDismissPopupWindow() {
                if (!config.isOnlySandboxDir) {
                    AnimUtils.rotateArrow(binding.ivArrow, false)
                }
            }
        })
        albumListPopWindow.setOnIBridgeAlbumWidget { position, curFolder ->
            val isDisplayCamera = config.isDisplayCamera && curFolder.bucketId == PictureConfig.ALL.toLong()
            mAdapter.setDisplayCamera(isDisplayCamera)
            if (position == 0) {
                binding.tvTitle.text = StringUtils.getString(R.string.album)
            } else {
                binding.tvTitle.text = curFolder.folderName
            }
            val lastFolder = SelectedManager.getCurrentLocalMediaFolder()
            val lastBucketId = lastFolder.bucketId
            if (curFolder.bucketId != lastBucketId) {
                // 1、记录一下上一次相册数据加载到哪了,到时候切回来的时候要续上
                val laseFolderData = ArrayList(mAdapter.data)
                lastFolder.data = laseFolderData
                lastFolder.currentDataPage = mPage
                lastFolder.isHasMore = binding.recyclerView.isEnabledLoadMore

                // 2、判断当前相册是否请求过,如果请求过则不从MediaStore去拉取了
                if (curFolder.data.size > 0 && !curFolder.isHasMore) {
                    setAdapterData(curFolder.data)
                    mPage = curFolder.currentDataPage
                    binding.recyclerView.isEnabledLoadMore = curFolder.isHasMore
                    binding.recyclerView.smoothScrollToPosition(0)
                } else {
                    // 3、从MediaStore拉取数据
                    mPage = 1
                    mLoader.loadPageMediaData(curFolder.bucketId, mPage, config.pageSize,
                        object : OnQueryDataResultListener<LocalMedia>() {
                            override fun onComplete(
                                result: ArrayList<LocalMedia>,
                                isHasMore: Boolean,
                            ) {
                                handleSwitchAlbum(result, isHasMore)
                            }
                        })
                }
            }
            SelectedManager.setCurrentLocalMediaFolder(curFolder)
            albumListPopWindow.dismiss()
            if (mDragSelectTouchListener != null && config.isFastSlidingSelect) {
                mDragSelectTouchListener!!.setRecyclerViewHeaderCount(if (mAdapter.isDisplayCamera()) 1 else 0)
            }
        }
    }

 创建一个透明的dialog,在完全显示之后重新设置一下主题,初始化获取数据源的配置,设置列表适配器,RecyclerView也是用的pictureselector库中的RV,满足长按滑动选择功能;初始化点击事件及标题栏媒体的弹框

    // 加载数据 判断权限
    private fun requestLoadData() {
        if (XXPermissions.isGranted(mActivity, Permission.Group.STORAGE)) {
            loadAllAlbumData()
        } else {
            XXPermissions.with(mActivity)
                // 申请多个权限
                .permission(Permission.Group.STORAGE)
                .request(object : OnPermissionCallback {
                    override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
                        if (!allGranted) {
                            val tips = StringUtils.getString(R.string.photos_media_missing_storage_permissions)
                            permissionsDialog(mActivity, permissions, tips)
                            return
                        }
                        loadAllAlbumData()
                    }

                    override fun onDenied(
                        permissions: MutableList<String>,
                        doNotAskAgain: Boolean,
                    ) {
                        if (doNotAskAgain) {
                            // 如果是被永久拒绝就跳转到应用权限系统设置页面
                            XXPermissions.startPermissionActivity(mActivity, permissions)
                        } else {
                            val tips =
                                StringUtils.getString(R.string.photos_media_missing_storage_permissions)
                            permissionsDialog(mActivity, permissions, tips)
                        }
                    }
                })
        }
    }

    override fun loadAllAlbumData() {
        preloadPageFirstData()
        mLoader.loadAllAlbum { localMediaFolder ->
            handleAllAlbumData(localMediaFolder)
        }
    }

    private fun handleAllAlbumData(result: List<LocalMediaFolder>) {
        if (ActivityCompatHelper.isDestroy(mActivity)) return
        if (result.isNotEmpty()) {
            val firstFolder = result[0]
            SelectedManager.setCurrentLocalMediaFolder(firstFolder)
            binding.tvTitle.text = StringUtils.getString(R.string.album)
            albumListPopWindow.bindAlbumData(result)
            binding.recyclerView.isEnabledLoadMore = true
        } else {
            showDataNull()
        }
    }

    override fun loadFirstPageMediaData(firstBucketId: Long) {
        mPage = 1
        binding.recyclerView.isEnabledLoadMore = true
        mLoader.loadPageMediaData(firstBucketId, mPage, mPage * config.pageSize,
            object : OnQueryDataResultListener<LocalMedia>() {
                override fun onComplete(result: ArrayList<LocalMedia>, isHasMore: Boolean) {
                    handleFirstPageMedia(result, isHasMore)
                }
            })
    }

    private fun handleFirstPageMedia(result: ArrayList<LocalMedia>, isHasMore: Boolean) {
        if (ActivityCompatHelper.isDestroy(mActivity)) return
        binding.recyclerView.isEnabledLoadMore = isHasMore
        if (binding.recyclerView.isEnabledLoadMore && result.size == 0) {
            // 如果isHasMore为true但result.size() = 0;
            // 那么有可能是开启了某些条件过滤,实际上是还有更多资源的再强制请求
            onRecyclerViewPreloadMore()
        } else {
            setAdapterData(result)
        }
    }

    override fun loadOnlyInAppDirectoryAllMediaData() {
        mLoader.loadOnlyInAppDirAllMedia { folder -> handleInAppDirAllMedia(folder) }
    }

    private fun handleInAppDirAllMedia(folder: LocalMediaFolder?) {
        if (!ActivityCompatHelper.isDestroy(mActivity)) {
            val sandboxDir = config.sandboxDir
            val isNonNull = folder != null
            val folderName = if (isNonNull) folder!!.folderName else File(sandboxDir).name
            binding.tvTitle.text = folderName
            if (isNonNull) {
                SelectedManager.setCurrentLocalMediaFolder(folder)
                setAdapterData(folder!!.data)
            } else {
                showDataNull()
            }
        }
    }

    /**
     * 加载更多
     */
    override fun loadMoreMediaData() {
        if (binding.recyclerView.isEnabledLoadMore) {
            mPage++
            val localMediaFolder = SelectedManager.getCurrentLocalMediaFolder()
            val bucketId = localMediaFolder?.bucketId ?: 0
            mLoader.loadPageMediaData(bucketId, mPage, config.pageSize,
                object : OnQueryDataResultListener<LocalMedia>() {
                    override fun onComplete(result: ArrayList<LocalMedia>, isHasMore: Boolean) {
                        handleMoreMediaData(result, isHasMore)
                    }
                })
        }
    }

    /**
     * 处理加载更多的数据
     */
    @SuppressLint("NotifyDataSetChanged")
    private fun handleMoreMediaData(result: ArrayList<LocalMedia>, isHasMore: Boolean) {
        if (ActivityCompatHelper.isDestroy(mActivity)) return
        binding.recyclerView.isEnabledLoadMore = isHasMore
        if (binding.recyclerView.isEnabledLoadMore) {
            removePageCameraRepeatData(result.toMutableList())
            if (result.isNotEmpty()) {
//                val positionStart: Int = mAdapter.data.size
                mAdapter.data.addAll(result)
                mAdapter.notifyDataSetChanged()
                if (mAdapter.data.isEmpty()) {
                    showDataNull()
                } else {
                    hideDataNull()
                }
            } else {
                // 如果没数据这里在强制调用一下上拉加载更多,防止是因为某些条件过滤导致的假为0的情况
                onRecyclerViewPreloadMore()
            }
            if (result.size < PictureConfig.MIN_PAGE_SIZE) {
                // 当数据量过少时强制触发一下上拉加载更多,防止没有自动触发加载更多
                binding.recyclerView.onScrolled(binding.recyclerView.scrollX,
                    binding.recyclerView.scrollY)
            }
        }
    }

    private fun removePageCameraRepeatData(result: MutableList<LocalMedia>) {
        try {
            if (config.isPageStrategy) {
                val iterator = result.iterator()
                while (iterator.hasNext()) {
                    if (mAdapter.data.contains(iterator.next())) {
                        iterator.remove()
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {

        }
    }

    private fun handleSwitchAlbum(result: ArrayList<LocalMedia>, isHasMore: Boolean) {
        if (ActivityCompatHelper.isDestroy(mActivity)) return
        binding.recyclerView.isEnabledLoadMore = isHasMore
        if (result.size == 0) {
            // 如果从MediaStore拉取都没有数据了,adapter里的可能是缓存所以也清除
            mAdapter.data.clear()
        }
        setAdapterData(result)
        binding.recyclerView.onScrolled(0, 0)
        binding.recyclerView.smoothScrollToPosition(0)
    }

    private fun setAdapterData(result: ArrayList<LocalMedia>) {
        mAdapter.setList(result)
        SelectedManager.clearAlbumDataSource()
        SelectedManager.clearDataSource()
        if (mAdapter.data.isEmpty()) {
            showDataNull()
        } else {
            hideDataNull()
        }
        if (isFirstLoadData && mAdapter.data.isNotEmpty()) {
            isFirstLoadData = false
            val delayMills = if (RomUtils.isSamsung()) ALERT_LAYOUT_TRANSLATION_DELAY else 0L
            mHandler.postDelayed({ mAdapter.notifyItemChanged(0, NOTIFY_DATA_CHANGE) }, delayMills)
        }
    }

选中的逻辑处理,Tegegram选中之后是有一个缩放的效果的,我这目前需求没有,就只加上一层遮罩,然后处理选中的数字排序,但由于用的是pictureselector库,那里面也有已经处理完的数字排序,就没有重复造轮子了,网上也是一搜一堆的,我就直接用库自带的了;然后设置一下字体大小,如果超过两位数的话那么重新设置一下,不然显示不下了。

//<editor-fold desc="相机事件回调处理">
//    ***********************  相机事件回调处理  ***********************
    /**
     * 相机事件回调处理
     */
    fun dispatchHandleCamera(intent: Intent?) {
        ForegroundService.stopService(mActivity)
        PictureThreadUtils.executeByIo(object : PictureThreadUtils.SimpleTask<LocalMedia?>() {
            override fun doInBackground(): LocalMedia? {
                val outputPath = getOutputPath(intent)
                if (!TextUtils.isEmpty(outputPath)) {
                    config.cameraPath = outputPath
                }
                if (TextUtils.isEmpty(config.cameraPath)) {
                    return null
                }
                if (config.chooseMode == SelectMimeType.ofAudio()) {
                    copyOutputAudioToDir()
                }
                return buildLocalMedia(config.cameraPath)
            }

            override fun onSuccess(result: LocalMedia?) {
                PictureThreadUtils.cancel(this)
                if (result != null) {
                    onScannerScanFile(result)
                    dispatchCameraMediaResult(result)
                }
            }
        })
    }

    /**
     * 尝试匹配查找自定义相机返回的路径
     *
     * @param data
     * @return
     */
    private fun getOutputPath(data: Intent?): String? {
        if (data == null) return null
//        var outPutUri = data.getParcelableExtra<Uri>(MediaStore.EXTRA_OUTPUT)
        val url = data.getStringExtra("url")
//        if (config.chooseMode == SelectMimeType.ofAudio() && outPutUri == null) {
//            outPutUri = data.data
//        }
//        if (outPutUri == null) {
//            return null
//        }
        return if (PictureMimeType.isContent(url.toString())) url.toString() else Uri.parse(url)
            .toString()
//        return if (PictureMimeType.isContent(outPutUri.toString())) outPutUri.toString() else outPutUri.path
    }

    /**
     * 刷新相册
     *
     * @param media 要刷新的对象
     */
    private fun onScannerScanFile(media: LocalMedia) {
        if (ActivityCompatHelper.isDestroy(mActivity)) return
        if (SdkVersionUtils.isQ()) {
            if (PictureMimeType.isHasVideo(media.mimeType) && PictureMimeType.isContent(config.cameraPath)) {
                PictureMediaScannerConnection(mActivity, media.realPath)
            }
        } else {
            val path =
                if (PictureMimeType.isContent(config.cameraPath)) media.realPath else config.cameraPath
            PictureMediaScannerConnection(mActivity, path)
            if (PictureMimeType.isHasImage(media.mimeType)) {
                val dirFile = File(path)
                val lastImageId = MediaUtils.getDCIMLastImageId(mActivity, dirFile.parent)
                if (lastImageId != -1) {
                    MediaUtils.removeMedia(mActivity, lastImageId)
                }
            }
        }
    }

    /**
     * buildLocalMedia
     *
     * @param absolutePath
     */
    private fun buildLocalMedia(absolutePath: String?): LocalMedia {
        val media: LocalMedia = LocalMedia.generateLocalMedia(mActivity, absolutePath)
        media.chooseModel = config.chooseMode
        if (SdkVersionUtils.isQ() && !PictureMimeType.isContent(absolutePath)) {
            media.sandboxPath = absolutePath
        } else {
            media.sandboxPath = null
        }
        if (config.isCameraRotateImage && PictureMimeType.isHasImage(media.mimeType)) {
            BitmapUtils.rotateImage(mActivity, absolutePath)
        }
        return media
    }

    /**
     * copy录音文件至指定目录
     */
    private fun copyOutputAudioToDir() {
        try {
            if (!TextUtils.isEmpty(config.outPutAudioDir) && PictureMimeType.isContent(config.cameraPath)) {
                val inputStream =
                    PictureContentResolver.getContentResolverOpenInputStream(
                        mActivity,
                        Uri.parse(config.cameraPath)
                    )
                val audioFileName: String = if (TextUtils.isEmpty(config.outPutAudioFileName)) {
                    ""
                } else {
                    if (config.isOnlyCamera) config.outPutAudioFileName else System.currentTimeMillis()
                        .toString() + "_" + config.outPutAudioFileName
                }
                val outputFile = PictureFileUtils.createCameraFile(
                    mActivity,
                    config.chooseMode, audioFileName, "", config.outPutAudioDir
                )
                val outputStream = FileOutputStream(outputFile.absolutePath)
                val isCopyStatus = PictureFileUtils.writeFileFromIS(inputStream, outputStream)
                if (isCopyStatus) {
                    MediaUtils.deleteUri(mActivity, config.cameraPath)
                    config.cameraPath = outputFile.absolutePath
                }
            }
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        }
    }

    private fun dispatchCameraMediaResult(media: LocalMedia) {
        val exitsTotalNum = albumListPopWindow.firstAlbumImageCount
        if (!isAddSameImp(exitsTotalNum)) {
            mAdapter.data.add(0, media)
        }
        // 进入下面 永远等于false
        if (config.selectionMode == SelectModeConfig.SINGLE && config.isDirectReturnSingle) {
            SelectedManager.clearSelectResult()
            val selectResultCode = confirmSelect(media, false)
            if (selectResultCode == SelectedManager.ADD_SUCCESS) {
                dispatchTransformResult()
            }
        } else {
            confirmSelect(media, false)
        }
        mAdapter.notifyItemInserted(if (mAdapter.isDisplayCamera()) 1 else 0)
        mAdapter.notifyItemRangeChanged(if (mAdapter.isDisplayCamera()) 1 else 0,
            mAdapter.getDefItemCountDataSize())
        if (config.isOnlySandboxDir) {
            var currentLocalMediaFolder = SelectedManager.getCurrentLocalMediaFolder()
            if (currentLocalMediaFolder == null) {
                currentLocalMediaFolder = LocalMediaFolder()
            }
            currentLocalMediaFolder.bucketId = ValueOf.toLong(media.parentFolderName.hashCode())
            currentLocalMediaFolder.folderName = media.parentFolderName
            currentLocalMediaFolder.firstMimeType = media.mimeType
            currentLocalMediaFolder.firstImagePath = media.path
            currentLocalMediaFolder.folderTotalNum = mAdapter.data.size
            currentLocalMediaFolder.currentDataPage = mPage
            currentLocalMediaFolder.isHasMore = false
            val data = ArrayList(mAdapter.data)
            currentLocalMediaFolder.data = data
            binding.recyclerView.isEnabledLoadMore = false
            SelectedManager.setCurrentLocalMediaFolder(currentLocalMediaFolder)
        } else {
            mergeFolder(media)
        }
        allFolderSize = 0
    }

    /**
     * 拍照出来的合并到相应的专辑目录中去
     *
     * @param media
     */
    private fun mergeFolder(media: LocalMedia) {
        val allFolder: LocalMediaFolder
        val albumList = albumListPopWindow.albumList
        if (albumListPopWindow.folderCount == 0) {
            // 1、没有相册时需要手动创建相机胶卷
            allFolder = LocalMediaFolder()
            val folderName: String = if (TextUtils.isEmpty(config.defaultAlbumName)) {
                if (config.chooseMode == SelectMimeType.ofAudio())
                    StringUtils.getString(com.luck.picture.lib.R.string.ps_all_audio)
                else
                    StringUtils.getString(com.luck.picture.lib.R.string.ps_camera_roll)
            } else {
                config.defaultAlbumName
            }
            allFolder.folderName = folderName
            allFolder.firstImagePath = ""
            allFolder.bucketId = PictureConfig.ALL.toLong()
            albumList.add(0, allFolder)
        } else {
            // 2、有相册就找到对应的相册把数据加进去
            allFolder = albumListPopWindow.getFolder(0)
        }
        allFolder.firstImagePath = media.path
        allFolder.firstMimeType = media.mimeType
        val data = ArrayList(mAdapter.data)
        allFolder.data = data
        allFolder.bucketId = PictureConfig.ALL.toLong()
        allFolder.folderTotalNum =
            if (isAddSameImp(allFolder.folderTotalNum)) allFolder.folderTotalNum else allFolder.folderTotalNum + 1
        val currentLocalMediaFolder = SelectedManager.getCurrentLocalMediaFolder()
        if (currentLocalMediaFolder == null || currentLocalMediaFolder.folderTotalNum == 0) {
            SelectedManager.setCurrentLocalMediaFolder(allFolder)
        }
        // 先查找Camera目录,没有找到则创建一个Camera目录
        var cameraFolder: LocalMediaFolder? = null
        for (i in albumList.indices) {
            val exitsFolder = albumList[i]
            if (TextUtils.equals(exitsFolder.folderName, media.parentFolderName)) {
                cameraFolder = exitsFolder
                break
            }
        }
        if (cameraFolder == null) {
            // 还没有这个目录,创建一个
            cameraFolder = LocalMediaFolder()
            albumList.add(cameraFolder)
        }
        cameraFolder.folderName = media.parentFolderName
        if (cameraFolder.bucketId == -1L || cameraFolder.bucketId == 0L) {
            cameraFolder.bucketId = media.bucketId
        }
        // 分页模式下,切换到Camera目录下时,会直接从MediaStore拉取
        if (config.isPageStrategy) {
            cameraFolder.isHasMore = true
        } else {
            // 非分页模式数据都是存在目录的data下,所以直接添加进去就行
            if (!isAddSameImp(allFolder.folderTotalNum)
                || !TextUtils.isEmpty(config.outPutCameraDir)
                || !TextUtils.isEmpty(config.outPutAudioDir)
            ) {
                cameraFolder.data.add(0, media)
            }
        }
        cameraFolder.folderTotalNum =
            if (isAddSameImp(allFolder.folderTotalNum)) cameraFolder.folderTotalNum else cameraFolder.folderTotalNum + 1
        cameraFolder.firstImagePath = config.cameraPath
        cameraFolder.firstMimeType = media.mimeType
        albumListPopWindow.bindAlbumData(albumList)
    }

    /**
     * 数量是否一致
     */
    private fun isAddSameImp(totalNum: Int): Boolean {
        return if (totalNum == 0) {
            false
        } else allFolderSize in 1 until totalNum
    }
    //    </editor-fold>

也是没啥可说的,打开相机,处理相机回调数据;基本与pictureselector库一致

    /**
     * @param isDismissDialog 不管执行什么操作  如果为true 那么执行完操作 会关闭弹框  默认为false
     */
    private fun dismissWithDelayRun(isDismissDialog: Boolean = false, block: (() -> Unit)? = null) {
        if (KeyboardUtils.isSoftInputVisible(mActivity)) {
            binding.emotionContainerFrameLayout.gone()
            KeyboardUtils.hideSoftInput(binding.layoutBottomEdit.editText)
            if (isDismissDialog) {
                dismissDelayRun(block)
            }
        } else {
            if (isDismissDialog) {
                dismissDelayRun(block)
            } else {
                // 如果有选中的  二次提示弹框
                if (SelectedManager.getSelectCount() > 0) {
                    val dialog = AlertDialog.Builder(activity)
                    dialog.setTitle("是否关闭dialog")
                    dialog.setMessage("是否放弃选择关闭dialog")
                    dialog.setNegativeButton("否") { d, _ ->
                        d.cancel()
                    }
                    dialog.setPositiveButton("是") { d, _ ->
                        d.dismiss()
                        dismissDelayRun(block)
                    }
                    dialog.show()
                } else {
                    dismissDelayRun(block)
                }
            }
        }
    }

    /**
     * @param block 关闭弹框
     */
    private fun dismissDelayRun(block: (() -> Unit)? = null) {
        SelectedManager.clearDataSource()
        SelectedManager.clearAlbumDataSource()
        SelectedManager.clearSelectResult()
        binding.nestedScrolling.setDoPerformAnyCallbacks(false)
        // 在关闭消失之前 添加动画
        dialog?.window?.setWindowAnimations(R.style.DialogBottomAnim)
        // 关闭之前先把颜色设置透明 在执行关闭
        dialog?.window?.statusBarColor = Color.TRANSPARENT
        binding.nestedScrolling.setBackgroundColor(0)
        mHandler.postDelayed({
            if (block != null) {
                block.invoke()
            } else {
                dismissAllowingStateLoss()
            }
        }, ALERT_LAYOUT_TRANSLATION_DELAY)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        if (mDragSelectTouchListener != null) {
            mDragSelectTouchListener!!.stopAutoScroll()
        }
    }

关闭DialogFragment,因为图片是可以添加说明的,所以需要先判断键盘,如果显示就先关闭键盘,再则判断是否有选中的图片,如果有的话 需要再次弹出一个确认提示框(这个图方便就快速写了一个,不标准,按照自己需求写就可以),如果确认关闭那么就清除选中的数据缓冲,添加一个DialogFragment的关闭动画。在销毁的时候把初始化长按滚动选中释放掉。

以上基本就是选中媒体图片的弹框了,我的需求查看图片跟Telegram的有点不太一样,还有查看预览的图文混排,目前注释掉了,如果后续有时间的话我会补上,会有一个图文混排和图片预览的介绍。

git地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值