android tv 列表滚动控件-MetroRecyclerView

焦点展示结合了----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 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 genData() {
        List 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 {

    private List list;

    public MyAdapter(List 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




    

    


item.xml


//必须指定宽高

    

    


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 列表滚动控件-MetroRecyclerView)