Android仿抖音右滑清屏左滑列表功能的实现代码

概述

​ 项目中要实现仿抖音直播间滑动清屏,侧滑列表的功能,在此记录下实现过程和踩坑记录希望避免大家走些弯路,也当作自己的一个总结

​ 首先看下Demo中的效果

​ 阅读文章需要提前熟悉些事件分发的内容,相信大家都已经了解过了,网上也有很多优秀的文章,这里推荐两篇自己读过印象较深的文章

https://www.jb51.net/article/124249.htm

https://www.jb51.net/article/124861.htm

关于这方面的知识,在Android中是再重要不过的了,是迟早都要掌握的知识,所以还是希望大家都能提早掌握,最好可以跟着源码一起分析,理解掌握的更深刻一点

实践

所以网上基于这部分内容讲解已经很详细了,这里就不再搬砖了,主要分享一下自己项目中结合这部分知识运用过程中产生的一些想法和经验,解决的一些bug

以上就是功能在实现过程中要解决的问题,下面详细展开

1. 布局结构

​ 布局结构始终是界面设计时首先要考虑的一个问题,从接到一个需求开始,首先要根据项目中现有的布局结构,考虑如何更优雅的嵌入布局层次。如果一不小心,走上了错误的实现道路,那么不好意思,即使功能最后实现了,到了后期,也有千万种理由迫使你不得不走上重构的道路。

​ 比如实现不合理,导致的布局结构复杂,嵌套冗余层次,比如代码业务逻辑处理复杂蹩脚,比如资源浪费,内存消耗过多等等。虽然功能好使,使用起来也没有差别,但是,作为一个有追求的程序员,我们还是要避免这种情况的发生不是吗

不巧的是,本文就属于上述踩坑记录,下面详细分析

 1.1 初步实现

​ 上来以后,思路很直接明了的去想要实现清屏和滑屏的功能是每个房间都有的功能,每个房间又都是一个RecyclerView 的一个Item。所以,很明显在Item的布局上包一层,实现清屏和侧滑列表的功能就可以了,这样每个房间都可以上下滑,切换房间。切换以后,滑屏的功能是在每个房间里的,互不影响,所以很好理解

我们项目中实现直播间上下滑切换的功能是RecyclerView + 自定义LinearLayoutManager实现的,这部分内容网上demo很多,就不展开了

​ 具体实施,是自定义布局继承RelativeLayout,解析自定义的布局文件,里面包含,直播间的房间布局,和自己右侧滑块儿布局,然后用自己实现的布局替换之前的房间Item布局位置

  • 由于我们自定义的Container布局是继承子RelativeLayout实现的,内部三个子View 又全部是占满父布局的,所以就是三层覆盖的效果,类似抖音直播间效果
  • 这里我们尽量将覆盖层/清屏控件,封装成一个ViewGroup 内部包含了上边细分的各个子View,例如头部个人信息,头像列表等等;中间弹幕,SVGA礼物展示区域;底部聊天评论区域方便管理
  • 还有右侧滑块我们也做成继承自RelativeLayout形式,解析自定义布局,方便扩展

这样我们调用封装的Container将清屏控件,和右侧滑块儿布局View分别添加到内部即可

API提供如下

// 添加需要清屏的view
 fun addClearViews(vararg views: View?)
 // 添加需要滑入的view
 fun addSlideView(view: RightSlideLayout)

这样我们在视频播放页面滑动,就可以在Container内判断手势,处理清屏控件或者滑出右侧滑块儿了

右侧滑块再动态加载Fragment,展示列表布局,基本完成功能效果了

1.2 重构

​ 本来以为开开心心的可以上线了,谁知到下边继续体验和对比抖音到过程中还是发现不足:

