美文网首页
android tv 列表滚动控件-MetroRecyclerV

android tv 列表滚动控件-MetroRecyclerV

作者: ihu11 | 来源:发表于2019-10-17 10:53 被阅读0次

    焦点展示结合了----FlowView-https://www.jianshu.com/p/dd559bcae221

    github库---demo源码-https://github.com/ihu11/MetroRecyclerView

    1.类GridView的实现,先看效果图

    device-2019-10-17-101627.gif

    功能
    1、基于RecyclerView实现,主要实现了列表数据的展现,包括了横向滚动的列表和竖向滚动的列表模式。

    2、控件对于上下左右事件的响应是立即的,不存在卡顿。

    3、实现了动态增加,修改和删除列表项的功能。

    4、图片的加载可以实现延迟加载,在用户松开遥控器时才加载,优化内存和网络加载的速度。

    5、内部封装了各种事件监听器,包括了
    1)焦点选择回调事件(MetroItemFocusListener),响应遥控器方向控制事件,当用户选择了列表中的一个项后,这个事件只在焦点移动停止后才会响应,回调给控件使用者,当前选择的项是哪个。
    2)列表项点击事件(MetroItemClickListener),当前焦点在列表中时,点击遥控器的确定键或触摸点击到某个项则回调控件使用者,当前点击的是哪个项。
    3)滚动到最顶端或最底端事件(OnScrollEndListener),当滚动到了列表的最顶端或最低端时,用户在继续按遥控器滚动时回调给控件使用者,当前已经到顶或底了,方便使用者加载分页数据。
    4)焦点移动事件(OnMoveToListener),响应遥控器方向控制事件,移动焦点框到选择的位置上,和焦点选择事件不同,焦点移动每次移动都会回调,不用等事件停止,用于与焦点框控件交互,告诉焦点框需要把焦点移动到什么位置。
    5)列表项长按事件(MetroItemLongClickListener),与列表项点击事件类似,不过这个只响应长按。
    6)滚动条位置监听事件(OnScrollBarStatusListener),每次滚动完回调给使用者,列表项还能否继续向上或向下滚动,用于提示滚动条的位置。

    6、优化滚动的效果,使界面滚动的之后更加平滑。

    7、列表控件界面自动获得焦点的功能。

    8、三种翻页模式:1.当焦点到了最后一行才往下滚,2.当焦点在倒数第二行在向下就自动滚动。3.在可以滚动时,焦点永远在中间,悠闲滚动布局,再移动焦点。

    先上代码

    package com.ihu11.metrorecylcerview;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    
    import com.ihu11.metro.flow.FlowView;
    import com.ihu11.metro.recycler.MetroItemClickListener;
    import com.ihu11.metro.recycler.MetroItemFocusListener;
    import com.ihu11.metro.recycler.MetroRecyclerView;
    import com.ihu11.metro.recycler.OnMoveToListener;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class GridActivity extends Activity {
    
        private MetroRecyclerView recyclerView;
        private FlowView flowView;
        private MyAdapter adapter;
        private List<Integer> dataList;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_grid);
    
            flowView = findViewById(R.id.flow_view);
            recyclerView = findViewById(R.id.recycler_view);
    
            recyclerView.setScrollType(MetroRecyclerView.SCROLL_TYPE_ALWAYS_CENTER);
            MetroRecyclerView.MetroGridLayoutManager layoutManager = new MetroRecyclerView.MetroGridLayoutManager(
                    this, 6, MetroRecyclerView.VERTICAL);
            recyclerView.setLayoutManager(layoutManager);
            recyclerView.setOnMoveToListener(new OnMoveToListener() {
                @Override
                public void onMoveTo(View view, float scale, int offsetX, int offsetY, boolean isSmooth) {
                    flowView.moveTo(view, scale, offsetX, offsetY, isSmooth);
                }
            });
            recyclerView.setOnItemClickListener(new MetroItemClickListener() {
                @Override
                public void onItemClick(View parentView, View itemView, int position) {
                    //TODO
                }
            });
            recyclerView.setOnItemFocusListener(new MetroItemFocusListener() {
                @Override
                public void onItemFocus(View parentView, View itemView, int position, int total) {
                    //TODO
                }
            });
            recyclerView.setOnScrollEndListener(new MetroRecyclerView.OnScrollEndListener() {
                @Override
                public void onScrollToBottom(int keyCode) {
                    //滑动到底部回调,用于刷新数据,每次到底部都会有回调,可能会回调多次,注意异步操作时的控制
                    int size = dataList.size();
                    dataList.addAll(genData());
                    adapter.notifyItemInserted(size);
                }
    
                @Override
                public void onScrollToTop(int keyCode) {
                }
            });
    
    
            dataList = genData();
            adapter = new MyAdapter(dataList);
            recyclerView.setAdapter(adapter);
            recyclerView.requestFocus();
        }
    
        private List<Integer> genData() {
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < 24; i++) {
                if (i % 5 == 0) {
                    list.add(R.drawable.a1);
                } else if (i % 5 == 1) {
                    list.add(R.drawable.a2);
                } else if (i % 5 == 2) {
                    list.add(R.drawable.a3);
                } else if (i % 5 == 3) {
                    list.add(R.drawable.a4);
                } else if (i % 5 == 4) {
                    list.add(R.drawable.a5);
                }
            }
            return list;
        }
    }
    

    MyAdapter

    package com.ihu11.metrorecylcerview;
    
    import android.support.annotation.NonNull;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ImageView;
    import android.widget.TextView;
    
    import com.ihu11.metro.recycler.MetroRecyclerView;
    
    import java.util.List;
    
    public class MyAdapter extends MetroRecyclerView.MetroAdapter<MyAdapter.ItemViewHolder> {
    
        private List<Integer> list;
    
        public MyAdapter(List<Integer> list) {
            this.list = list;
        }
    
        @Override
        public void onPrepareBindViewHolder(ItemViewHolder holder, int position) {
            holder.dataTxt.setText("p-" + position);
        }
    
        @Override
        public void onDelayBindViewHolder(ItemViewHolder holder, int position) {
            holder.icon.setBackgroundResource(list.get(position));
            Log.i("Catch", "onDelayBindViewHolder:" + position);
        }
    
        @Override
        public void onUnBindDelayViewHolder(ItemViewHolder holder) {
            holder.icon.setBackgroundResource(R.drawable.translate);
            Log.i("Catch", "onUnBindDelayViewHolder:" + holder.getAdapterPosition());
        }
    
        @NonNull
        @Override
        public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
            View convertView = LayoutInflater.from(viewGroup.getContext()).inflate(
                    R.layout.item, viewGroup, false);
            return new ItemViewHolder(convertView);
        }
    
        @Override
        public int getItemCount() {
            if (list == null) {
                return 0;
            }
            return list.size();
        }
    
        public final static class ItemViewHolder extends MetroRecyclerView.MetroViewHolder {
            TextView dataTxt;
            ImageView icon;
    
            public ItemViewHolder(View itemView) {
                super(itemView);
                dataTxt = itemView
                        .findViewById(R.id.text);
                icon = itemView.findViewById(R.id.img);
            }
        }
    }
    
    

    布局文件
    activity_grid.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 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"
        android:background="#ffB0C4DE">
    
        <com.ihu11.metro.recycler.MetroRecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginLeft="200px"
            android:layout_marginTop="50px"
            app:itemSpaceLeft="10px"
            app:itemSpaceRight="10px"
            app:itemSpaceTop="15px"
            app:itemSpaceBottom="15px"
            app:delayBindEnable="true"
            app:supportVLeftKey="true"
            app:supportVRightKey="true"
            app:focusViewOnFrontEnable="true"/>
    
        <com.ihu11.metro.flow.FlowView
            android:id="@+id/flow_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:flow_color1="@color/color3"
            app:flow_color2="@color/color2"
            app:flow_stroke_width="2px"
            app:round_radius="5px"
            app:viewType="normal" />
    
    </RelativeLayout>
    

    item.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="200px"
        android:layout_height="250px">//必须指定宽高
    
        <ImageView
            android:id="@+id/img"
            android:layout_width="200px"
            android:layout_height="200px" />
    
        <TextView
            android:id="@+id/text"
            android:layout_width="200px"
            android:layout_height="50px"
            android:layout_below="@+id/img"
            android:gravity="center" />
    
    </RelativeLayout>
    

    2.可删除项的功能

    先看看效果图


    device-2019-10-17-102920.gif

    结合上面的代码-加入关键代码

    recyclerView.setOnItemClickListener(new MetroItemClickListener() {
                @Override
                public void onItemClick(View parentView, View itemView, int position) {
                    recyclerView.deleteItem(position, dataList);
                }
            });
    

    3.垂直的单列类似ListView

    效果图


    device-2019-10-17-103921.gif

    只需要设置LayoutManager的spanCount为 1 方向为垂直的

    MetroRecyclerView.MetroGridLayoutManager layoutManager = new MetroRecyclerView.MetroGridLayoutManager(
                    this, 1, MetroRecyclerView.VERTICAL);
    

    也可以使用LinearyLayoutManager来实现

    LinearLayoutManager layoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
    

    4.水平方向的单列表

    效果图


    device-2019-10-17-104602.gif

    同上面类似只需要设置LayoutManager的spanCount为 1 方向为水平的

    MetroRecyclerView.MetroGridLayoutManager layoutManager = new MetroRecyclerView.MetroGridLayoutManager(
                    this, 1, MetroRecyclerView.HORIZONTAL);
    

    也可以使用LinearyLayoutManager来实现

    LinearLayoutManager layoutManager = new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false);
    

    5.实现原理

    实现流程图
    • 整体滚动的实现
      1)整体思路是,按到遥控器时计算滚动位置和需要滚动的距离,如果需要滚动则调用smoothScrollBy方法滚动界面,并控制焦点的位置,在滚动的时候计算出mLeftDistance剩余滚动距离的值,并在滚动回调方法里public void onScrolled(RecyclerView recyclerView, int dx, int dy)计算mLeftDistance的剩余值,只有当mLeftDistance的值为0了才结束滚动事件。每次操作都是对mLeftDistance的值的改变和计算。

      2)当连续的同方向的按键则会启动飞行事件,实现快速滚动mLeftDistance的值会不断的增加,为了不使界面过快,这个值是有上限的,当松开按键时则开始计算剩余mLeftDistance的滚动值,并自动计算焦点位置。

    • 为同时适配竖向和横向滚动布局将按键事件转化
      将上下左右按键转化为了上一个,下一个,上一行,下一行四个事件,在不同的布局方向事对应不同的转化。
      竖向滚动布局方向:
      上->上一行,下->下一行,左->上一个,右->下一个
      横向滚动布局方向:
      上->上一个,下->下一个,左->上一行,右->下一行

        // 进行按键转换,用于变化方向的时候直接映射到虚拟按键
        private int convertVirtualKeyCode(int keyCode) {
            if (mOrientation == VERTICAL) {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        return VIRTUAL_KEY_CODE_NEXT_ROW;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        return VIRTUAL_KEY_CODE_PRE_ROW;
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        return VIRTUAL_KEY_CODE_PRE_ONE;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        return VIRTUAL_KEY_CODE_NEXT_ONE;
                }
            } else {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        return VIRTUAL_KEY_CODE_NEXT_ONE;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        return VIRTUAL_KEY_CODE_PRE_ONE;
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        return VIRTUAL_KEY_CODE_PRE_ROW;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        return VIRTUAL_KEY_CODE_NEXT_ROW;
                }
            }
            return -1;
        }
    
    • 列表中头尾item的index计算
      1)findFirstCompletelyVisibleItemPosition计算屏幕中列表里第一个完全显示的列表项的位置。

      2)findLastCompletelyVisibleItemPosition计算屏幕列表里中最后完全显示的列表项的位置。

      3)findFirstVisibleItemPositionInScreen计算屏幕中第一个显示的列表项的位置(可能显示不完全)。

      4)findLastVisibleItemPositionInScreen计算屏幕中最后一个显示的列表项的位置(可能显示不完全)。

    • 计算下一个焦点的位置 computeNextPosition()
      计算返回值分成3种类型,一种是正确计算值,用于后续计算,另外两种一个是ERROR_POSITION,返回这个值则不拦截返回方便其他地方响应按键事件,NONE_POSTION为拦截返回,忽略此次响应。
      1)当按键事件为下一行:如果已经滑到最底部了则返回ERROR_POSITION状态,否则直接计算把当前位置加上列数则为下一行的位置,如果超出总数则下一行的位置是最后一个
      2)当按键事件为上一行:如果已经滑到顶部了则返回ERROR_POSITION状态,否则直接计算把当前位置减去列数则为上一行的位置。
      3)当按键事件为上一个:首先判断是否支持到第一个了在继续按上一个能不能往上滚动,如果不能,当前又在第一个,再判断是否是飞行状态,如果不是则返回ERROR_POSITION,如果是飞行状态则返回NONE_POSITION。前面没有retrun则继续往下计算,如果当前为飞行状态,只移动焦点的列数位置,不直接计算上一个位置的值,如果不为飞行状态,则把当前位置减一得到即可。
      4)当按键事件为下一个:同上类似,只是方向相反。

    • 列表正在滚动时特殊按键事件处理
      这些事件的响应前提都是正在滚动的时候
      1)左右一直按的时候,突然直接按上下:这时候的处理是忽略本次按键事件并滚动完成
      2)当上一个按键是往下滚一行,本次按键也是向下滚一行,且还没滚到最底部时则启动向下飞行事件加速滚动
      3)当上一个按键是往上滚一行,本次按键也是向上滚一行,且还没滚到最顶部时则启动向上飞行事件加速滚
      4)当长按左右按键,且发现下一个焦点view还没来得及加载时,则直接忽略本次按键
      5)当前正是飞行事件中时,突然按了左右键,则只改变当前列的位置,拦截本次按键事件,最后焦点位置还需等飞行结束后自动计算
      6)在飞行事件中,突然按相反方向键,则变为刹车事件
      7)普通滚动时的刹车动作则直接忽略这个事件等待滑动完成
      8)在启动了飞行之后,由于按键事件过多,且做了最大速度限制,中间忽略了一些按键,计算的距离也不能根据开始的位置来算了,所以当松开按键结束飞行时,只根据飞行启动前焦点在屏幕中的位置来决定飞行结束后的位置,取起飞前的位置作为基准点,自动滚动到起飞前的屏幕位置,然后结束飞行。
      这里的计算分为两步,第一步是保存起飞前的焦点位置,当飞行结束时,则根据飞行方向计算结束点的位置,这个是向下飞行的计算,向上飞行也类似

    • 误差计算:由于public void onScrolled(RecyclerView recyclerView, int dx, int dy)这个方法中返回的值是int类型的,但是屏幕滚动的实际值是float的,这样会造成滚动完成后提交滚动的距离与实际滚动的距离存在误差,则会造成计算的错误,所以在计算值的时候引入可接受的误差,这里的误差为1

    • 对父类RecyclerView反射方法的调用
      由于可直接继承方法smoothScrollBy(int dx, int dy)只提供了两个参数,无法传入加速度和滚动时间的变量,所以这样的调用方法无法达到盒子或电视上平滑滚动的需要,滚动起来特别难看,而提供了加速度和滚动时间参数的方法封装在父类的ViewFilnger中,所以这里需要使用反射,来调用这个方法

    • 第一个焦点的获取
      当在列表加载完后,需要默认获得焦点时,等待view的onGlobalLayout完成后才能得到view的位置

    • 图片延迟加载的实现
      1)对于滚动事件,在列表项较多的时候,图片过多的加载会影响到内存和网络加载速度,所以对RecyclerView.Adapter这个适配器进行了改造,拆分onBindViewHolder方法为两个onPrepareBindViewHolderonDelayBindViewHolder
      2)onPrepareBindViewHoler每次数据绑定都会执行,onDelayBindViewHolder只有在滚动停止后执行,只绑定当前界面显示的view的数据
      3)onUnBindDelayViewHolder主要是用户释放延迟绑定数据的资源,方便下次再来绑定时,view状态已经清空
      4)如何实现延迟绑定,MetroRecyclerView对象中有一个当前attached到列表中的view列表,它保存了当前界面里的存在的view列表,当滚动事件结束后再调用bindView方法,去绑定延迟加载的数据

    • 删除列表项的功能
      1)由于这里要考虑到焦点的问题,在删除列表项的时候,焦点有时候要自动移动或者界面要自动滚动,不能直接简单删除方法,所以需要重新实现
      2)当删除的是最后一项时,最后一项正好是第一列,则需要自动往上滚动一行,并把焦点挪动到删除后的最后一个,如果不是最后列,则直接删除并把焦点往前挪一个
      3)当删除的是列表中间某项时,则焦点框不动,先删除这项,后面的项都整体往前挪一格,如果行数减少了,需要自动滚动的时候,还要往上滚动一行,最后焦点位置还是原来的位置

    • 界面全局刷新功能
      因为界面有焦点,所以重新定义的刷新所有数据的方法(MetroRecyclerView.notifyResetData()),而不使用adapter的notifyDataSetChanged(),保证焦点不错乱

    这里焦点FlowView使用了不同的实现样式,可参见源码

    核心类MetroRecyclerView的实现代码太长了详见github
    github库---demo源码-https://github.com/ihu11/MetroRecyclerView

    相关文章

      网友评论

          本文标题:android tv 列表滚动控件-MetroRecyclerV

          本文链接:https://www.haomeiwen.com/subject/buhmmctx.html