Android 手写RecyclerView实现列表加载

目录
  • 前言
  • 1 RecyclerView的加载流程
  • 2 自定义RecyclerView
    • 2.1 RecyclerView三板斧
    • 2.2 初始化工作
    • 2.3 ItemView的获取与摆放
    • 2.4 复用池
    • 2.5 数据更新
  • 3 RecyclerView滑动事件处理
    • 3.1 点击事件与滑动事件
    • 3.2 scrollBy和scrollTo
    • 3.3 滑动带来的View回收
    • 3.4 加载机制
    • 3.5 RecyclerView下滑处理
    • 3.6 边界问题

前言

我相信一点,只要我们的产品中,涉及到列表的需求,肯定第一时间想到RecyclerView,即便是自定义View,那么RecyclerView也会是首选,为什么会选择RecyclerView而不是ListView,主要就是RecyclerView的内存复用机制,这也是RecyclerView的核心

当RecyclerView展示列表信息的时候,获取ItemView的来源有2个:一个是从适配器拿,另一个是从复用池中去拿;一开始的时候就是从复用池去拿,如果复用池中没有,那么就从Adapter中去拿,这个时候就是通过onCreateViewHolder来创建一个ItemView。

1 RecyclerView的加载流程

首先,当加载第一屏的时候,RecyclerView会向复用池中请求获取View,这个时候复用池中是空的,因此就需要我们自己创建的Adapter,调用onCreateViewHolder创建ItemView,然后onBindViewHolder绑定数据,展示在列表上

当我们滑动的时候第一个ItemView移出屏幕时,会被放到复用池中;同时,底部空出位置需要加载新的ItemView,触发加载机制,这个时候复用池不为空,拿到复用的ItemView,调用Adapter的onBIndViewHolder方法刷新数据,加载到尾部;

这里有个问题,放在复用池的仅仅是View吗?其实不是的,因为RecyclerView可以根据type类型加载不同的ItemView,那么放在复用池中的ItemView也是根据type进行归类,当复用的时候,根据type取出不同类型的ItemView;

例如ItemView07的类型是ImageView,那么ItemView01在复用池中的类型是TextView,那么在加载ItemView07时,从复用池中是取不到的,需要Adapter新建一个ImageView类型的ItemView。

2 自定义RecyclerView

其实RecyclerView,我们在使用的时候,知道怎么去用它,但是内部的原理并不清楚,而且就算是看了源码,时间久了就很容易忘记,所以只有当自己自定义RecyclerView之后才能真正了解其中的原理。

2.1 RecyclerView三板斧

通过第一节的加载流程,我们知道RecyclerView有3个重要的角色:RecyclerView、适配器、复用池,所以在自定义RecyclerView的时候,就需要先创建这3个角色;

/**
 * 自定义RecyclerView
 */
public class MyRecyclerView extends ViewGroup {

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public void scrollBy(int x, int y) {
        super.scrollBy(x, y);

    }
    interface Adapter<VH extends ViewHolder>{
        VH onCreateViewHolder(ViewGroup parent,int viewType);
        void onBindViewHolder(VH holder,int position);
        int getItemCount();
        int getItemViewType(int position);
    }
}
/**
 * 复用池
 */
public class MyRecyclerViewPool {
}
/**
 * Rv的ViewHolder
 */
public class ViewHolder {

    private View itemView;

    public ViewHolder(View itemView) {
        this.itemView = itemView;
    }
}

真正在应用层使用到的就是MyRecyclerView,通过设置Adapter实现View的展示

2.2 初始化工作

从加载流程中,我们可以看到,RecyclerView是协调Adapter和复用池的关系,因此在RecyclerView内部是持有这两个对象的引用的。

//持有Adapter和复用池的引用
private Adapter mAdapter;
private MyRecyclerViewPool myRecyclerViewPool;
//Rv的宽高
private int mWidth;
private int mHeight;
//itemView的高度
private int[] heights;

那么这些变量的初始化,是在哪里做的呢?首先肯定不是在构造方法中做的,我们在使用Adapter的时候,会调用setAdapter,其实就是在这个时候,进行初始化的操作。

public void setAdapter(Adapter mAdapter) {
    this.mAdapter = mAdapter;
    this.needLayout = true;
    //刷新页面
    requestLayout();
}

