Android开发小白,还在实习阶段。请大佬们轻喷,谢谢!git
由于公司在作电影院线的手机应用,有一个需求是作如图的这种多层视差头部背景(multi-layer parallax background),原生且不使用第三方库。因此首先想到的就是直接使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout来实现最基础的视差背景效果。github
平台:Android Studio, 语言:Kotlinide
最终效果如图,经反复测试流畅无问题(若是后续测试有问题还会更新)。 布局
首先使用谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout布局来实现一个基本的带折叠效果的布局,能够自定义背景图片的大小和布局方式,想实现parallax只要添加“parallax” 属性就能够轻松实现。post
而后就是如何添加另外多出来的这一层背景布局并给它不同的移动速度,让咱们看起来是有三层(背景+背景层内容+下方具体内容)layout带有三个不一样的移动速度,形成多层视差效果。测试
背景层添加内容很容易,只要新建一个新的LinearLayout,构建好内容的布局,在AppbarLayout中include进来就能够了。由于这一部分的源码本质其实是extend了一个FrameLayout,因此咱们能够将多层内容重叠摆放在头部位置。而后在MainActivity中获取内容的id,这一步算完成了。ui
下面是最重要的一步,如何让这一部分的layout有不同的上划速度,而且在惯性滑动过程当中,也能够随时监测底部位置并更改本身自己的位置。this
4.1.全部的触摸事件都绕不开三个大佬,dispatchTouchEvent()
,InterceptedTouchEvent()
和 onTouchEvent()
。因此果断重写CoordinatorLayout, 重写 InterceptedTouchEvent()
和 onTouchEvent()
,dispatchTouchEvent()
暂时不用管他。咱们在InterceptedTouchEvent()
中截获手指在屏幕上的动做,而后根据咱们的要求来分发事件。若是检测到手指是向上划的,就return true
把事件传递给onTouchEvent()
去处理。spa
4.2 在新的CoordinatorLayout中,还要写一个open function来使Acticity能够将头部背景的图片传递过来,只有这样咱们才能正常在新建的layout中处理图片位置和获取相关信息。这一点很重要,不然咱们无法在这个文件里找到背景图片的代码位置(无法findViewById)。code
fun getContent (content : LinearLayout, header:View, realcontent : View){
this.content = content
this.header = header
this.realcontent = realcontent
content.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 = true
isDragging = false
initX = ev.x
initY = ev.y
}
MotionEvent.ACTION_MOVE -> {
isDragging = true
val draggedX = ev.x - initX
val draggedY = ev.y - initY
if (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 - initX
val draggedY = ev.y - initY
println("$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) - realContentInitPosition
println("real content $changedY")
val threshold = 450
if (getViewPositionY(real_content)<=threshold){
val temp = threshold-toolbar_statusbar_height
val ratio = 1-(getViewPositionY(real_content)-toolbar_statusbar_height)/temp
top_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)
}
}
复制代码
不知道本身使用的postDelayed方法来一直不停的检测位置变化的方法是否是正确,是否会形成对软件运行流畅度的影响。若是各位有建议请提给我谢谢!
传送门:Github -- Multi-Layer-Parallax-Background
谷歌官方的CoordinatorLayout + AppbarLayout + CollapsingLayout 布局有一个bug,至今据我测试尚未修复,就是若是在调整了头部背景的高度的时候,很容易在向下滑动的时候从头部图片滑动,若是手指离开屏幕布局进入惯性滑动fling阶段,在惯性滑动没有中止以前从新滑动屏幕(非头部区域),布局会产生抖动并且没法控制。这是由于当开始从头部滑动时,该动做被头部layout处理,产生的fling也是由它产生的,咱们没有办法从外部中止这个fling,若是在这个时候触摸屏幕并且触摸点在非头部背景区域,这个动做就会和以前的惯性滑动动做冲突。
查过解决方案,也尝试过手动解决这个问题可是并无奏效。用反射的方法获取父类的父类的父类中的overScroller和flingRunnable对象,在自定义的layout中用set
方法手动注入咱们本身的scroller,这样咱们就能够控制惯性滑动的动做并随时使用abortAnimation()
中止fling。若是有大神有更好的办法请赐教!