第一个是,右侧滑块儿(后边称RightSlider)包含在房间,这样上下切换房间(后边称Container),RightSlider布局也会随着Container新建而新建,虽然有RecyclerView的布局缓存,但是至少也会新建Holder几次,造成资源的浪费。第二个是,RightSlider的新建就会导致里边的Fragment的新建,所以又会重新请求加载列表数据,再次造成资源浪费,而且,新建后右侧列表又会重新顶到头,之前滑动过的距离就会丢失。这样就造成,用户从右侧列表点击切换房间后,再次滑出RightSlider切换房间,发现又要从头开始往下滑,这样肯定不符合用户体验。观察抖音列表后发现,每次滑动到固定位置点击Item切换房间后,再次滑出滑块儿,发现列表还是之前的位置,好像跟之前滑出的是一个滑块儿的效果,于是恍然大悟,滑块儿是跟Activity绑定的,也就是要把RightSlider放在跟Activity布局那一层

​ 其实提出RightSlider到外层的过程中,还是走了不少弯路,因为之前毕竟已经实现好的逻辑,如果改动布局结构,肯定要重写滑动冲突、事件分发这部分代码,工作量又不可预计。所以想着能不能不动布局结构的情况下实现仿抖音效果

动态替换Fragment

​ 首先想到的是滑出RightSlider里的列表每次都好像是同一个,那么保证里边的Fragment是同一个不就好了,滑出的滑块儿虽然不同,但是里边装载的Fragment列表是同一个,这样就营造出同一个滑块儿的效果。

​ 但是实现过程中还是出现了问题,由于RecyclerView的预加载功能,导致我们项目中,从第一个房间上滑到下一个房间,过程中会新建两个Holder,这样Fragment替换就出了问题,切换房间后Fragment添加不上去,折腾一下午后最终放弃这个方案

固定List高度

​ 然后想的,既然Fragment替换不了了,那么RecyclerView肯定不是同一个了,如果点击后记录当前RecyclerView滑动的位置,下次滑出时,代码固定到当前位置不是也可以伪造出同一个滑块儿的效果嘛,这部分也去找了一些资料,实现了个小demo。其中用到的主要方法是

/**
    * 获取滑动距离
    */
    fun getScollYDistance(): Int {
    // 获取recyclerview 的layoutManager
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        // 获取当前第一个可见View的位置
        val position = layoutManager.findFirstVisibleItemPosition()
        // 根据position 获取当前View
        val firstVisiableChildView = layoutManager.findViewByPosition(position)
        // 获取当前View 高度
        val itemHeight = firstVisiableChildView.height
        // 滑动距离
        return position * itemHeight - firstVisiableChildView.top
    }

滑动距离计算的思想是:根据当前可见View 的position * 每个ItemView 的高度 + 当前View已经滑出去的部分

​ 计算出高度后,每次加载时,调用RecyclerView的API

recyclerView.scrollBy(0,scroll) //scroll 刚才计算的高度

还有其他几个滑动的方法:

// 带动画移动距离
public void smoothScrollBy(int dx, int dy)
// 带动画移动到position
public void smoothScrollToPosition(int position)
// 移动到adapter position ,由LayoutManager实现
public void scrollToPosition(int position)
// 空实现,无效
public void scrollTo(int x, int y)

原理上可以实现,但是最后综合比较还是放弃了这种方式,因为总感觉这种方法属于投机取巧不是正道,还是老老实实将RightSlider 提到外面得了

 2. 动画

​ 动画也是这个功能中很重要的一个方面,因为动画效果的流畅直接影响了用户体验,所以这方面也是细扣了很久。首先这个功能主要分成三个动画效果:

 2.1 进场出场

​ 包含清屏控件入场、出场:

mClearAnimator = ValueAnimator.ofFloat(0f, 1.0f).setDuration(300)
        mClearAnimator.addUpdateListener(ValueAnimator.AnimatorUpdateListener { valueAnimator ->
            val value = valueAnimator.animatedValue as Float
            translateClearChild((startX + value * (endX - startX)).toInt())
        })
        mClearAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                isCleared = !isCleared
            }
        })

这里使用了属性动画ValueAnimator,其中 translateClearChild 负责移动View 代码如下:

