动效设计
1、两级吸顶
CoordinatorLayout+AppBarLayout可以轻松的实现一级吸顶的功能,两级吸顶并不支持。要实现两级吸顶,可以有两种思考:一,在此基础之上再完成一次吸顶;二,直接重写两次吸顶的逻辑,两次吸顶算法跟一次吸顶算法思路上应该是一致的。首页改版使用了第一种思路,因为考虑到顶部搜索框特殊的过渡动画效果,这块本身需要自定义,即便使用控件自带的效果,这块也要重写,所以首先完成了顶部搜索框的自定义吸顶效果,然后借力CoordinatorLayout控件实现了feed流头部的吸顶。这样,整个两级吸顶效果就完成了,顶部搜索栏吸顶通过自定义充分满足了动效,后续扩展和修改会更自如;而feed流头部使用父级控件自带的吸顶效果,大大减轻了再次吸顶的逻辑重写。
对于自定义顶部搜索栏吸顶的功能,可以通过AppBarLayout的OnOffsetChangedListener接口监听完成。onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset)方法返回了appBarLayout的垂直偏移量,通过这个变量基本能完成所有头部的动效。
// 背景图滑动处理if (Math.abs(verticalOffset) < mHeaderHeight) { mIvBackground.scrollTo(0, (int) (-verticalOffset / VELOCITY_OF_REFRESH_BACKGROUND)); // 搜索栏动效处理 int width = mCityWidth - mSearcherBarLeftMargin; int height = mHeaderHeight - statusBarHeight - mSearcherBarShadowHeight - mSearcherBarStickyHeight - mSearcherBarStickyTopMargin;; float rate = ((float) width) / height; int rateScrollY = (int) (rate * -verticalOffset); int scrollWidth = rateScrollY > width ? width : rateScrollY; float widthRate = scrollWidth * 1.0f / width; if (rateScrollY < width) { // 搜索框大小 RelativeLayout.LayoutParams searchParams = (RelativeLayout.LayoutParams) mSearcherBar.getLayoutParams(); searchParams.leftMargin = mSearcherBarLeftMargin + scrollWidth; searchParams.height = mSearcherBarHeight - (int)(diffHeight * widthRate); mSearcherBar.setLayoutParams(searchParams); ... }}
2、头图动效设计图
首页开发时头图下拉的参数设计
文案动画区,放置了下拉刷新的动画和文案,下拉到这个区域即展示刷新动画和相应的文案。
刷新触发区,下拉到这个区域松手后会触发刷新事件,下拉到文案动画区则不会触发刷新。
二楼触发区,下拉到这个区域松手后则触发负二楼的动效,以及负二楼落地;如果负二楼开关没有打开,延续刷新的功能。
此外,下拉刷新的偏移和头图的偏移不是1:1,是500:270的效果,这样可以形成视差,创造出空间层次感。补充,负二楼的过渡效果偏移和下拉偏移是1:1的,体现了跟随效果。
头图下拉偏移,通过SmartRefreshLayout的OnMultiPurposeListener接口监听操作。它提供了onHeaderMoving(RefreshHeader header, boolean isDragging, float percent, int offset, int headerHeight, int maxDragHeight)方法,输出了RefreshHeader下拉的偏移量,通过视差系数,从而实现了头图的动效。
// 下拉刷新时,背景图的移动速率private final static float VELOCITY_OF_REFRESH_BACKGROUND = 500.0f / 270.0f;FrameLayout.LayoutParams lpBg = (FrameLayout.LayoutParams) mIvBackground.getLayoutParams();lpBg.topMargin = mBackgroundTopMargin + (int) (offset / VELOCITY_OF_REFRESH_BACKGROUND);mIvBackground.setLayoutParams(lpBg); 3、负二楼动效设计图
研发实现后的负二楼主页效果图
UI划分区块后的布局元素设计参考图
动画页面渐隐落地页出现的一个过渡效果
整个页面使用全屏设计,对高度进行等比缩放处理以适应不同大小的屏幕设备。整体动画实现难度不大,问题在落地页如何从动画页下面渐显,我们知道逻辑上,负二楼页面和首页是在一个层级,实现落地页在动画页下面实现就是要实现落地页从首页下面出现。这个逻辑是行不通的,即便可以改造落地页,也不能实现Activity从下面出现的过渡效果。
最终解决方案是,负二楼动画页面用一个Activity实现,启动负二楼动画页之前先启动落地页,这两者几乎先后顺序几乎是保持同步的。然而,同时启动两个ui界面,势必会造成动画界面的播放卡顿,尤其是启动承载内容繁多的落地页,对上层动画页造成很大的影响。为解决这一问题,采用了“遮掩”的背景效果,即动画页背景和首页出现负二楼过渡效果图的背景是一致的,且启动动画页后有短暂停留,是为了把这一停留时间分配给落地页先行,从而最大可能的减少卡顿时长。
4、上下身和主体滑动冲突
主体用ScrollView而下身用ListView或RecyclerView会存在滑动冲突问题,这个我们都知道,当然也有解决方案。但是,新版首页的需求远不止这些,它是外层集成下拉刷新和上拉加载功能、内部整体滚动两级吸顶、下半身横向滚动切换页面为一体的复杂构造,我们不能单纯地通过传统的ScrollView+RecyclerView就可以解决。那么脱离传统父级向子级传递事件的思想,Google提供了一套“材料设计”的思想来处理这种复杂的空间关系,其中一个精髓就是将传统开发设计思想颠覆,将子视图的滑动事件向父级传递,通过父级的消费再告知其它子视图进行处理滑动,整个材料形成了一起非线性联动的效果,创造出空间层级感。父子级交互的两个通道是通过NestedScrollingParent和NestedScrollingChild两个接口协议通信的。我们通过CoordinatorLayout、AppBarLayout以及RecyclerView几乎完美地实现了这三者之间的冲突;几乎,但还是存在一些缺陷——AppBarLayout下滑,RecyclerView同时上划,会出现CoordinatorLayout消费子视图事件时的上下抖动问题。这个问题的主要原因是AppBarLayout滑动后的Fling效果未结束时,又再次触发RecyclerView的触摸事件。找到了这个原因,就能容易地解决最后的冲突问题了;在触摸事件里拦截Fling,让父级滚动在每次子视图被触摸时先停止掉就可以解决冲突问题。