/**
 * 对子View进行位置计算摆放
 * @param changed
 * @param l
 * @param t
 * @param r
 * @param b
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if(changed || needLayout){
        needLayout = false;
        mWidth = r - l;
        mHeight = b - t;
    }
}

每次调用setAdapter的时候,都会调用requestLayout刷新重新布局,这个时候会调用onLayout,因为onLayout的调用很频繁非常耗性能,因此我们通知设置一个标志位needLayout,只有当需要刷新的时候,才能刷新重新摆放子View

2.3 ItemView的获取与摆放

其实在RecyclerView当中,是对每个子View进行了测量,得到了它们的宽高,然后根据每个ItemView的高度摆放,这里我们就写死了高度是200,仅做测试使用,后续优化。

那么在摆放的时候,比如我们有200条数据,肯定不会把200条数据全部加载进来,默认就展示一屏的数据,所以需要判断如果最后一个ItemView的bottom超过了屏幕的高度,就停止加载。

 @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if(changed || needLayout){
        needLayout = false;
        if(mAdapter != null){
            mWidth = r - l;
            mHeight = b - t;

            //计算每个ItemView的宽高,然后摆放位置
            rowCount = mAdapter.getItemCount();
            //这里假定每个ItemView的高度为200,实际Rv是需要测量每个ItemView的高度
            heights = new int[rowCount];
            for (int i = 0; i < rowCount; i++) {
                heights[i] = 200;
            }
            //摆放 -- 满第一屏就停止摆放
            for (int i = 0; i < rowCount; i++) {
                bottom = top + heights[i];
                //获取View
                ViewHolder holder = getItemView(i,0,top,mWidth,bottom);
                viewHolders.add(holder);
                //第二个top就是第一个的bottom
                top = bottom;
            }

        }
    }
}

我们先拿到之前的图,确定下子View的位置

其实每个子View的left都是0,right都是RecyclerView的宽度,变量就是top和bottom,其实从第2个ItemView开始,top都是上一个ItemView的bottom,那么bottom就是 top + ItemView的高度

在确定了子View的位置参数之后,就可以获取子View来进行摆放,其实在应用层是对子View做了一层包装 --- ViewHolder,因此这里获取到的也是ViewHolder。

private ViewHolder getItemView(int row,int left, int top, int right, int bottom) {

    ViewHolder viewHolder = obtainViewHolder(row,right - left,bottom - top);
    viewHolder.itemView.layout(left,top,right,bottom);
    return viewHolder;
}

private ViewHolder obtainViewHolder(int row, int width, int height) {

    ViewHolder viewHolder = null;
    //首先从复用池中查找

    //如果找不到,那么就通过适配器生成
    if(mAdapter !=null){
        viewHolder = mAdapter.onCreateViewHolder(this,mAdapter.getItemViewType(row));
    }
    return viewHolder;
}

通过调用obtainViewHolder来获取ViewHolder对象,其实是分2步的,首先 是从缓存池中去拿,在第一节加载流程中提及到,缓存池中不只是存了一个ItemView的布局,而是通过type标注了ItemView,所以从缓存池中需要根据type来获取,如果没有获取到,那么就调用Adapter的onCreateViewHolder获取,这种避免了每个ItemView都通过onCreateViewHolder创建,浪费系统资源;

在拿到了ViewHolder之后,调用根布局ItemView的layout方法进行位置摆放。

2.4 复用池

前面我们提到,在复用池中不仅仅是缓存了一个布局,而是每个type都对应一组回收的Holder,所以在复用池中存在一个容器存储ViewHolder

/**
 * 复用池
 */
public class MyRecyclerViewPool {

    static class scrapData{
        List<ViewHolder> viewHolders = new ArrayList<>();
    }

    private SparseArray<scrapData> array = new SparseArray<>();

    /**
     * 从缓存中获取ViewHolder
     * @param type ViewHolder的类型,用户自己设置
     * @return ViewHolder
     */
    public ViewHolder getRecyclerView(int type){

    }

    /**
     * 将ViewHolder放入缓存池中
     * @param holder
     */
    public void putRecyclerView(ViewHolder holder){

    }
}

当RecyclerView触发加载机制的时候,首先会从缓存池中取出对应type的ViewHolder;当ItemView移出屏幕之后,相应的ViewHolder会被放在缓存池中,因此存在对应的2个方法,添加及获取

