嘛,几天没学习,我浑身难受。
今天把前段时间公司这边的几个需求放在一起总结了一下,有这样几个:
把这几个东西放一起来重新梳理一下吧,先上效果图:
左边是Android4.1上的效果,右边是5.0以上效果。下面就一点点来分析这种效果是如何实现的。
从上面的效果图可以看出,副标题在RecyclerView滑动时会有位移效果,我这里的思路是使用一个FrameLayout重叠放置两个TextView,在Activity初始化后,监听RecyclerView的滑动,并在特定时机执行相应动画。
那么首先布局文件如下:
<RelativeLayout
android:id="@+id/rl_title_bar"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#0873d2">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="16sp"
android:textColor="#ffffff"
android:text="这是重要的标题"/>
<FrameLayout
android:layout_alignParentBottom="true"
android:layout_marginBottom="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true">
<TextView
android:id="@+id/tv_sub_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="12sp"
android:textColor="#e3e3e3"
android:text="这是不重要的副标题"/>
<TextView
android:id="@+id/tv_sub_title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="12sp"
android:textColor="#e3e3e3"
android:text="这是更不重要的副标题"/>
FrameLayout>
RelativeLayout>
在Activity的onCreate
方法中,添加RecyclerView监听,并且执行动画:
private final int animationHeight = 60;
/**
* 给RecyclerView增加滑动监听
*/
private void initListener() {
rvContainer.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisible = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if(firstVisible > 0) {
} else {
View view = linearLayoutManager.findViewByPosition(0);//获取第一item
int verticalOffset = recyclerView.computeVerticalScrollOffset();//获取第一个item纵向滑动距离
float viewHeight = view.getMeasuredHeight() - rlTitleBar.getMeasuredHeight();//获取滑动距离
if(verticalOffset > animationHeight) {
startAnimator(1);
} else {
startAnimator(0);
}
}
}
});
}
/**
* 开始副标题动画
*/
private void startAnimator(int subTitleStatus) {
//处理不需要动画的状况
if(this.subTitleStatus == subTitleStatus) {
return;
}
this.subTitleStatus = subTitleStatus;
if(subTitleStatus == 0) {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvSubTitle, "y", (float) -tvSubTitle.getMeasuredHeight(),0f);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(tvSubTitle2, "y",0f , (float) tvSubTitle2.getMeasuredHeight());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(500);
animatorSet.play(objectAnimator).with(objectAnimator2);
animatorSet.start();
} else {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvSubTitle, "y", 0f, (float) -tvSubTitle.getMeasuredHeight());
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(tvSubTitle2, "y", (float) tvSubTitle2.getMeasuredHeight(),0f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(500);
animatorSet.play(objectAnimator).with(objectAnimator2);
animatorSet.start();
}
}
代码很清晰,使用属性动画模拟副标题的滚动,注意这里需要处理不需要执行动画的情况,不然疯狂重复执行容易导致crash。属性动画的使用可以看我之前的博文Android动画
Android5.0的沉浸式状态栏会更美观一下,我这里没有理会4.4的情况,其实就是因为懒。
咳咳,我们继续。其实要实现沉浸式状态栏,只需要这样就可以了:
private void initView() {
if(Build.VERSION.SDK_INT >= 21) {
//Android5.0以上,设置状态栏透明并且全屏
View decorView = getWindow().getDecorView();
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
initTitleBar();
}
/**
* 设置TitleBar状态
*/
private void initTitleBar() {
if(Build.VERSION.SDK_INT >= 21) {
//Android5.0以上,由于状态栏透明了,所以给TitleBar设置一个topPadding,高度为statusBar高度
ViewGroup.LayoutParams params = rlTitleBar.getLayoutParams();
params.height = params.height + getStatusBarHeight(this);
rlTitleBar.setPadding(0,getStatusBarHeight(this),0,0);
}
}
/**
* 根据反射获取状态栏高度
*/
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen",
"android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
代码中注释很详细了,这里只需要注意一点,在开启了沉浸式状态栏后,Activity整个View是从屏幕最顶端开始的,因此需要为Titlebar设置一个状态栏高度的padding,否则,感兴趣的朋友可以自己测试看看。
在RecyclerView滑动过程添加监听,不多废话,代码如下:
private float scrollRatio = 0;//滑动系数,初始设置为0
/**
* 给RecyclerView增加滑动监听
*/
private void initListener() {
rvContainer.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisible = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if(firstVisible > 0) {
//滑动到第二个item及之后item,将滑动系数设置为1
scrollRatio = 1;
} else {
View view = linearLayoutManager.findViewByPosition(0);//获取第一item
int verticalOffset = recyclerView.computeVerticalScrollOffset();//获取第一个item纵向滑动距离
float viewHeight = view.getMeasuredHeight() - rlTitleBar.getMeasuredHeight();//获取滑动距离
scrollRatio = verticalOffset / viewHeight;
}
setUpRatio();
}
});
}
这里设置了一个滑动系数,记录滑动的百分比,并在之后设置titleBar的透明度:
private int startColor = 0x600873d2;
private int endColor = 0xff0873d2;
/**
* 为rlTitleBar设置滑动系数
*/
private void setUpRatio() {
if(scrollRatio < 0 || scrollRatio >= 1) {
rlTitleBar.setBackgroundColor(endColor);
} else {
int color = (int) ArgbHelper.evaluate(scrollRatio,startColor,endColor);
Log.e("scrollRatio",scrollRatio+" color"+color);
rlTitleBar.setBackgroundColor(color);
}
}
这里也没什么好说的,滑动系数为0 或者大余1,设置为不透明,0到1中间,使用ArgbEvaluator来计算颜色值(其实只有Alpha在变化,从0x600873d2到0xff0873d2),这里之所以不用SDK中的ArgbEvaluator是由于低版本貌似有点问题,具体原因不知道,我这里直接copy了API27的代码。关于Evaluator的知识,见Android 动画学习之属性动画 (Property Animator)-2、属性动画执行流程
悬浮其实是一个老生常谈的东西了,我们都知道RecyclerView做悬浮条一般会在Activity中放置一个与RecyclerViewItem中同样的View,然后在滑动监听中显示或者隐藏这个View。这里一个难点在于怎么将RecyclerView中的Tablayout与Activity中TabLayout的滑动点击等等,我这里选择在TabLayout的dispatchTouchEvent传递触摸事件。
先来看看悬浮条的实现:
/**
* 给RecyclerView增加滑动监听
*/
private void initListener() {
rvContainer.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisible = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if(firstVisible > 0) {
//滑动到第二个item及之后item,将滑动系数设置为1
scrollRatio = 1;
} else {
View view = linearLayoutManager.findViewByPosition(0);//获取第一item
int verticalOffset = recyclerView.computeVerticalScrollOffset();//获取第一个item纵向滑动距离
float viewHeight = view.getMeasuredHeight() - rlTitleBar.getMeasuredHeight();//获取滑动距离
scrollRatio = verticalOffset / viewHeight;
}
updateSuspension();
}
});
}
/**
* 更新悬浮条
*/
private void updateSuspension() {
if(scrollRatio >= 1) {
//显示悬浮条
tlSuspension.setVisibility(View.VISIBLE);
} else {
//隐藏悬浮条
tlSuspension.setVisibility(View.INVISIBLE);
}
}
这里就是一个简单的在TitleBar的alpha值为1时显示悬浮条,其余时间隐藏。主要代码需要来看Adapter中的ViewHolder:
public static class TabViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tl_item)
BindTabLayout tlItem;
@BindView(R.id.tv_notice)
TextView tvNotice;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
public TabViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
tvNotice.setText("选中Tab:"+tab.getText());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
};
tlItem.addOnTabSelectedListener(onTabSelectedListener);
}
public void bind(TabTouchListener listener) {
tlItem.setListener(listener);
listener.setListener(tlItem);
tlItem.setTabMode(TabLayout.MODE_SCROLLABLE);
for(int i = 0;i < 10;i++) {
TabLayout.Tab tab = tlItem.newTab().setText("Tab"+i);
tlItem.addTab(tab);
}
}
}
这里使用的重写的tabLayout,只是用了两个方法:
public class BindTabLayout extends TabLayout implements TabTouchListener{
private TabTouchListener bindLisnter;
public BindTabLayout(Context context) {
super(context);
}
public BindTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BindTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(bindLisnter != null) {
bindLisnter.dispatchBindEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
@Override
public void dispatchBindEvent(MotionEvent event) {
super.dispatchTouchEvent(event);
}
@Override
public void setListener(TabTouchListener listener) {
this.bindLisnter = listener;
}
}
public interface TabTouchListener {
void dispatchBindEvent(MotionEvent event);
void setListener(TabTouchListener listener);
}
这里主要在dispatchTouchEvent方法中,直接将触摸事件传递给另一个TabLayout。这样Tab选中,滑动等等操作两者都是一致的。之后将两个Tablayout绑定在一起,完成功能。
本篇文章就是这样了,其实要讲的东西没有很多,主要东西都在代码里写出了,所以有兴趣的还是看代码吧。
代码地址,欢迎fork,评论~
欢迎访问我的个人博客来与我交流~
enjoy~