实践

手写RecyclerView回收池及沉浸式布局设计

2020-02-25  本文已影响0人  大苏打6815
RecyclerView只是一个控件,和上亿级存储是没半毛钱关系的。

如果要手写Recycleview,那么就不会去继承他,继承的话就叫做扩展了或者叫复用了。
因此我们要手写的话,主要要注意几个方面的问题。

1、触摸滑动事件的处理。

RecycleView是一个具备滑动功能的控件,所以他要进行触摸滑动事件的监听

2、适配器与UI的交互

通过适配器要讲数据与UI进行交互。

3、回收池与适配器的交互

RecycleView要协调回收池中view对象与适配器中对象之间的工作。

回收池

回收池的作用是回收RecyclewView中那些已经不需要展示的item,但是回收池的作用,并不是把不显示的item的View进行回收,而是将item的View进行复用

适配器

Adapter接口是辅助RecyclerView实现数据展示,适配器模式将用户展示和交互相分离。

直接上代码了,写的只是个思想,在布局文件写自己自定义的Recycleview,并且写嵌套滑动控件

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="观察者"
        android:gravity="center"
        app:layout_behavior=".view.MyBehavior"/>
    <com.dongnao.dn_vip_ui_17_2.view.MyRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"/>

</android.support.design.widget.CoordinatorLayout>

在主Activity初始化recycleview并设置数据源(具体的复用池处理逻辑在后面)

public class MainActivity extends AppCompatActivity {
    MyRecyclerView recyclerView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        recyclerView = findViewById(R.id.recyclerView);
        recyclerView.setAdapter(new MyRecyclerView.Adapter() {
            @Override
            public View onCreateViewHolder(int position, View convertView, ViewGroup parent) {
                convertView = getLayoutInflater().inflate(R.layout.layout_item,parent,false);
                TextView textView = convertView.findViewById(R.id.textView);
                textView.setText("大苏打"+position);
                return convertView;
            }

            @Override
            public View onBinderViewHolder(int position, View convertView, ViewGroup parent) {
                TextView textView = convertView.findViewById(R.id.textView);
                textView.setText("大苏打"+position);
                return convertView;
            }

            @Override
            public int getItemViewType(int row) {
                return 0;
            }

            @Override
            public int getViewTypeCount() {
                return 1;
            }

            @Override
            public int getCount() {
                return 40;
            }

            @Override
            public int getHeight(int index) {
                return 100;
            }
        });
    }
}

Recycle类是复用池

public class Recycler {
    //回收池的容器  存储所有的的回收了的View
    private Stack<View>[] views;

    public Recycler(int viewTypeCount){
        //根据类型的种类的数量来创建数组
        views = new Stack[viewTypeCount];
        //初始化数组总的每一个Stack
        for(int x=0;x<viewTypeCount;x++){
            views[x] = new Stack<>();
        }
    }

    /**
     * 将View放入到对应类型的Stack中
     * @param itemView
     * @param viewType
     */
    public void put(View itemView,int viewType){
        views[viewType].push(itemView);
    }

    public View get(int viewType){
        try{
            return views[viewType].pop();
        }catch (Exception e){
            return null;
        }
    }

在自定义的recycleview里面写相应的逻辑,注释已经写在代码里面了

public class MyRecyclerView extends ViewGroup implements NestedScrollingChild2 {
    //当前RecyclerView的适配器
    private Adapter adapter;
    //当前显示的vIEW的集合
    private List<View> viewList;
    //当前滑动的Y值
    private int currentY;
    //总行数
    private int rowCount;
    //显示的的第一行在数据源中的position
    private int firstRow;
    //y偏移量
    private int scrollY;
    //初始化  是否是第一屏
    private boolean needRelayout;
    //当前RecyclerView的宽度
    private int width;
    //当前RecyclerView的高度
    private int height;
    //所有ItemView的高度数组
    private int[] heights;
    //View对象回收池
    private Recycler recycler;
    //最小滑动距离
    private int touchSlop;
    //获取到嵌套滑动子控件的helper类
    private NestedScrollingChildHelper nestedScrollingChildHelper;



    public MyRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
    }