/**
 * 从缓存中获取ViewHolder
 *
 * @param type ViewHolder的类型,用户自己设置
 * @return ViewHolder
 */
public static ViewHolder getRecyclerView(int type) {
    //首先判断type
    if (array.get(type) != null && !array.get(type).viewHolders.isEmpty()) {

        //将最后一个ViewHolder从列表中移除
        List<ViewHolder> scrapData = array.get(type).viewHolders;
        for (int i = scrapData.size() - 1; i >= 0; i--) {
            return scrapData.remove(i);
        }
    }
    return null;
}

/**
 * 将ViewHolder放入缓存池中
 *
 * @param holder
 */
public static void putRecyclerView(ViewHolder holder) {

    int key = holder.getItemViewType();
    //获取集合
    List<ViewHolder> viewHolders = getScrapData(key).viewHolders;
    viewHolders.add(holder);
}

private static ScrapData getScrapData(int key) {
    ScrapData scrapData = array.get(key);
    if(scrapData == null){
        scrapData = new ScrapData();
        array.put(key,scrapData);
    }
    return scrapData;
}

2.5 数据更新

无论是从缓存池中拿到了缓存的ViewHolder,还是通过适配器创建了ViewHolder,最终都需要将ViewHolder进行数据填充

private ViewHolder obtainViewHolder(int row, int width, int height) {

    int itemViewType = mAdapter.getItemViewType(row);
    //首先从复用池中查找
    ViewHolder viewHolder = MyRecyclerViewPool.getRecyclerView(itemViewType);
    //如果找不到,那么就通过适配器生成
    if(viewHolder == null){
        viewHolder = mAdapter.onCreateViewHolder(this,itemViewType);
    }
    //更新数据
    if (mAdapter != null) {
        mAdapter.onBindViewHolder(viewHolder, row);
        //设置ViewHOlder的类型
        viewHolder.setItemViewType(itemViewType);

        //测量
        viewHolder.itemView.measure(
                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        );
        addView(viewHolder.itemView);
    }
    return viewHolder;
}

如果跟到这里,我们其实已经完成了RecyclerView的基础功能,一个首屏列表的展示

3 RecyclerView滑动事件处理

3.1 点击事件与滑动事件

对于RecyclerView来说,我们需要的其实是对于滑动事件的处理,对于点击事件来说,通常是子View来响应,做相应的跳转或者其他操作,所以对于点击事件和滑动事件,RecyclerView需要做定向的处理。

那么如何区分点击事件和滑动事件?

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    switch (ev.getAction()){

        case MotionEvent.ACTION_MOVE:
            return true;
    }
    return false;
}

在容器中,如果碰到MOVE事件就拦截就认为是滑动事件,这种靠谱吗?显然 不是的,当手指点击到屏幕上时,首先系统会接收到一次ACTION_DWON时间,在手指抬起之前,ACTION_DWON只会响应一次,而且ACTION_MOVE会有无数次,因为人体手指是有面积的,当我们点下去肯定不是一个点,而是一个面肯定会存在ACTION_MOVE事件,但这种我们会认为是点击事件;

所以对于滑动事件,我们会认为当手指移动一段距离之后,超出某个距离就是滑动事件,这个最小滑动距离通过ViewConfiguration来获取。

private void init(Context context) {
    ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
    this.touchSlop = viewConfiguration.getScaledTouchSlop();
}

因为列表我们认为是竖直方向滑动的,所以我们需要记录手指在竖直方向上的滑动距离。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    //判断是否拦截
    boolean intercept = false;

    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            mCurrentY = (int) ev.getY();
            break;

        case MotionEvent.ACTION_MOVE:
            //y值在不停改变
            int y = (int) ev.getY();
            if(Math.abs(y - mCurrentY) > touchSlop){
                //认为是滑动了
                intercept = true;
            }
            break;
    }
    return intercept;
}

我们通过intercept标志位,来判断当前是否在进行滑动,如果滑动的距离超出了touchSlop,那么就将事件拦截,在onTouchEvent中消费这个事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            //判断滑动的方向
            int diff = (int) (mCurrentY - event.getRawY());
            if(Math.abs(diff) > touchSlop){
                Log.e(TAG,"diff --- "+diff);
                scrollBy(0, diff);
                mCurrentY = (int) event.getRawY();
            }
            break;
        }
    }
    return super.onTouchEvent(event);
}

