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>
我是直接上的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和下面的虚拟按键都被铺满了
如果我们不让内容填充到状态栏呢?就是不遮挡状态栏怎么办?如下图,有三种做法
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