    /**
     * 初始化RecyclerView的方法
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        //获取到最小的滑动距离
        touchSlop = viewConfiguration.getScaledTouchSlop();
        //初始化vieaList
        viewList = new ArrayList<>();
        //初始化是否需要重新布局
        needRelayout = true;
        //初始化帮助类
        nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        nestedScrollingChildHelper.setNestedScrollingEnabled(true);
    }

    /**
     * 重写onInterceptTouchEvent方法  来决定滑动还是不滑动
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                currentY = (int) ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                //取出手指当前移动到的Y轴值 跟之前手指触摸的位置想比减
                float y2 = Math.abs(currentY - ev.getRawY());
                //如果滑动的距离  小于最小滑动的距离 就不滑动  如果大于 就滑动
                if(y2 > touchSlop){
                    intercept = true;
                }
                break;
        }
        return intercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                //先获取到当前Y轴的值
                float y2 = event.getRawY();
                //手指触摸点减去滑动到的这个Y轴的值
                float diffY = currentY - y2;
                //不加会影响反应速度
                currentY = (int) y2;
                //滑动的方法
                scrollBy(0, (int) diffY);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL,ViewCompat.TYPE_TOUCH);
                stopNestedScroll();
                break;

        }
        return super.onTouchEvent(event);

    }


    @Override
    public void scrollBy(int x, int y) {
        scrollY += y;
        //纠正scrollY
        scrollY = scrollBounds(scrollY);
        if(scrollY>0){  //向上滑
            //上滑要做两件事情
            // 1 将上面的itemView移除掉
            while (scrollY > heights[firstRow]){
                //删除第一行 就是删除当前布局中的第0个Item
                removeView(viewList.remove(0));
                //改变scrollY scrollY要减去这一行的高度
                scrollY -=heights[firstRow];
                //当前显示的行标
                firstRow++;
            }
            //给下面添加一个新的itemView进来
            //判断当前所显示的view的高度是不是小于RecyclerView的高度  如果小于  就添加新的item
            while(getFillHeighet() < height){
                //首先  当前第一行的行标 加上以及显示的itemView的长度 其实就是要添加进去的那一行的Item
                int addlast = firstRow +viewList.size();
                //获取到itemView
                View view = obtainView(addlast,width,heights[addlast]);
                //将新的itemView添加进viewList
                viewList.add(view);
            }

        }else if(scrollY<0){    //向下滑
            //下滑过程中 要做两件事情
            //创建一个新的itemView放置在最上面
            while (scrollY <0){
                //当前现实的itemView的第一行的行标减去1
                int firstAddRow = firstRow - 1;
                //获取到显示在第一行的itemView的上一个itemView
                View view = obtainView(firstAddRow,width,heights[firstAddRow]);
                //添加到当前可见的item的最上面
                viewList.add(0,view);
                //更新当前现实的第一行的航标
                firstRow--;
                //改变scrollY scrollY要将当前添加进去的行的行高加起来
                scrollY+=heights[firstAddRow];
            }
            //把最下面移出了屏幕的item移除掉 判断当前显示的View的总高度是不是大于RecyclerView的高于 如果大于 将最上面的itemView移除掉
            while (sumArray(heights,firstRow,viewList.size())-scrollY-heights[firstRow+viewList.size()-1]>=height){
                //移除当前显示的再最下面的item
                removeView(viewList.remove(viewList.size()-1));
            }
        }else{

        }
        //重新摆放位置
        rePositionView();
    }


    @Override
    public void removeView(View view) {
        super.removeView(view);
        int key = (int) view.getTag(R.id.tag_type_view);
        //将view添加进回收池中
        recycler.put(view,key);
    }

    /**
     * 纠正
     * @param scrollY
     * @return
     */
    private int scrollBounds(int scrollY) {
        if(scrollY>0){
            //判断上滑的极限值  防止滚动的距离 大于当前所有内容的高度
            scrollY = Math.min(scrollY,sumArray(heights,firstRow,heights.length-firstRow)-height);
        }else{
            //判断下滑的极限值  防止滚动的距离 小于第0个item的高度
            scrollY = Math.max(scrollY,-sumArray(heights,0,firstRow));
        }
        return scrollY;
    }

    /**
     * 当我们上啦或者下滑的时候  重新摆放View的位置
     */
    private void rePositionView() {
        int top=0,right,bottom,left =0,i;
        top =- scrollY;
        //将当前第一行的行标赋值给I
        i= firstRow;
        for (View view : viewList) {
            //下一一个或者上移一个item
            bottom = top + heights[i++];
            view.layout(0,top,width,bottom);
            top = bottom;
        }
    }

    /**
     * 获取到显示在控件中的view的总高度
     * @return
     */
    private int getFillHeighet() {
        return sumArray(heights,firstRow,viewList.size())-scrollY;
    }