3.2 scrollBy和scrollTo

在onTouchEvent中,我们使用了scrollBy进行滑动,那么scrollBy和scrollTo有什么区别,那就根据Android的坐标系开始说起

scrollBy滑动,其实是滑动的偏移量,相对于上一次View所在的位置,例如上图中,View上滑,偏移量就是(200 - 100 = 100),所以调用scrollBy(0,100)就是向上滑动,反之就是上下滑动;

scrollTo滑动,滑动的是绝对距离,例如上图中,View上滑,那么需要传入详细的坐标scrollTo(200,100),下滑scrollTo(200,300),其实scrollBy内部调用也是调用的scrollTo,所以偏移量就是用来计算绝对位置的。

3.3 滑动带来的View回收

当滑动屏幕的时候,有一部分View会被滑出到屏幕外,那么就涉及到了View的回收和View的重新摆放。

首先分析向上滑动的操作,首先我们用scrollY来标记,屏幕中第一个子View左上角距离屏幕左上角的距离,默认就是0.

@Override
public void scrollBy(int x, int y) {
    super.scrollBy(x, y);
    scrollY += y;

    if (scrollY > 0) {
        Log.e(TAG, "上滑");
        //防止一次滑动多个子View出去
        while (scrollY > heights[firstRow]) {
            //被移除,放入回收池
            if (!viewHolders.isEmpty()) {
                removeView(viewHolders.remove(0));
            }
            scrollY -= heights[firstRow];
            firstRow++;
        }

    } else {
        Log.e(TAG, "下滑");
    }
}

当ItemView1移出屏幕之后,因为上滑scrollY > 0,所以scrollY肯定会超过Itemiew 的高度,这里有个情况就是,如果一次滑出去多个ItemView,那么高度肯定是超过单个ItemView的高度,这里用firstRow来标记,当前子View在数据集合中的位置,所以这里使用的是while循环。

/**
 * 移除ViewHolder,放入回收池
 *
 * @param holder
 */
private void removeView(ViewHolder holder) {
    MyRecyclerViewPool.putRecyclerView(holder);
    //系统方法,从RecyclerView中移除这个View
    removeView(holder.itemView);
    viewHolders.remove(holder);
}

如果滑出去多个子View,那么就循环从viewHolders(当前屏幕展示的View的集合)中移除,移除的ViewHolder就被放在了回收池中,然后从当前屏幕中移除;

3.4 加载机制

既然有移除,那么就会有新增,当底部出现空缺的时候,就会触发加载机制,那么每次移除一个元素,都会有一个元素添加进来吗?其实不然

像ItemView1移除之后,最底部的ItemView还没有完全展示出来,其实是没有触发加载的,那么什么时候触发加载呢?

在当前屏幕中展示的View其实是在缓存中的,那么只要计算缓存中全部ItemView的高度跟屏幕的高度比较,如果不足就需要填充。

 //如果小于屏幕的高度
 while (getRealHeight(firstRow) <= mHeight) {
     //触发加载机制
     int addIndex = firstRow + viewHolders.size();
     ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
     viewHolders.add(viewHolders.size(), viewHolder);
     Log.e(TAG,"添加一个View");
 }
/**
 * 获取实际展示的高度
 *
 * @param firstIndex
 * @return
 */
private int getRealHeight(int firstIndex) {
    return getSumArray(firstRow, viewHolders.size()) - scrollY;
}

private int getSumArray(int firstIndex, int count) {
    int totalHeight = 0;
    count+= firstIndex;
    for (int i = firstIndex; i < count; i++) {
        totalHeight += heights[i];
    }
    return totalHeight;
}

这样其实就实现了,一个View移除屏幕之后,会有一个新的View添加进来

/**
 * 重新摆放View
 */
private void repositionViews() {
    int left = 0;
    int top = -scrollY;
    int right = mWidth;
    int bottom = 0;

    int index = firstRow;

    for (int i = 0; i < viewHolders.size(); i++) {
        bottom = top + heights[index++];
        viewHolders.get(i).itemView.layout(left,top,right,bottom);
        top = bottom;
    }
}

当然新的View只要添加进来,就需要对他进行重新摆放,这样上滑就实现了(只有上滑哦)

3.5 RecyclerView下滑处理