/**
    * 移动清屏控件
    */
    private fun translateClearChild(translate: Int) {
        for (i in mClearViews.indices) {
            mClearViews[i].translationX = translate.toFloat()
        }
    }

​ 滑块儿的入场、出场:

mSlideInAnimator = ValueAnimator.ofFloat(0f, 1.0f).setDuration(500)
// 设置减速拦截器
mSlideInAnimator.interpolator = DecelerateInterpolator(3f)
mSlideInAnimator.addUpdateListener(ValueAnimator.AnimatorUpdateListener { valueAnimator ->
            val value = valueAnimator.animatedValue as Float
            translateSlideView((startX + value * (endX - startX)).toInt())
        })
mSlideInAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationStart(animation: Animator) {
                mSlideView!!.visibility = View.VISIBLE
                mBgColorView.isClickable = true
            }
            override fun onAnimationEnd(animation: Animator) {
                if (!isSlideShow && translateX == 0) {
                    isSlideShow = !isSlideShow
                } else if (isSlideShow && abs(translateX) == width - mSlideView!!.paddingLeft) {
                    isSlideShow = !isSlideShow
                }
                if (!isSlideShow) {
                    parent.requestDisallowInterceptTouchEvent(false)
                    mSlideView!!.visibility = View.GONE
                    removeView(mBgColorView)
                    addView(mBgColorView, childCount - 4)
                }
                isSliderGoning = false
            }
        })

这里startX,endX 分别代表入场和出场时候,动画起止位置。由于清屏控件没有中间位置状态,直接是从0 到屏幕宽度两个值之间替换;而滑块儿中间由于要跟随手势移动,所以要记录中间translateX,标记为startX

 2.2 跟随手势

​ 跟随手势实现主要是拦截移动手势,根据按下手势位置坐标和Move移动位置坐标的差值,调用移动SliderView的方法

val x = event.rawX.toInt()
// 标记移动距离
val offsetX = x - mDownX
when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                if ((isSlideShow) && offsetX > 0 && mSlideInAnimator.isRunning && !isSliderGoning) {
                    // 滑入情况下,向右滑一段松开,再向右滑,清除回弹动画,跟随手势
                    mSlideInAnimator.cancel()
                    translateSlideView(offsetX)
                }
                if ((isSlideShow) && offsetX > 0 && !mSlideInAnimator.isRunning) {
                    // 滑入情况下,向右滑,跟随手势
                    translateSlideView(offsetX)
                }
                return true
            }
      }

 2.3 颜色渐变

​ 跟随手势滑动过程中还伴随的左侧空白区域颜色渐变,这部分可以在RightSlider移动过程中的距离值关联起来,设置起始颜色透明和截止颜色灰色蒙层。再根据距离动态算出当前颜色在区间范围内取值,主要代码逻辑如下

/**
    * 移动滑块儿
    */
    private fun translateSlideView(translate: Int) {
        val percent = (mSlideView!!.width.toFloat() - translate) / mSlideView!!.width
        // 根据百分比算出色值
val color = (MASK_DARK_COLOR * percent).toInt() shl 24
        // 动态设置背景色渐变
        mBgColorView.setBackgroundColor(color)
        translateX = translate
        mSlideView!!.translationX = translate.toFloat()
    }

 3 事件分发

​ 这部分可以说是本功能实现的核心,也是耗费了相当时间的精力,从最开始的Container包含RightSlider布局处理经典的事件分发顺序,到最后重构布局,将RightSlider提到外层变成不是包含关系,而是并列或者说是覆盖关系,中间对事件传递的顺序理解又深入了一层

3.1 传递顺序

​ 重构之前的布局结构是每个Container包含了一个RightSlider,两个是一个整体使用的,滑动的逻辑都可以在Container层内的onInterceptTouchEvent方法内处理。判断是否拦截事件即可,然后RightSlider内想要禁止父层Container拦截事件,可以使用parent.requestDisallowInterceptTouchEvent(true)禁止父层拦截;是属于经典模式的事件分发模型,事件分发的顺序在一个U型结构里,比较好处理

