Android 多层视差头部背景的实现
Android开发小白,还在实习阶段。请大佬们轻喷,谢谢!
前言
因为公司在做电影院线的手机应用,有一个需求是做如图的这种多层视差头部背景(multi-layer parallax background),原生且不使用第三方库。所以首先想到的就是直接使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout来实现最基础的视差背景效果。
平台:Android Studio, 语言:Kotlin
最终效果如图,经反复测试流畅无问题(如果后续测试有问题还会更新)。
想法
首先使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout布局来实现一个基本的带折叠效果的布局,可以自定义背景图片的大小和布局方式,想实现parallax只要添加“parallax” 属性就可以轻松实现。
然后就是如何添加另外多出来的这一层背景布局并给它不一样的移动速度,让我们看起来是有三层(背景+背景层内容+下方具体内容)layout带有三个不同的移动速度,造成多层视差效果。
背景层添加内容很容易,只要新建一个新的LinearLayout,构建好内容的布局,在AppbarLayout中include进来就可以了。因为这一部分的源码本质实际上是extend了一个FrameLayout,所以我们可以将多层内容重叠摆放在头部位置。然后在MainActivity中获取内容的id,这一步算完成了。
下面是最重要的一步,如何让这一部分的layout有不一样的上划速度,并且在惯性滑动过程中,也可以随时监测底部位置并更改自己本身的位置。
4.1.所有的触摸事件都绕不开三个大佬,
dispatchTouchEvent()
,InterceptedTouchEvent()
和onTouchEvent()
。所以果断重写CoordinatorLayout, 重写InterceptedTouchEvent()
和onTouchEvent()
,dispatchTouchEvent()
暂时不用管他。我们在InterceptedTouchEvent()
中截获手指在屏幕上的动作,然后根据我们的要求来分发事件。如果检测到手指是向上划的,就return true
把事件传递给onTouchEvent()
去处理。4.2 在新的CoordinatorLayout中,还要写一个open function来使Acticity可以将头部背景的图片传递过来,只有这样我们才能正常在新建的layout中处理图片位置和获取相关信息。这一点很重要,否则我们没法在这个文件里找到背景图片的代码位置(没法findViewById)。
fun getContent (content : LinearLayout, header:View, realcontent : View){this.content = contentthis.header = headerthis.realcontent = realcontentcontent.post {run{headerInitPosition = getViewPositionY(header).toFloat()headerContentInitPosition = getViewPositionY(content).toFloat()realContentInitPosition = getViewPositionY(realcontent).toFloat()System.out.println("init positions get : header-->$headerInitPosition, header content-->$headerContentInitPosition, real content-->$realContentInitPosition")}} } 复制代码
4.3 在
onTouchEvent()
中,实时检测底部的位置变化。这就需要我们在4.2所定义的方法中将三层内容的信息全部传递过来,方便我们在layout中检测和更改。InterceptTouchEvent()
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {parent.requestDisallowInterceptTouchEvent(true)when(ev!!.action){MotionEvent.ACTION_DOWN -> {isTouched = trueisDragging = falseinitX = ev.xinitY = ev.y}MotionEvent.ACTION_MOVE -> {isDragging = trueval draggedX = ev.x - initXval draggedY = ev.y - initYif (draggedY < 0){return true}}MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL -> {}}return super.onInterceptTouchEvent(ev) } 复制代码
onTouchEvent()
override fun onTouchEvent(ev: MotionEvent?): Boolean {when(ev!!.action){MotionEvent.ACTION_DOWN -> {}MotionEvent.ACTION_MOVE -> {val draggedX = ev.x - initXval draggedY = ev.y - initYprintln("$draggedY")println("content x: ${getViewPositionX(content)}, y: ${getViewPositionY(content)}")println("header image x: ${getViewPositionX(header)}, y: ${getViewPositionY(header)}")println("real content x: ${getViewPositionX(realcontent)}, y: ${getViewPositionY(realcontent)}")}MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL -> {}}return super.onTouchEvent(ev) } 复制代码
4.4 因为有惯性滑动的存在,我们不能在onTouchEvent中根据手指位置的移动来改变第二层layout的位置,所以在layout的onTouchEvent中我们只观察布局原件们的位置变化,最终的动作还是要在activity中完成。在Activity中,我们用一个handler和runneble,使用postDelayed来自定义一个每1ms执行一次的检测动作,来实时监测layout中各个原件的位置变化,来进行位置调整。
val handler = Handler()val runnable: Runnable = object : Runnable {override fun run() {val changedY = getViewPositionY(real_content) - realContentInitPositionprintln("real content $changedY")val threshold = 450if (getViewPositionY(real_content)<=threshold){val temp = threshold-toolbar_statusbar_heightval ratio = 1-(getViewPositionY(real_content)-toolbar_statusbar_height)/temptop_title.alpha = ratio}else{top_title.alpha = 0f}if (getViewPositionY(real_content).toFloat() == toolbar_statusbar_height){top_title.alpha = 1f}content.scrollY = (changedY/5).toInt()handler.postDelayed(this, 1)}}handler.postDelayed(runnable,1) 复制代码
4.5 既然在Activity中要处理布局的位置变化,我们就要先获取布局的初始位置并做出相应的位置调整,由于activity中的布局初始化比layout中的布局初始化要早执行,所以我们通过一个小的延时来在Activity中获取到所需的layout的初始位置坐标。
val handler1 = Handler()val runnable1 = Runnable {realContentInitPosition = getViewPositionY(real_content).toFloat()toolbar_statusbar_height = toolbar.layoutParams.height + getStatusBarHeight()}handler1.postDelayed(runnable1,100) 复制代码
另外,获取位置坐标的方法:(返回值即为Y轴坐标,
return position[0]
即返回x轴坐标)fun getViewPositionY(view: View):Int{val position = IntArray(2)view.getLocationOnScreen(position)return position[1] } 复制代码
如果在处理touchEvent的时候,发现动作意外的被父控件拦截或者捕捉不到动作了,一定要在
dispatchTouchEvent()
,InterceptedTouchEvent()
和onTouchEvent()
中加上parent.requestDisallowInterceptTouchEvent(true)
就OK了。还有如果发现在使用了自定义的新CoordinatorLayout之后,下部的NestedScrollView中的内容无法滑动了,再新建一个class然后像这样写一个新的NestedScrollView就行了。
class CustomeNestedScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : NestedScrollView(context, attrs, defStyleAttr) {override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {parent.requestDisallowInterceptTouchEvent(true)return super.dispatchTouchEvent(ev)}override fun onTouchEvent(ev: MotionEvent?): Boolean {parent.requestDisallowInterceptTouchEvent(true)return super.onTouchEvent(ev)}
}
复制代码
- 顶部标题的问题,由于没有使用behavior,所以还是在自己建立的实时检测循环里加入了改变顶部title透明度的代码,根据谷歌官方CoordinatorLayout给的出现遮罩层的位置和出现toolbar的位置,来调整title的alpha值,就OK了。代码也在4.4中有体现。
总结
不知道自己使用的postDelayed方法来一直不停的检测位置变化的方法是不是正确,是否会造成对软件运行流畅度的影响。如果各位有建议请提给我谢谢!
传送门:Github -- Multi-Layer-Parallax-Background
bug :
谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout 布局有一个bug,至今据我测试还没有修复,就是如果在调整了头部背景的高度的时候,很容易在向下滑动的时候从头部图片滑动,如果手指离开屏幕布局进入惯性滑动fling阶段,在惯性滑动没有停止之前重新滑动屏幕(非头部区域),布局会产生抖动而且无法控制。这是因为当开始从头部滑动时,该动作被头部layout处理,产生的fling也是由它产生的,我们没有办法从外部停止这个fling,如果在这个时候触摸屏幕而且触摸点在非头部背景区域,这个动作就会和之前的惯性滑动动作冲突。
查过解决方案,也尝试过手动解决这个问题但是并没有奏效。用反射的方法获取父类的父类的父类中的overScroller和flingRunnable对象,在自定义的layout中用set
方法手动注入我们自己的scroller,这样我们就可以控制惯性滑动的动作并随时使用abortAnimation()
停止fling。如果有大神有更好的办法请赐教!
转载于:https://juejin.im/post/5cdc256ff265da03a54c41a3
Android 多层视差头部背景的实现相关推荐
- [转]android的selector,背景选择器
本文转自:http://dev.10086.cn/cmdn/wiki/index.php?doc-view-6014.html 最近做listview和button都要改变Android原来控件的背景 ...
- android relativelayout 点击事件,Android Relativelayout点击背景行为
所以我有一个RecyclerView,它是由我的自定义布局(代码如下)填充.我制作了一个可绘制的背景,并将其设置在我的自定义布局的根部,以便用户单击该项目时,背景颜色会发生变化.每个项目的布局上还有一 ...
- 浅谈android的selector,背景选择器
2019独角兽企业重金招聘Python工程师标准>>> 关于listview和button都要改变android原来控件的背景,在网上查找了一些资料不是很全,所以现在总结一下andr ...
- android recyclerview添加头部,Android RecyclerView添加Header头部
Android RecyclerView添加Header头部 Android RecyclerView不像以前的ListView那样直接添加头部,如果要给RecyclerView增加头部,则需要 ...
- Android如何设置渐变色背景 渐变shape
Android如何设置渐变色背景 Android开发过程中,会用到android:backgroud属性来设置背景的颜色,一般情况下我们直接设置一个类似#FFFF0000的值代表是背景颜色,如果想设置 ...
- Android 修改默认的背景壁纸(msm8909)
Android 修改默认的背景壁纸 第一步定位文件: /frameworks/base/core/res/res中的drawable-sw720dp-nodpi .drawable-sw600dp-n ...
- html+css网页开发实战——1、头部背景和文字的制作
网页成品图: 一. 自己动手实践: 首先,把网页分为三大部分,定义了三个div,class分别为header,center,footer.然后在定义它们里面的div.代码如下: <!DOCTYP ...
- Android样式和主题背景
简介: 本文将简单介绍Android样式与主题背景的相关内容 文章目录 前言 一.样式 二.主题背景 三.样式层次结构 四.创建并应用样式 五.自定义默认主题 六.添加特定于版本的样式 七.常见的主题 ...
- Android样式和主题背景·
简介: 本文将简单介绍Android样式与主题背景的相关内容 文章目录 前言 一.样式 二.主题背景 三.样式层次结构 四.创建并应用样式 五.自定义默认主题 六.添加特定于版本的样式 七.常见的主题 ...
- android开发 listview 头部 轮播,listview添加的头部布局超过一屏头部内容显示不全...
headView的实际高度超过一个屏幕,但是显示的结果只有一个屏幕,超过一个屏幕高度意外的部分显示不全. 只使用了listView.getRefreshable().addHeadView(headV ...
最新文章
- arc0 oracle,ORA-01194错误恢复方法一
- OpenCV学习笔记之图像融合
- python中的栈结构_python中有栈吗
- C++ 变量判定的螺旋法则
- 数据科学竞赛-文本分类
- 可以用计算机进行模拟实验,随着信息技术的发展,包括核实验在内的许多科学研究都可以用计算机进行模拟实验, - 问答库...
- Jquery——hover与toggle
- 3.FreeRTOS学习笔记-任务
- php 字符 index,php函数之字符串篇String
- 全国计算机二级C 错题/知识点整理
- java序列化,看这篇就够了
- 【机房收费系统C#版】——导出Excel
- MySQL破log_MySQL中的binlog相关命令和恢复技巧
- java输入一个矩阵顺时针打印_剑指Offer(Java版):顺时针打印矩阵
- jquery 获取 选中的radio的值
- 高通工具QXDM、QCAT和QPST关系及功能
- 小技巧(12):关于PC端简单的视频剪辑处理中,bandicam(录制)、pr(配音)、pr(导出)、剪映(字幕识别)、pr(最终版导出)的全过程及基础设置
- BPE, WordPiece, SentencePiece
- 网易微专业Android实战教程
- C语言实现组合式的计算