在此之前,我们处理了上滑的事件,顶部的View移出,下部分的View添加进来,那么下滑正好相反。

那么下滑添加View的时机是什么呢?就是scrollY小于0的时候,会有新的View添加进来

//下滑顶部添加View
while (scrollY < 0) {

    //获取ViewHolder
    ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);
    //放到屏幕缓存ViewHolder最顶部的位置
    viewHolders.add(0, viewHolder);
    firstRow--;
    //当顶部ItemView完全加进来之后,需要改变scrollY的值
    scrollY += heights[firstRow];
}

此时需要将添加的View,放在屏幕展示View缓存的首位,然后firstRow需要-1;

那么当新的View添加进来之后,底部View需要移除,那么移除的时机是什么呢?先把尾部最后一个View的高度抛开,继续往下滑动,如果当前屏幕展示的View的高度超过了屏幕高度,那么就需要移除

//底部移除View
while (!viewHolders.isEmpty() &&
        getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {
    //需要移除
    removeView(viewHolders.remove(viewHolders.size() - 1));
}

3.6 边界问题

当我们上滑或者下滑的时候,firstRow都在递增或者递减,但是firstRow肯定是有边界的,例如滑到最上端的时候,firstRow最小就是0,如果再-1,那么就会数组越界,最下端也有边界,那就是数组的最大长度。

/**
 * @param scrollY
 * @param firstRow
 */
private void scrollBounds(int scrollY, int firstRow) {

    if (scrollY > 0) {
        //上滑
        if (getSumArray(firstRow, heights.length - firstRow) - scrollY > mHeight) {
            this.scrollY = scrollY;
        } else {
            this.scrollY = getSumArray(firstRow, heights.length - firstRow) - mHeight;
        }
    } else {
        //下滑
        this.scrollY = Math.max(scrollY, -getSumArray(0, firstRow));
    }
}

首先看下滑,这个时候firstRow > 0,这个时候getSumArray的值是逐渐减小的,等到最顶部,也就是滑到firstRow = 0的时候,这个时候getSumArray = 0,那么再往下滑其实还是能滑的,这个时候我们需要做限制,取scrollY 和 getSumArray的最大值,如果一致下滑,getSumArray一致都是0,然后scrollY < 0,最终scrollY = 0,不会再执行下滑的操作了。

接下来看上滑,正常情况下,如果200条数据,那么当firstRow = 10的时候,剩下190个ItemView的高度(减去上滑的高度)肯定是高于屏幕高度的,那么一直滑,当发现剩余的ItemView的高度不足以占满整个屏幕的时候,就是没有数据了,这个时候,其实就可以把scrollY设置为0,不能再继续滑动了。

 @Override
 public void scrollBy(int x, int y) {
//        super.scrollBy(x, y);

     scrollY += y;
     scrollBounds(scrollY, firstRow);

     if (scrollY > 0) {
         Log.e(TAG, "上滑");
         //防止一次滑动多个子View出去
         while (scrollY > heights[firstRow]) {
             //被移除,放入回收池
             if (!viewHolders.isEmpty()) {
                 removeView(viewHolders.remove(0));
             }
             scrollY -= heights[firstRow];
             firstRow++;
             Log.e("scrollBy", "scrollBy 移除一个View size =="+viewHolders.size());
         }

         //如果小于屏幕的高度
         while (getRealHeight(firstRow) < mHeight) {
             //触发加载机制
             int addIndex = firstRow + viewHolders.size();
             ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
             viewHolders.add(viewHolders.size(), viewHolder);
             Log.e("scrollBy", "scrollBy 添加一个View size=="+viewHolders.size());
         }
         //重新摆放
         repositionViews();

     } else {
         Log.e(TAG, "下滑");

         //底部移除View
         while (!viewHolders.isEmpty() &&
                 getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {
             //需要移除
             removeView(viewHolders.remove(viewHolders.size() - 1));
         }

         //下滑顶部添加View
         while (scrollY < 0) {

             //获取ViewHolder
             ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);
             //放到屏幕缓存ViewHolder最顶部的位置
             viewHolders.add(0, viewHolder);
             firstRow--;
             //当顶部ItemView完全加进来之后,需要改变scrollY的值
             scrollY += heights[firstRow];
         }
     }
 }

OK,这其实跟RecyclerView的源码相比,简直就是一个穷人版的RecyclerView,但是其中的思想我们是可以借鉴的,尤其是回收池的思想,在开发中是可以借鉴的,下面展示的就是最后的成果

到此这篇关于Android 手写RecyclerView实现列表加载的文章就介绍到这了,更多相关Android RecyclerView 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android RecyclerView实现九宫格效果

    RecyclerView更加优化的复用机制和方便实现UI效果,几乎替代Listview和GridView的使用.但是分割线的实现,需要自己继承ItemDecoration来绘制. 效果图 item的布局文件 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  • Android MVVM架构实现RecyclerView列表详解流程

    目录 效果图 导入引用 导入Recyclerview依赖 导入dataBinding引用 代码解析 建立实体类 建立RecyclerView子项 适配器 建立适配器 设置子项点击事件 adapter全部代码 建立VM层 子项点击事件的使用 VM层代码 数据与视图交互 效果图 导入引用 导入Recyclerview依赖 implementation 'androidx.recyclerview:recyclerview:1.1.0' 导入dataBinding引用 dataBinding { en

  • Android Recyclerview实现左滑删除功能

    本文实例为大家分享了Android Recyclerview实现左滑删除的具体代码,供大家参考,具体内容如下 1.先创建一个工具类 SlideRecyclerView public class SlideRecyclerView extends RecyclerView {       private static final String TAG = "SlideRecyclerView";     private static final int INVALID_POSITION

  • Android利用RecyclerView实现列表倒计时效果

    最近面试时,面试官问了一个列表倒计时效果如何实现,现在记录一下. 运行效果图 实现思路 实现方法主要有两个: 1.为每个开始倒计时的item启动一个定时器,再做更新item处理: 2.只启动一个定时器,然后遍历数据,再做再做更新item处理. 经过思考,包括性能.实现等方面,决定使用第2种方式实现. 实现过程 数据实体 /** * 总共的倒计时的时间(结束时间-开始时间),单位:毫秒 * 例: 2019-02-23 11:00:30 与 2019-02-23 11:00:00 之间的相差的毫秒数

  • Android中RecyclerView 滑动时图片加载的优化

    RecyclerView 滑动时的优化处理,在滑动时停止加载图片,在滑动停止时开始加载图片,这里用了Glide.pause 和Glide.resume.这里为了避免重复设置增加开销,设置了一个标志变量来做判断. mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, in

  • Android列表RecyclerView排列布局

    本文实例为大家分享了Android列表RecyclerView排列布局的具体代码,供大家参考,具体内容如下 效果图: 1.要添加相关的依赖 implementation 'androidx.recyclerview:recyclerview:1.1.0' 2.然后布局文件中准备容器 这个标签是显示目标容器对象的,其他需求可自定义 <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_list" android

  • Android XRecyclerView实现多条目加载

    本文实例为大家分享了Android实现多条目加载展示的具体代码,供大家参考,具体内容如下 展示效果 这里写图片描述 依赖 testCompile 'junit:junit:4.12' compile 'com.hjm:BottomTabBar:1.1.1' compile 'com.android.support:design:23.4.0' compile 'com.android.support:mediarouter-v7:25.0.0' compile 'com.android.supp

  • 解决Android-RecyclerView列表倒计时错乱问题

    前言 转眼间距离上次写博客已是过了一个年轮,期间发生了不少事:经历了离职.找工作,新公司的第一版项目上线.现在总算是有时间可以将遇到的问题梳理下了,后期有时间也会分享更多的东西-- 场景 今天分享的问题是当在列表里面显示倒计时,这时候滑动列表会出现时间显示不正常的问题.首先关于倒计时我们需要注意的问题有以下几方面: 在RecyclerView中ViewHolder的复用导致的时间乱跳的问题. 滑动列表时倒计时会重置的问题. 在退出页面后定时器的资源释放问题,这里我使用的是用系统自带的CountD

  • Android 手写RecyclerView实现列表加载

    目录 前言 1 RecyclerView的加载流程 2 自定义RecyclerView 2.1 RecyclerView三板斧 2.2 初始化工作 2.3 ItemView的获取与摆放 2.4 复用池 2.5 数据更新 3 RecyclerView滑动事件处理 3.1 点击事件与滑动事件 3.2 scrollBy和scrollTo 3.3 滑动带来的View回收 3.4 加载机制 3.5 RecyclerView下滑处理 3.6 边界问题 前言 我相信一点,只要我们的产品中,涉及到列表的需求,肯

  • Android RecyclerView上拉加载和下拉刷新

    之前写过一篇刷新加载<RecyclerView上拉加载和下拉刷新(基础版)> ,这次是进行改装完善. 代码中注释的很详细,所以就直接上代码了. 核心实现 package com.example.fly.recyclerviewrefresh.base; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.widget.SwipeRefreshLayout;

  • Android RecyclerView 上拉加载更多及下拉刷新功能的实现方法

    RecyclerView 已经出来很久了,但是在项目中之前都使用的是ListView,最近新的项目上了都大量的使用了RecycleView.尤其是瀑布流的下拉刷新,网上吧啦吧啦没有合适的自己总结了一哈. 先贴图上来看看: 使用RecyclerView实现上拉加载更多和下拉刷新的功能我自己有两种方式: 1.使用系统自带的Android.support.v4.widget.SwipeRefreshLayout这个控价来实现. 2.自定义的里面带有RecyleView的控件. 使用RecycleVie

  • Android RecyclerView上拉加载更多功能回弹实现代码

    实现原理是使用RecyclerView的OnTouchListener方法监听滑动 在adapter里面增加两项footview 其中date.size为显示的加载条,可以自定义,date.size+1为空白的View,我们设置其高度为0 我们通过LinearLayoutManager的 findLastVisibleItemPosition判断显示的最后一条数据,如果是空白view,表示加载条已经完全展示,松开即可刷新. 回弹效果是通过在滑动时动态改变空白view的高度,达到阻尼效果 ,回弹时

  • Android插件化之资源动态加载

    Android插件化之资源动态加载 一.概述 Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题 1.如何加载插件资源 2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找.这样能做到动态更新的效果. 3.如何确保插件和宿主使用到的是被修改过的资源. 二.原理分析 在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理. 1.资源链 Contex

  • Android自定义view实现阻尼效果的加载动画

    效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动.衰减振动.[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来.这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运

  • Android使用PullToRefresh实现上拉加载和下拉刷新效果的代码

    在没给大家介绍正文之前,先给大家介绍展示下运行图,如果大家感觉还不错,请继续往下阅读: 相关阅读:分享Android中pullToRefresh的使用心得 项目已同步至:https://github.com/nanchen2251/pullToRefreshDemo 简单使用详情: 1)studio可以直接在app的module设置中直接进行搜索,但是有-的必须添上,而不能用空格代替,为了更加了解这个东西,我还是推荐大家去这里看看,奉上网址: https://github.com/chrisba

  • Android下拉刷新上拉加载控件(适用于所有View)

    前面写过一篇关于下拉刷新控件的文章下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅如此,我已经把它改成了对所有View都通用!可以随心所欲使用这两个功能~~ 我做了一个大集合的demo,实现了ListView.GridView.ExpandableListView.ScrollView.WebView.ImageView.TextView的下拉刷新和上拉加载.后面会提供demo的

  • Android 自定义圆形头像CircleImageView支持加载网络图片的实现代码

    在Android开发中我们常常用到圆形的头像,如果每次加载之后再进行圆形裁剪特别麻烦.所以在这里写一个自定义圆形ImageView,直接去加载网络图片,这样的话就特别的方便. 先上效果图 主要的方法 1.让自定义 CircleImageView 继承ImageView /** * 自定义圆形头像 * Created by Dylan on 2015/11/26 0026. */ public class CircleImageView extends ImageView { } 2.在构造方法中

  • Android 使用 Path 实现搜索动态加载动画效果

    今天实现一个搜索动态加载数据的动画效果,还是先看效果吧,用文字描述干巴巴的,看图说话什么都明白了, 实现这个就是使用Path中的getSegment()不断的去改变它截取片段的start和stop,再结合动画,今天就分步骤实现它,看完以后你也会觉的不是很难,只是没想到这么实现而已,所以要多见识,所谓眼界决定你的高度,还是延续我写博客的习惯,一步步分析,第一步就是绘制如下图: 如果单纯的绘制这个图很简单很简单的,绘制一个圆,然后再绘制一根线就搞定,但是要考虑这里的效果,就不能这么干了,如果你看了上

随机推荐