​ 然后重构以后布局结构变成了如下图所示

​ 每个Container 共用一个RightSlider,这样属于事件的分发处理不在一个ViewGroup的U型模型里了,这样的分发顺序也是属于自己的一个大胆尝试,想着实在不行,还是要把Activity内布局包一层,将Container和RightSlider 放在一个U型结构里去处理。

​ 还好最后不断踩坑,终于实现了事件从Activity分发,到RightSlider,再分发到Container的过程

​ 这里贴下Demo里的布局实现:

<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    tools:context=".MainActivity">
    <com.fxf.slide.SlideContainerLayout
        android:id="@+id/layout_slider_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:id="@+id/ll12"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="100dp"
            android:background="#00f"
            android:orientation="vertical">
            <TextView
                android:id="@+id/tv111"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="111111111"
                android:textColor="#fff" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="222222222"
                android:textColor="#fff" />
        </LinearLayout>
    </com.fxf.slide.SlideContainerLayout>
    <com.fxf.slide.RightSlideLayout
        android:id="@+id/layout_right_slider"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="60dp"
        android:visibility="gone">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/shape_slider_background">
            <View
                android:id="@+id/live_slide_bar"
                android:layout_width="4.5dp"
                android:layout_height="90dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="5dp"
                android:layout_marginRight="5dp"
                android:background="@drawable/shape_slider_dark_bar" />
            <FrameLayout
                android:id="@+id/list_fragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_toRightOf="@+id/live_slide_bar" />
        </RelativeLayout>
    </com.fxf.slide.RightSlideLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

其中做了部分简化,主要帮助大家理解布局层次

​ 然后贴下RightSlider核心分发代码:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
// 获取坐标,这里用rawX 相对屏幕绝对位置,不然随手势移动过程中父布局的移动,导致获取的坐标左右抖动,会出现移动过程中左右一直抖动现象
        val x = event.rawX.toInt()
        val y = event.rawY.toInt()
// X方向位移
        val offsetX = x - mDownX
        if (!mSlideContainerLayout.isSlideShow){
        // Container滑块儿没滑出来不分发事件
            return false
        }
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
            // 记录按下点坐标
                mDownX = x
                mDownY = y
                mSlideContainerLayout.setDownXY(mDownX,mDownY)
            }
            MotionEvent.ACTION_MOVE -> if (abs(x - mDownX) < abs(y - mDownY) && paddingLeft < x) { // 上下滑动情况处理
                if (isSlideHorizontal) {
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
            } else if ( offsetX < 0 && mSlideContainerLayout.isAlignLeftSide()) {
            // 向左滑动,滑块儿已经靠最左边了,不分发
                return super.dispatchTouchEvent(event)
            } else if (abs(x - mDownX) > abs(y - mDownY)){
            // 水平方向移动,分发事件
                isSlideHorizontal = true
                    return mSlideContainerLayout.dispatchTouchEvent(event)// 事件传递给Container处理
            }
            MotionEvent.ACTION_UP,
            MotionEvent.ACTION_CANCEL ->{
            // 抬起时处理
                if (offsetX < 0 && mSlideContainerLayout.isAlignLeftSide()){
                    return super.dispatchTouchEvent(event)
                }
                if (abs(x - mDownX) > abs(y - mDownY) || isSlideHorizontal){
                    isSlideHorizontal = false
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
                isSlideHorizontal = false
            }
        }
        return super.dispatchTouchEvent(event)
    }

3.2 滑动冲突

因为房间是可以上下滑动的,所以可以判断如果滑块儿没滑粗来时,直接返回分发,不让RightSlider和Container处理事件

if (!mSlideContainerLayout.isSlideShow){
 return false
 }

然后滑块儿滑出来以后,因为里边有列表,所以要消费上下滑动事件,可以处理如下:

