仿oppo手机浏览器首页的滑动布局
周末的午后,我悠然的躺着床上,甚是无聊,拿起我的oppo手机,打开浏览器准备输入我熟悉的xxx .com,
忽然我发现了一个比较好玩的滑动效果,滑动了几次决定我也要来写一个类似的,就这样,这个事情终结了我访问xxx的行为
话不多说直接上图
原效果图
我大概实现的样子
gif只能传小于5M的,所以会有掉帧卡顿的表现,其实一点也不卡顿!
这个其实主要是一些事件分发的一些处理
手指在布局上下滑动时,TabLayout和底下的ViewPager一个从上至下滑动,一个从上到下滑动,两个View的终点是Toolbar的bottom(同时到达),在TabLayout和ViewPager滑动时中间的View也稍许上下移动,这边我给到的移动范围是TabLayout的高度,外加一个透明度的变化
————————————————————————————————————————————————
这个效果一开始坑也比较多,本来是重写的ScrollView,ScrollView中包含了ViewPager,
ViewPager的item是Fragment,Fragment中是SwipeRefreshLayout+RecycleVIew。
这样嵌套的坑遇到了两个
第一:ViewPager的没有显示出来,应该是嵌套后高度出现了问题,我重写了它的onMeasure方法,遍历它的子view,重新测量它子view的高度,找到高度最高的一个算出heightMeasureSpec,再super回去,ViewPager是显示出来了,又出现了第二个问题;
第二:合并后的滑动问题,视图合并后,RecycleView一直流畅上滑,滑到底部后接着上滑ScrollView把事件拿去了,ScrollView在滑动了,因为我是通过setY的方式改变了布局的位置,所有ScrollView的高度是没变的,继续上滑会滑出空白,后来也想过处理RecycleView的onTouch,但感觉也会比较麻烦,就放弃了这种方案。
后面就直接继承了RelativeLayout,这样看起来就一个地方要修改了,因为是match_parent,ViewPager的高度是除了Toolbar高度+Content高度,但我setY的改变ViewPager位置的时候,尴尬的地方就出现了,ViewPager滑上来的时候底下是空白的,没有RecycleView的填充,因为RecycleView的滚动范围高度也是ViewPager的高度;
这个ViewPager最多可以移动到Toobar的底下,他的高度应该为原本高度+Content的高度!!这样问题就解决了,下面贴一贴主要代码
先贴下布局的代码
<?xml version="1.0" encoding="utf-8"?>
<com.ljp.newsdemo.widget.NewsScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/news_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/rl_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@color/colorAccent"
app:title="我是标题"/>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
android:padding="10dp"
android:text="@string/content"
android:textColor="@color/colorPrimary"
android:textSize="14sp"/>
</RelativeLayout>
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@color/colorPrimary"/>
<com.ljp.newsdemo.widget.NewsViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/rl_content"
android:background="#ffffff"/>
</com.ljp.newsdemo.widget.NewsScrollView>
NewsScrollView为RelativeLayout,拦截了整个view的触摸事件做了处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//点击事件不拦截
int evY = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downEvY = evY;
return super.onInterceptTouchEvent(ev);
case MotionEvent.ACTION_UP:
Log.d(TAG, "onInterceptTouchEvent: evY - downEvY = " + (evY - downEvY));
//小于等于5 就算是点击,正常的点击 down-up=0
if (Math.abs(evY - downEvY) <= 5) {
return super.onInterceptTouchEvent(ev);
}
break;
default:
break;
}
//其他情况看 是否已经合并了 如果已经合并了 不拦截;如果没合并,拦截掉
if (isMerge) {
return super.onInterceptTouchEvent(ev);
} else {
return true;
}
}
下面是滑动的处理,一开始写了很多代码,慢慢简化到了这一点。这块是主要逻辑代码了
逻辑是这样的,要有一个滑动距离范围,这个范围就是整个布局View除了Toolbar和底下的ViewPager的高度;
move的时候来计算移动的距离,通过回调把move的距离传出去,如果已经是合并状态的话,这边就不处理了,就交由下面的子view处理,可能就是刷新和点击事件了;
up和cancel的时候判断滑动的距离是否超过了范围的一半,如果超过了一半就做合并处理,反之就还原处理,up和cancel的事件都是回调出去做的动画效果处理;
@Override
public boolean onTouchEvent(MotionEvent ev) {
//手指距离view顶部的位置
int evY = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downEvY = evY;
return super.onTouchEvent(ev);
case MotionEvent.ACTION_MOVE:
if (isMerge) {
return super.onTouchEvent(ev);
}
//最大可移动范围 为scrollview中去除recyclerView的距离
//计算滚动距离
mMoveY = downEvY - evY;
if (mMoveY <= 0) {
mMoveY = 0;
} else if (mMoveY >= mMaxScrollViewRange) {
mMoveY = mMaxScrollViewRange;
}
if (mOnScrollListener != null) {
mOnScrollListener.onScrollChange(-mMoveY, mMaxScrollViewRange, false);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mOnScrollListener != null) {
//如果滑动的距离小于于最大距离的一半 让两个view做动画还原
if (Math.abs(mMoveY) < mMaxScrollViewRange / 2) {
//还原
isMerge = false;
mOnScrollListener.onScrollChange(0, mMaxScrollViewRange, true);
} else {
//合并
isMerge = true;
mOnScrollListener.onScrollChange(-mMaxScrollViewRange, mMaxScrollViewRange, true);
}
}
break;
default:
break;
}
if (isMerge) {
return super.onTouchEvent(ev);
} else {
return true;
}
}
重写的viewPager中的主要代码
private int maxHeight = 0;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (maxHeight != 0) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
requestLayout();
}
//外面调用
mViewPager.setMaxHeight(mTvContent.getMeasuredHeight() + mViewPager.getMeasuredHeight());
MainActivity中的主要代码
private final int animDuration = 500;
mViewPager.post(new Runnable() {
@Override
public void run() {
//设置viewpager的高度
mViewPager.setMaxHeight(mTvContent.getMeasuredHeight() + mViewPager.getMeasuredHeight());
//设置滑动的最大距离
mNewScrollview.setMaxScrollViewRange(mTvContent.getMeasuredHeight());
//初始化时把tablayout移除屏幕外
mTabLayout.setTranslationY(-mTabLayout.getMeasuredHeight());
initData();
initListener();
}
});
mNewScrollview.setOnScrollListener(new NewsScrollView.OnScrollListener() {
@Override
public void onScrollChange(final float y, int maxRange, boolean isAnim) {
//计算tabLayout要移动的距离
float height = mTabLayout.getHeight();
//计算倍率
float power = height / maxRange;
//计算tablayout的滚动距离
float tabMoveY = -height + power * Math.abs(y);
//透明度的计算0—1
float alphaPower = 1f / maxRange;
float alpha = 1 - (alphaPower * Math.abs(y));
//mRlContent 也一起移动 最多移动tablayout的高度,计算如下
float contentMoveY = -(height + tabMoveY);
if (isAnim) {
AnimListener animListener = new AnimListener();
ViewCompat.animate(mViewPager).translationY(y).setDuration(animDuration).setListener(animListener).start();
ViewCompat.animate(mTabLayout).translationY(tabMoveY).setDuration(animDuration).start();
ViewCompat.animate(mRlContent).translationY(contentMoveY).setDuration(animDuration).start();
if (y == 0) {
//还原了
ViewCompat.animate(mRlContent).alpha(1).setDuration(animDuration).start();
} else {
//合并了
ViewCompat.animate(mRlContent).alpha(0).setDuration(animDuration).start();
}
} else {
mViewPager.setTranslationY(y);
mTabLayout.setTranslationY(tabMoveY);
mRlContent.setTranslationY(contentMoveY);
mRlContent.setAlpha(alpha);
}
}
});
//恢复layout的动画
private void restoreLayoutAnim() {
ViewCompat.animate(mViewPager).translationY(0).setDuration(animDuration).start();
ViewCompat.animate(mTabLayout).translationY(-mTabLayout.getHeight()).setDuration(animDuration).start();
ViewCompat.animate(mRlContent).alpha(1).setDuration(animDuration).start();
ViewCompat.animate(mRlContent).translationY(0).setDuration(animDuration).start();
//要让recyclerview第一个条目滚动置顶
try {
NewsFragment fragment = ((ClassifyVpAdapter) mViewPager.getAdapter()).getItem(mViewPager.getCurrentItem());
fragment.setRvScrollTop();
} catch (Exception e) {
e.printStackTrace();
Log.d(TAG, "restoreLayoutAnim: e=" + e.getMessage());
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mNewScrollview.getIsMerge()) {
mNewScrollview.scrollTo(0, 0);
//恢复原来的样子
restoreLayoutAnim();
mNewScrollview.setMergeState(false);
return true;
}
}
return super.onKeyDown(keyCode, event);
}
写完了发现比较简单,但是感觉优化的地方还是有的,比如ViewPager的高度问题,在Activiy的init时候获取view宽高的问题,感觉用post的方式不太合理,大佬有建议希望指点一二!