Android沉浸式状态栏、颜色渐变、副标题动画和TabLayout悬浮条

嘛,几天没学习,我浑身难受。

今天把前段时间公司这边的几个需求放在一起总结了一下,有这样几个:

  • 沉浸式状态栏(5.0以上),并要求上部bar随着RecyclerView滑动变色
  • RecyclerView悬浮条,与普通悬浮条不同的是悬浮条是一个可滑动的TabLayout
  • 副标题随着RecyclerView滑动动画上移或者动画下移

把这几个东西放一起来重新梳理一下吧,先上效果图:

左边是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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

代码很清晰,使用属性动画模拟副标题的滚动,注意这里需要处理不需要执行动画的情况,不然疯狂重复执行容易导致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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

代码中注释很详细了,这里只需要注意一点,在开启了沉浸式状态栏后,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();
        }
    });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这里设置了一个滑动系数,记录滑动的百分比,并在之后设置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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里也没什么好说的,滑动系数为0 或者大余1,设置为不透明,0到1中间,使用ArgbEvaluator来计算颜色值(其实只有Alpha在变化,从0x600873d2到0xff0873d2),这里之所以不用SDK中的ArgbEvaluator是由于低版本貌似有点问题,具体原因不知道,我这里直接copy了API27的代码。关于Evaluator的知识,见Android 动画学习之属性动画 (Property Animator)-2、属性动画执行流程

TabLayout悬浮

悬浮其实是一个老生常谈的东西了,我们都知道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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

这里就是一个简单的在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);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

这里使用的重写的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 != ) {
            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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

这里主要在dispatchTouchEvent方法中,直接将触摸事件传递给另一个TabLayout。这样Tab选中,滑动等等操作两者都是一致的。之后将两个Tablayout绑定在一起,完成功能。

本篇文章就是这样了,其实要讲的东西没有很多,主要东西都在代码里写出了,所以有兴趣的还是看代码吧。

代码地址,欢迎fork,评论~
欢迎访问我的个人博客来与我交流~

enjoy~

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </div>
阅读更多
换一批

没有更多推荐了,返回首页