MotionEvent.ACTION_MOVE -> if (abs(x - mDownX) < abs(y - mDownY) && paddingLeft < x) {
                if (isSlideHorizontal) {
                    return mSlideContainerLayout.dispatchTouchEvent(event)
                }
            }

其中paddingLeft < x 是因为滑块左边有一部分空白区域 paddingLeft ,所以当x坐标在此区域右侧时才处理事件

Container动画执行过程中,说明正在消费事件,此时禁止父层拦截事件

if (mClearAnimator.isRunning || mSlideInAnimator.isRunning || isSlideShow) {
              // 滑入情况下,禁止上下滑切换直播间
              parent.requestDisallowInterceptTouchEvent(true)
    }

Container处理事件时候和直播间上的进入房间头像列表冲突,解决方法是判断mDownY 大于进入头像列表高度时才处理事件,因为正常人滑入滑块都是在屏幕中下部操作的,所以太靠上的部分不处理事件也可以接受

MotionEvent.ACTION_MOVE -> {
                if (!mClearAnimator.isRunning && mDownY > 200 && abs(x - mDownX) > abs(y - mDownY)) {
                    // 清屏不在执行时 && 高度大于200dp(解决进入房间头像滑动冲突)&& 横向滑动时拦截事件
                    if (abs(x - mDownX) > 10) {
                        return true
                    }
                }
            }

3.3 滑动优化

​ 这部分有很多细节处理的地方,包括动画执行到一半情况下,再次左右滑动,先向左后向右,左右滑一半再上下滑等等各种情况具体可以看代码中SlideContainerLayout中onTouchEvent方法内处理逻辑,都添加了注释

override fun onTouchEvent(event: MotionEvent): Boolean {
        mVelocityTracker!!.addMovement(event)
        val x = event.rawX.toInt()
        val offsetX = x - mDownX
        if (mLastOffsetList.size > 2){
            mLastOffsetList.removeFirst()
        }
        mLastOffsetList.add(offsetX)
        var slideRight = (offsetX - mLastOffsetList.first) > 0
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                if ((isSlideShow) && offsetX > 0 && mSlideInAnimator.isRunning && !isSliderGoning) {
                    // 滑入情况下,向右滑一段松开,再向右滑,清除回弹动画,跟随手势
                    mSlideInAnimator.cancel()
                    translateSlideView(offsetX)
                }
                if ((isSlideShow) && offsetX > 0 && !mSlideInAnimator.isRunning) {
                    // 滑入情况下,向右滑,跟随手势
                    translateSlideView(offsetX)
                }
                return true
            }
            MotionEvent.ACTION_UP -> {
                mVelocityTracker!!.computeCurrentVelocity(10)
                if (isSlideShow && offsetX > 0 && abs(offsetX) > width / 3 && !isSliderGoning && mVelocityTracker!!.xVelocity >= 0) {
                    // 滑入情况下,向右滑距离超过宽度1/3,滑出滑块
                    startX = offsetX
                    endX = width - mSlideView!!.paddingLeft
                    isSliderGoning = true
                    mSlideInAnimator.start()
                    return true
                }
                if (abs(mVelocityTracker!!.xVelocity) > 1) {
                    if (isCleared && offsetX < 0) {
                        // 清屏情况下,左滑速度超过10个像素时 ===》滑入清屏控件
                        layerShowWithAnim()
                    } else if (!isCleared && offsetX > 0 && !isSlideShow && !mSlideInAnimator.isRunning) {
                        // 未清屏 && 向右速度 > 10 && 没滑入滑块 && 滑块动画没执行的时候 ===》清屏
                        layerGoneWithAnim()
                    } else if (isSlideShow && offsetX > 0 && slideRight) {
                        // 滑入情况下 && 向右速度 > 10 ===》滑出滑块
                        mSlideInAnimator.cancel()
                        isSliderGoning = true
                        startX = translateX
                        endX = width - mSlideView!!.paddingLeft
                        mSlideInAnimator.start()
                    } else if (isSlideShow && offsetX < 0 && translateX != 0) {
                        // 滑入情况下 && 向左速度 > 10 && 已经向右滑动了一段距离 ===》 滑块回弹
                        startX = translateX
                        endX = 0
                        mSlideInAnimator.start()
                    } else if (!isSlideShow && offsetX < 0 && !mSlideInAnimator.isRunning) {
                        // 没滑入情况下 && 向左滑速度 > 10 && 没右正在滑入情况下 ===》 滑入滑块
                        sliderShowWithAnim()
                    } else {
                        if (isSlideShow && translateX != 0) {
                            // 滑入情况下 && 已经向右滑动过,速度没达到松开 ===》回弹
                            startX = translateX
                            mSlideInAnimator.start()
                        }
                    }
                }else {
                    if (isSlideShow && translateX != 0) {
                        // 滑入情况下 && 已经向右滑动过,速度没达到松开 ===》回弹
                        startX = translateX
                        mSlideInAnimator.start()
                    }
                }
                return super.onTouchEvent(event)
            }
            MotionEvent.ACTION_CANCEL -> {
                if (isSlideShow) {
                    //取消事件时,滑入情况下回弹
                    startX = translateX
                    mSlideInAnimator.start()
                }
            }
        }
        return super.onTouchEvent(event)
    }