    /**
     * 获取到数组中数据的高度
     * @param heights   数组
     * @param index 从哪一个item拿起
     * @param count    一共要拿多少个item的高度
     * @return
     */
    private int sumArray(int[] heights, int index, int count) {
        int sum = 0;
        count+=index;
        for(int x=index;x<count;x++){
            sum +=heights[x];
        }
        return sum;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取到RecyclerView再当前窗体中的宽高
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //当前内容的高度
        int h=0;
        //判断适配器是否为空
        if(adapter!=null){
            //获取到当前数据的中条数
            rowCount = adapter.getCount();
            //根据适配器的数据的总长度来创建数组
            heights = new int[rowCount];
            //循环获取到所有的view的高度
            for(int x=0;x<heights.length;x++){
                heights[x] = adapter.getHeight(x);
            }
        }
        //获取到所有数据的高度
        int tempHeight = sumArray(heights,0,heights.length);
        //判断所有的item的高度 和RecyclerView的高度谁低
        h = Math.min(tempHeight,heightSize);
        setMeasuredDimension(widthSize,h);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //先判断是否需要布局  布局发生改变的时候重新布局
        if(needRelayout || changed){
            needRelayout = false;
            //清除掉所有的view
            removeAllViews();
            //清楚掉当前屏幕中显示的itemView
            viewList.clear();
            //如果适配器不为空  就去摆放itemView
            if(adapter !=null){
                //获取到RecyclerView的宽高
                width = r - l;
                height = b - t;
                //定义布局ietmView的四个变量
                int top=0,right,bottom,left = 0;
                for(int x=0;x<rowCount;x++){
                    //获取到绘制宽度的最右边
                    right = width;
                    bottom = top+heights[x];
                    //生成View
                    View view = makeAndStep(x,left,top,right,bottom);
                    //添加到当前的itenView的集合中
                    viewList.add(view);
                    //因为循环 摆放 所以  下一个控件的top就是上一个控件的botton  而且要累加
                    top = bottom;
                }
            }

        }
    }

    /**
     * 创建View的方法
     * @param x
     * @param left
     * @param top
     * @param right
     * @param bottom
     * @return
     */
    private View makeAndStep(int x, int left, int top, int right, int bottom) {
        //生成View
        View view = obtainView(x,right-left,bottom-top);
        //布局itemView
        view.layout(left,top,right,bottom);
        return view;
    }

    /**
     * 生成itemView
     * @param row
     * @param width
     * @param height
     * @return
     */
    public View obtainView(int row,int width,int height){
        //首先获取到这一行数的item的布局类型
        int itemViewType = adapter.getItemViewType(row);
        //去栈中拿
        View view = recycler.get(itemViewType);
        //定义一个view
        View itemView = null;
        if(view == null){
            itemView = adapter.onCreateViewHolder(row,itemView,this);
            if(itemView ==null){
                throw new RuntimeException("onCreateViewHolder 必须要填充布局");
            }
        }else{
            itemView = adapter.onBinderViewHolder(row,view,this);
        }
        //给每个ItemView设置一个tag
        itemView.setTag(R.id.tag_type_view,itemViewType);
        //先测量每个itenView
        itemView.measure(MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));
        //没生成一个itenView  都添加进RecyclerView
        addView(itemView);
        return itemView;
    }





    public Adapter getAdapter() {
        return adapter;
    }

    public void setAdapter(Adapter adapter) {
        this.adapter = adapter;
        //初始化回收池
        if(adapter !=null){
            recycler = new Recycler(adapter.getViewTypeCount());
            scrollY = 0;
            firstRow = 0;
            needRelayout = true;
            //重新测量  重新摆放
            requestLayout();
        }
    }

    @Override
    public boolean startNestedScroll(int i, int i1) {
        return nestedScrollingChildHelper.startNestedScroll(i,i1);
    }

    @Override
    public void stopNestedScroll(int i) {

    }

    @Override
    public boolean hasNestedScrollingParent(int i) {
        return false;
    }

    @Override
    public boolean dispatchNestedScroll(int i, int i1, int i2, int i3, @Nullable int[] ints, int i4) {
        return false;
    }

    @Override
    public boolean dispatchNestedPreScroll(int i, int i1, @Nullable int[] ints, @Nullable int[] ints1, int i2) {
        return nestedScrollingChildHelper.dispatchNestedPreScroll(i,i1,ints,ints1,i2);
    }

    @Override
    public void stopNestedScroll() {
        super.stopNestedScroll();
        nestedScrollingChildHelper.stopNestedScroll();
    }

    public interface Adapter{
        //创建ViewHolder的方法
        View onCreateViewHolder(int position, View convertView, ViewGroup parent);
        //绑定ViewHolder的方法
        View onBinderViewHolder(int position,View convertView,ViewGroup parent);

        //获取到当前row item的控件类型
        int getItemViewType(int row);

        //获取当前控件类型的总数量
        int getViewTypeCount();

        //获取当前item的总数量
        int getCount();

        //获取index item的高度
        int getHeight(int index);
    }