总结

​ 最后通过这次实践,感触比较深的是功能实现之前,一定要做好充分的调研,研究好需求的细节,并预先想几种实现策略,对比哪一种更合理。不要埋头就写,结果最后发现不符合需求还要重构

​ 感谢,这里Contanier内的逻辑主要参考了gitHub上[这篇文章](https://github.com/lmxjw3/clearscreen )的处理不过里边处理滑动冲突的逻辑比较少还是要自己结合项目处理

奉上GitHub 项目地址

项目地址

总结

到此这篇关于Android仿抖音右滑清屏左滑列表功能的实现代码的文章就介绍到这了,更多相关android 抖音右滑清屏左滑列表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android实现左滑删除列表功能

    本文实例为大家分享了Android自定义左滑删除列表的具体代码,供大家参考,具体内容如下 1.布局文件view_left_slide_remove.xml 包含一个RelativeLayout和TextView,RelativeLayout是实际界面,TextView是删除按钮. <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://sch

  • Android仿QQ列表左滑删除操作

    最近学习了如何做一个像QQ的左滑RecyclerView的item显示选项的,主要是用到Scroller 我们首先新建一个自己的RecyclerView 定义好一些要用的的变量 重写构造方法,把前两个构造方法改为如下,使无论如何构造都要执行第三个构造方法 在第三个构造方法里初始化Scroller public class LeftSwipeMenuRecyclerView extends RecyclerView { //置顶按钮 private TextView tvTop; //删除按钮 p

  • Android仿抖音右滑清屏左滑列表功能的实现代码

    概述 ​ 项目中要实现仿抖音直播间滑动清屏,侧滑列表的功能,在此记录下实现过程和踩坑记录希望避免大家走些弯路,也当作自己的一个总结 ​ 首先看下Demo中的效果 ​ 阅读文章需要提前熟悉些事件分发的内容,相信大家都已经了解过了,网上也有很多优秀的文章,这里推荐两篇自己读过印象较深的文章 https://www.jb51.net/article/124249.htm https://www.jb51.net/article/124861.htm 关于这方面的知识,在Android中是再重要不过的了

  • Android仿抖音主页效果实现代码

    写在前面 各位老铁,我又来啦!既然来了,那肯定又来搞事情啦,话不多说,先上图! "抖音"都玩过吧,是不是很好玩,我反正是天天刷,作为一个非著名的Android低级攻城狮,虽然技术菜的一匹,但是也经常刷着刷着会思考:咦?这玩意是用哪个控件做的?这个效果是咋实现的啊?由于本人技术水平有限,所以今天咱就先挑个比较简单的来看看是如何实现的,思考再三,我们就拿抖音首页的这个效果来练练手吧,话不多说,开搞! 一.准备工作 我们先不急着写代码,先对抖音的这种效果做一个简单的分析,首先需要明确的是它是

  • Android 仿抖音的评论列表的UI和效果的实现代码

    抖音是一款音乐创意短视频社交软件,是一个专注年轻人的15秒音乐短视频社区.用户可以通过这款软件选择歌曲,拍摄15秒的音乐短视频,形成自己的作品.此App已在Android各大应用商店和APP Store均有上线. 在design包里面 有一个 BottomSheetDialogFragment 这个Fragment,他已经帮我们处理好了手势,所以实现起来很简单.下面是代码: public class ItemListDialogFragment extends BottomSheetDialog

  • Android仿抖音列表效果

    本文实例为大家分享了Android仿抖音列表效果的具体代码,供大家参考,具体内容如下 当下抖音非常火热,是不是也很心动做一个类似的app吗? 那我们就用RecyclerView实现这个功能吧,关于内存的回收利用就交给RecyclerView就好了. 首先我们先说3个和视频播放暂停相关的接口 public interface OnViewPagerListener { /** * 初始化 */ void onInitComplete(View view); /** * 释放 */ void onP

  • Android仿抖音上下滑动布局

    抖音上下滑动,监听播放,自动吸顶,吸底效果,供大家参考,具体内容如下 使用RecyclerView+PagerSnapHelper实现 public class DouYinLayoutManager extends LinearLayoutManager implements RecyclerView.OnChildAttachStateChangeListener{ //判断是否上滑还是下滑 private int mDrift; private OnViewPagerListener on

  • 基于vue+uniapp直播项目实现uni-app仿抖音/陌陌直播室功能

    一.项目简介 uni-liveShow是一个基于vue+uni-app技术开发的集小视频/IM聊天/直播等功能于一体的微直播项目.界面仿制抖音|火山小视频/陌陌直播,支持编译到多端(H5.小程序.App端) 且兼容效果一致. 二.效果预览 在H5.小程序.App端测试效果如下:(后续大图均为APP端) 三.使用技术 编码器+技术:HBuilderX + vue/NVue/uniapp/vuex iconfont图标:阿里字体图标库 自定义导航栏 + 底部Tabbar 弹窗组件:uniPop(un

  • Android 之BottomsheetDialogFragment仿抖音评论底部弹出对话框效果(实例代码)

    实现的效果图: 自定义Fragment继承BottomSheetDialogFragment 重写它的三个方法: onCreateDialog() onCreateView() onStart() 他们的执行顺序是从上到下 import android.app.Dialog; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable;

  • Android自定义view实现仿抖音点赞效果

    前言 学习自定义view,想找点东西耍一下,刚好看到抖音的点赞效果不错,尝试一下. 抖音效果: 话不多说,先上代码: public class Love extends RelativeLayout { private Context mContext; float[] num = {-30, -20, 0, 20, 30};//随机心形图片角度 public Love(Context context) { super(context); initView(context); } public

  • Android 使用SwipeRefreshLayout控件仿抖音做的视频下拉刷新效果

    SwipeRefreshLayout(这个控件),我先跟大家介绍一下这个控件: 一.SwipeRefreshLayout简单介绍 •先看以下官方文档,已有了很详细的描述了. 官方文档说明 •这里我再大概解释一下: •在竖直滑动时想要刷新页面可以用SwipeRefreshLayout来实现.它通过设置OnRefreshListener来监听界面的滑动从而实现刷新.也可以通过一些方法来设置SwipeRefreshLayout是否可以刷新.如:setRefreshing(true),展开刷新动画. s

  • Android自定义videoview仿抖音界面

    本文实例为大家分享了Android自定义videoview仿抖音界面的具体代码,供大家参考,具体内容如下 1.效果图 和抖音的界面效果一模一样,而且可以自定义,需要什么页面,请自己定义 2.自定义videoview package com.example.myapplication20; import android.content.Context; import android.util.AttributeSet; import android.widget.VideoView; /** *

随机推荐