因为嵌套滑动,所以要用到coordinatorlayout,这个再前面的一篇有讲到

public class MyBehavior extends CoordinatorLayout.Behavior {

    public MyBehavior(Context context, AttributeSet attributeSet){
        super(context,attributeSet);
    }

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof MyRecyclerView;
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                       @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        Log.e("我到啦我到啦2","我到啦我到啦");
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
    }
手写Recyclerview支持嵌套滑动和沉浸式布局设计

沉浸式:有关状态栏和虚拟按键(4.4以下是没有这个概念的)
其实沉浸式状态栏没有什么可讲的,但是自己在写学习的过程中,遇到了一些问题,也发现可以通过很多种方式可以解决,难就难在怎样去适配安卓版本的问题。

我们可以通过两种方式,设置沉浸式状态栏

一:通过设置Theme主题方式设置状态栏

4.4-5.0版本
状态栏透明设置 必须是4.4以上的版本
<item name="android:windowTranslucentStatus">true</item>
虚拟按键透明设置
<item name="android:windowTranslucentNavigation">true</item>

5.0以上版本
状态栏透明设置 必须是4.4以上的版本
<item name="android:windowTranslucentStatus">false</item>
虚拟按键透明设置
<item name="android:windowTranslucentNavigation">true</item>
5.0以上设置状态栏的颜色 但是必须是windowTranslucentStatus为false
<item name="android:statusBarColor">@android:color/transparent</item>


image.png

我是直接上的5.0以上版本,低版本逐渐淘汰了

二:通过代码设置
private void initStatus() {
        //版本大于等于4.4
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
            //获取到状态栏设置的两条属性
            int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
            int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
            //在4.4之后又有两种情况  第一种 4.4-5.0   第二种 5.0以上
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
                //第二种 5.0以上
                Window window   = getWindow();
                WindowManager.LayoutParams attributes = window.getAttributes();
                attributes.flags |= flagTranslucentNavigation;
                window.setAttributes(attributes);
                window.setStatusBarColor(0);
            }else{
                //第一种 4.4-5.0
                Window window   = getWindow();
                WindowManager.LayoutParams attributes = window.getAttributes();
                attributes.flags |= flagTranslucentStatus|flagTranslucentNavigation;
                window.setAttributes(attributes);
            }
        }
    }

如果有一张imageview在布局铺满的话,此时此刻顶部的statusbar和下面的虚拟按键都被铺满了


image.png

如果我们不让内容填充到状态栏呢?就是不遮挡状态栏怎么办?如下图,有三种做法


image.png
1.简单的设置 xml根布局添加 android:fitsSystemWindows="true"
2代码中通过设置一个控件来代替状态栏。-->首先获取到在xml中加一个控件代替状态栏,然后获取到状态栏的高度赋值给代替者。
        //获取到view控件
        View statusBar = findViewById(R.id.statusBar);
        //获取到它的Params对象
        ViewGroup.LayoutParams layoutParams = statusBar.getLayoutParams();
        //设置它的高度
        layoutParams.height = getStatusHeight();
        //设置layoutParams
        statusBar.setLayoutParams(layoutParams);
        //设置背景颜色
        statusBar.setBackgroundColor(Color.RED);
3.在代码中设置padding值并且设置一个控件来代替状态栏。
View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
        //给根布局设置padding值
        rootView.setPadding(0,getStatusHeight(),0,getNavigationBarHeight());
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
            //第二种 5.0以上
            getWindow().setStatusBarColor(Color.RED);
        }else{
            //第一种 4.4-5.0
            //获取到根布局
            ViewGroup decorView = (ViewGroup) getWindow().getDecorView();
            View statusBar = new View(this);
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,getStatusHeight());
            statusBar.setBackgroundColor(Color.RED);
            statusBar.setLayoutParams(layoutParams);
            decorView.addView(statusBar);
        } 

我们在做沉浸式布局的时候,特别用到了侧滑菜单的时候,会遇到一些问题。解决办法如下
1.5.0菜单有阴影:解决办法给NavigationView 加入app:insetForeground="#00000000"
2.4.4 可以给最外层布局设置fitSystemWidows为true且设置clipToPadding为false

上一篇下一篇

猜你喜欢

热点阅读