RecyclerView用法和源码深度解析

目录介绍

  • 1.RecycleView的结构
  • 2.Adapter
    • 2.1 RecyclerView.Adapter扮演的角色
    • 2.2 重写的方法
    • 2.3 notifyDataSetChanged()刷新数据
    • 2.4 数据变更通知之观察者模式
      • a.首先看.notifyDataSetChanged()源码
      • b.接着查看.notifyChanged()源码
      • c.接着查看setAdapter()源码中的setAdapterInternal(adapter, false, true)方法
      • d.notify……方法被调用,刷新数据
  • 3.ViewHolder
    • 3.1 ViewHolder的作用
    • 3.2 ViewHolder与复用
    • 3.3 ViewHolder简单封装
  • 4.LayoutManager
    • 4.1 作用
    • 4.2 LayoutManager样式
    • 4.3 LayoutManager当前有且仅有一个抽象函数
    • 4.4 setLayoutManager(LayoutManager layout)源码
  • 5.ItemDecoration
    • 5.1 作用
    • 5.2 RecyclerView.ItemDecoration是一个抽象类
    • 5.3 addItemDecoration()源码分析
      • a.首先看addItemDecoration源码
      • b.接着看下markItemDecorInsetsDirty这个方法
      • c.接着看下mRecycler.markItemDecorInsetsDirty();这个方法
      • d.回过头在看看addItemDecoration中requestLayout方法
      • e.在 RecyclerView 中搜索 mItemDecorations 集合
  • 6.ItemAnimator
    • 6.1 作用
    • 6.2 触发的三种事件
  • 7.其他知识点
    • 7.1 Recycler && RecycledViewPool
    • 7.2 Recyclerview.getLayoutPosition()区别
  • 8.RecyclerView嵌套方案滑动冲突解决方案
    • 8.1 如何判断RecyclerView控件滑动到顶部和底部
    • 8.2 RecyclerView嵌套RecyclerView 条目自动上滚的Bug
    • 8.3 ScrollView嵌套RecyclerView滑动冲突
    • 8.4 ViewPager嵌套水平RecyclerView横向滑动到底后不滑动ViewPager
  • 9.RecyclerView复杂布局封装库案例
    • 9.1 能够实现业务的需求和功能
    • 9.2 具备的优势分析
  • 10.针对阿里VLayout代码分析
  • 11.版本更新说明
    • v1.0.0 2016年5月5日
    • v1.1.0 更新于2017年2月1日
    • v1.1.1 更新于2017年6月9日
    • v2.0.0 更新于2018年8月21日
    • v2.1.0 更新于2018年9月29日

好消息

1.RecycleView的结构

  • 关于RecyclerView,大家都已经很熟悉了,用途十分广泛,大概结构如下所示
    • RecyclerView.Adapter - 处理数据集合并负责绑定视图
    • ViewHolder - 持有所有的用于绑定数据或者需要操作的View
    • LayoutManager - 负责摆放视图等相关操作
    • ItemDecoration - 负责绘制Item附近的分割线
    • ItemAnimator - 为Item的一般操作添加动画效果,如,增删条目等
  • 如图所示,直观展示结构
    • image
  • 针对上面几个属性,最简单用法如下所示
    recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    //设置layoutManager
    recyclerView.setLayoutManager(layoutManager);
    final RecycleViewItemLine line = new RecycleViewItemLine(this, LinearLayout.HORIZONTAL,1,this.getResources().getColor(R.color.colorAccent));
    //设置添加分割线
    recyclerView.addItemDecoration(line);
    adapter = new MultipleItemAdapter(this);
    //设置adapter
    recyclerView.setAdapter(adapter);
    //添加数据并且刷新adapter
    adapter.addAll(list);
    adapter.notifyDataSetChanged();
    
    
    //adapter
    //onCreateViewHolder(ViewGroup parent, int viewType)这里的第二个参数就是View的类型,可以根据这个类型判断去创建不同item的ViewHolder
    public class MultipleItemAdapter extends RecyclerView.Adapter<recyclerview.viewholder> {
        public static enum ITEM_TYPE {
            ITEM_TYPE_IMAGE,
            ITEM_TYPE_TEXT
        }
         
        private final LayoutInflater mLayoutInflater;
        private final Context mContext;
        private ArrayList<String> mTitles;
     
        public MultipleItemAdapter(Context context) {
            mContext = context;
            mLayoutInflater = LayoutInflater.from(context);
        }
     
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) {
                return new ImageViewHolder(mLayoutInflater.inflate(R.layout.item_image, parent, false));
            } else {
                return new TextViewHolder(mLayoutInflater.inflate(R.layout.item_text, parent, false));
            }
        }
     
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof TextViewHolder) {
                ((TextViewHolder) holder).mTextView.setText(mTitles[position]);
            } else if (holder instanceof ImageViewHolder) {
                ((ImageViewHolder) holder).mTextView.setText(mTitles[position]);
            }
        }
     
        @Override
        public int getItemViewType(int position) {
            return position % 2 == 0 ? ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal() : ITEM_TYPE.ITEM_TYPE_TEXT.ordinal();
        }
     
        @Override
        public int getItemCount() {
            return mTitles == null ? 0 : mTitles.length;
        }
        
        public void addAll(ArrayList<String> list){
            if(mTitles!=null){
                mTitles.clear();
            }else {
                mTitles = new ArrayList<>();
            }
            mTitles.addAll(list);
        }
     
        public static class TextViewHolder extends RecyclerView.ViewHolder {
            @InjectView(R.id.text_view)
            TextView mTextView;
            TextViewHolder(View view) {
                super(view);
                ButterKnife.inject(this, view);
            }
        }
     
        public static class ImageViewHolder extends RecyclerView.ViewHolder {
            @InjectView(R.id.text_view)
            TextView mTextView;
            @InjectView(R.id.image_view)
            ImageView mImageView;
            ImageViewHolder(View view) {
                super(view);
                ButterKnife.inject(this, view);
            }
        }
    }
    

2.Adapter

2.1 RecyclerView.Adapter扮演的角色

  • 一是,根据不同ViewType创建与之相应的的Item-Layout
  • 二是,访问数据集合并将数据绑定到正确的View上

2.2 重写的方法

  • 一般常用的重写方法有以下这么几个:
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    创建Item视图,并返回相应的ViewHolder
    public void onBindViewHolder(VH holder, int position)
    绑定数据到正确的Item视图上。
    public int getItemCount()
    返回该Adapter所持有的Itme数量
    public int getItemViewType(int position)
    用来获取当前项Item(position参数)是哪种类型的布局
    

2.3 notifyDataSetChanged()刷新数据

  • 当时据集合发生改变时,我们通过调用.notifyDataSetChanged(),来刷新列表,因为这样做会触发列表的重绘,所以并不会出现任何动画效果,因此需要调用一些以notifyItem*()作为前缀的特殊方法,比如:
    • public final void notifyItemInserted(int position) 向指定位置插入Item
    • public final void notifyItemRemoved(int position) 移除指定位置Item
    • public final void notifyItemChanged(int position) 更新指定位置Item

2.4 数据变更通知之观察者模式

  • a.首先看.notifyDataSetChanged()源码
    /** @see #notifyItemChanged(int)
     * @see #notifyItemInserted(int)
     * @see #notifyItemRemoved(int)
     * @see #notifyItemRangeChanged(int, int)
     * @see #notifyItemRangeInserted(int, int)
     * @see #notifyItemRangeRemoved(int, int)
     */
    public final void notifyDataSetChanged() {
        mObservable.notifyChanged();
    }
    
  • b.接着查看.notifyChanged();源码
    • 被观察者AdapterDataObservable,内部持有观察者AdapterDataObserver集合
    static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        public boolean hasObservers() {
            return !mObservers.isEmpty();
        }
    
        public void notifyChanged() {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    
        public void notifyItemRangeChanged(int positionStart, int itemCount) {
            notifyItemRangeChanged(positionStart, itemCount, null);
        }
    
        public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
            }
        }
    
        public void notifyItemRangeInserted(int positionStart, int itemCount) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
            }
        }
    }
    
    • 观察者AdapterDataObserver,具体实现为RecyclerViewDataObserver,当数据源发生变更时,及时响应界面变化
    public static abstract class AdapterDataObserver {
        public void onChanged() {
            // Do nothing
        }
    
        public void onItemRangeChanged(int positionStart, int itemCount) {
            // do nothing
        }
    
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            onItemRangeChanged(positionStart, itemCount);
        }
    }
    
  • c.接着查看setAdapter()源码中的setAdapterInternal(adapter, false, true)方法
    • setAdapter源码
    public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        requestLayout();
    }
    
    • setAdapterInternal(adapter, false, true)源码
    private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            //注册一个观察者RecyclerViewDataObserver
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
        markKnownViewsInvalid();
    }
    
  • d.notify……方法被调用,刷新数据
    • 当数据变更时,调用notify**方法时,Adapter内部的被观察者会遍历通知已经注册的观察者的对应方法,这时界面就会响应变更。

3.ViewHolder

3.1 ViewHolder的作用

  • ViewHolder作用大概有这些:
    • adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作
    • 其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型。item的ViewHolder时调用来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。

3.2 ViewHolder与复用

  • 在复写RecyclerView.Adapter的时候,需要我们复写两个方法:
    • onCreateViewHolder
    • onBindViewHolder
    • 这两个方法从字面上看就是创建ViewHolder和绑定ViewHolder的意思
  • 复用机制是怎样的?
    • 模拟场景:只有一种ViewType,上下滑动的时候需要的ViewHolder种类是只有一种,但是需要的ViewHolder对象数量并不止一个。所以在后面创建了5个ViewHolder之后,需要的数量够了,无论怎么滑动,都只需要复用以前创建的对象就行了。那么逗比程序员们思考一下,为什么会出现这种情况呢
    • 看到了下面log之后,第一反应是在这个ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法,但是onBindViewHolder方法每次都会调用的
    • image
  • 查看一下createViewHolder源代码
    • 发现这里并没有限制
    public final VH createViewHolder(ViewGroup parent, int viewType) {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        final VH holder = onCreateViewHolder(parent, viewType);
        holder.mItemViewType = viewType;
        TraceCompat.endSection();
        return holder;
    }
    
  • 对于ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法,可以查看
    • 获取为给定位置初始化的视图。
    • 此方法应由{@link LayoutManager}实现使用,以获取视图来表示来自{@LinkAdapter}的数据。
    • 如果共享池可用于正确的视图类型,则回收程序可以重用共享池中的废视图或分离视图。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试发回一个以前为该数据初始化的报废视图,而不进行重新绑定。
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
        //代码省略了,有需要的小伙伴可以自己看看,这里面逻辑实在太复杂呢
    }
    

3.3 ViewHolder简单封装

  • 关于ViewHolder简单的封装代码如下所示:
    public abstract class BaseMViewHolder<M> extends RecyclerView.ViewHolder {
    
    
        // SparseArray 比 HashMap 更省内存,在某些条件下性能更好,只能存储 key 为 int 类型的数据,
        // 用来存放 View 以减少 findViewById 的次数
    
        private SparseArray<View> viewSparseArray;
    
        BaseMViewHolder(View itemView) {
            super(itemView);
            if(viewSparseArray==null){
                viewSparseArray = new SparseArray<>();
            }
        }
    
        public BaseMViewHolder(ViewGroup parent, @LayoutRes int res) {
            super(LayoutInflater.from(parent.getContext()).inflate(res, parent, false));
            if(viewSparseArray==null){
                viewSparseArray = new SparseArray<>();
            }
        }
    
        /**
         * 子类设置数据方法
         * @param data
         */
        public void setData(M data) {}
    
        /**
         * 第二种findViewById方式
         * 根据 ID 来获取 View
         * @param viewId viewID
         * @param <T>    泛型
         * @return 将结果强转为 View 或 View 的子类型
         */
        @SuppressWarnings("unchecked")
        protected <T extends View> T getView(int viewId) {
            // 先从缓存中找,找打的话则直接返回
            // 如果找不到则 findViewById ,再把结果存入缓存中
            View view = viewSparseArray.get(viewId);
            if (view == null) {
                view = itemView.findViewById(viewId);
                viewSparseArray.put(viewId, view);
            }
            return (T) view;
        }
    
    
        /**
         * 获取上下文context
         * @return          context
         */
        protected Context getContext(){
            return itemView.getContext();
        }
    
    
        /**
         * 获取数据索引的位置
         * @return          position
         */
        protected int getDataPosition(){
            RecyclerView.Adapter adapter = getOwnerAdapter();
            if (adapter!=null && adapter instanceof RecyclerArrayAdapter){
                return getAdapterPosition() - ((RecyclerArrayAdapter) adapter).getHeaderCount();
            }
            return getAdapterPosition();
        }
    
    
        /**
         * 获取adapter对象
         * @param <T>
         * @return                  adapter
         */
        @Nullable
        private  <T extends RecyclerView.Adapter> T getOwnerAdapter(){
            RecyclerView recyclerView = getOwnerRecyclerView();
            //noinspection unchecked
            return recyclerView != null ? (T) recyclerView.getAdapter() : null;
        }
    
    
        @Nullable
        private RecyclerView getOwnerRecyclerView(){
            try {
                Field field = RecyclerView.ViewHolder.class.getDeclaredField("mOwnerRecyclerView");
                field.setAccessible(true);
                return (RecyclerView) field.get(this);
            } catch (NoSuchFieldException ignored) {
                ignored.printStackTrace();
            } catch (IllegalAccessException ignored) {
                ignored.printStackTrace();
            }
            return null;
        }
    
    
        /**
         * 添加子控件的点击事件
         * @param viewId                        控件id
         */
        protected void addOnClickListener(@IdRes final int viewId) {
            final View view = getView(viewId);
            if (view != null) {
                if (!view.isClickable()) {
                    view.setClickable(true);
                }
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(getOwnerAdapter()!=null){
                            if (((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener() != null) {
                                ((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener()
                                        .onItemChildClick(v, getDataPosition());
                            }
                        }
                    }
                });
            }
        }
    
        //省略部分代码
        //关于adapter封装可以查看我的开源adpater封装库:https://github.com/yangchong211/YCBaseAdapter
        //关于recyclerView封装库,可以查看我的开源库:https://github.com/yangchong211/YCRefreshView
    }
    

4.LayoutManager

4.1 作用

  • LayoutManager的职责是摆放Item的位置,并且负责决定何时回收和重用Item。
  • RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager

4.2 LayoutManager样式

  • LinearLayoutManager 水平或者垂直的Item视图。
  • GridLayoutManager 网格Item视图。
  • StaggeredGridLayoutManager 交错的网格Item视图。

4.3 LayoutManager当前有且仅有一个抽象函数

  • 具体如下:
    public LayoutParams generateDefaultLayoutParams()
    

4.4 setLayoutManager(LayoutManager layout)源码

  • a.setLayoutManager入口源码
    • 分析:当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout ,重新请求 measure、layout、draw。
    public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        // 停止滑动
        stopScroll();
        if (mLayout != null) {
            // 如果有动画,则停止所有的动画
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            // 移除并回收视图
            mLayout.removeAndRecycleAllViews(mRecycler);
            // 回收废弃视图
            mLayout.removeAndRecycleScrapInt(mRecycler);
            //清除mRecycler
            mRecycler.clear();
            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout +
                        " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        //更新新的缓存数据
        mRecycler.updateViewCacheSize();
        //重新请求 View 的测量、布局、绘制
        requestLayout();
    }
    

5.ItemDecoration

5.1 作用

  • 通过设置recyclerView.addItemDecoration(new DividerDecoration(this));来改变Item之间的偏移量或者对Item进行装饰。
  • 当然,你也可以对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。

5.2 RecyclerView.ItemDecoration是一个抽象类

  • 该抽象类常见的方法如下所示:
    public void onDraw(Canvas c, RecyclerView parent)
    装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡
    public void onDrawOver(Canvas c, RecyclerView parent)
    装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。
    

5.3 .addItemDecoration()源码分析

  • a.通过下面代码可知,mItemDecorations是一个ArrayList,我们将ItemDecoration也就是分割线对象,添加到其中。
    • 可以看到,当通过这个方法添加分割线后,会指定添加分割线在集合中的索引,然后再重新请求 View 的测量、布局、(绘制)。注意: requestLayout会调用onMeasure和onLayout,不一定调用onDraw!
    • 关于View自定义控件源码分析,可以参考我的其他博客:https://github.com/yangchong211/YCBlogs
    public void addItemDecoration(ItemDecoration decor) {
        addItemDecoration(decor, -1);
    }
    
    //主要看这个方法,我的GitHub:https://github.com/yangchong211/YCBlogs
    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) {
            setWillNotDraw(false);
        }
        if (index < 0) {
            mItemDecorations.add(decor);
        } else {
            // 指定添加分割线在集合中的索引
            mItemDecorations.add(index, decor);
        }
        markItemDecorInsetsDirty();
        // 重新请求 View 的测量、布局、绘制
        requestLayout();
    }
    
  • b.接着看下markItemDecorInsetsDirty这个方法做了些什么
    • 这个方法先获取所有子View的数量,然后遍历了 RecyclerView 和 LayoutManager 的所有子 View,再将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true,最后调用了 mRecycler.markItemDecorInsetsDirty()方法处理复用逻辑。
    void markItemDecorInsetsDirty() {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        //先遍历了 RecyclerView 和 LayoutManager 的所有子 View
        for (int i = 0; i < childCount; i++) {
            final View child = mChildHelper.getUnfilteredChildAt(i);
            //将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
        //调用了 mRecycler.markItemDecorInsetsDirty(),
        //Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑
        mRecycler.markItemDecorInsetsDirty();
    }
    
  • c.接着看下markItemDecorInsetsDirty()这个方法
    • 该方法就是获取RecyclerView 缓存的集合,然后遍历集合得到RecyclerView 的缓存单位是 ViewHolder,获取缓存对象,在获取到layoutParams,并且将其 mInsetsDirty 字段一样置为 true
    void markItemDecorInsetsDirty() {
        //就是 RecyclerView 缓存的集合
        final int cachedCount = mCachedViews.size();
        for (int i = 0; i < cachedCount; i++) {
            //RecyclerView 的缓存单位是 ViewHolder,获取缓存对象
            final ViewHolder holder = mCachedViews.get(i);
            //获得 LayoutParams
            LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
            if (layoutParams != null) {
                //将其 mInsetsDirty 字段一样置为 true
                layoutParams.mInsetsDirty = true;
            }
        }
    }
    
  • d.回过头在看看addItemDecoration中requestLayout方法
    • requestLayout 方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局
    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
    
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
    
  • e.在 RecyclerView 中搜索 mItemDecorations 集合
    • 在onDraw中
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
    
    • 在draw方法中
    @Override
    public void draw(Canvas c) {
        super.draw(c);
    
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
         //省略部分代码
    }
    
    • 总结概括
      • 可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。
      • 这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。
      • 所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

6.ItemAnimator

6.1 作用

  • ItemAnimator能够帮助Item实现独立的动画

6.2 触发的三种事件

  • 某条数据被插入到数据集合中
  • 从数据集合中移除某条数据
  • 更改数据集合中的某条数据

7.其他知识点

7.1 Recycler && RecycledViewPool

  • RecycledViewPool
    • RecyclerViewPool用于多个RecyclerView之间共享View。只需要创建一个RecyclerViewPool实例,然后调用RecyclerView的setRecycledViewPool(RecycledViewPool)方法即可。RecyclerView默认会创建一个RecyclerViewPool实例。
    • 下列源码,是我借助于有道词典翻译部分注释内容……
    • 看出mScrap是一个<viewType, List>的映射,mMaxScrap是一个<viewType, maxNum>的映射,这两个成员变量代表可复用View池的基本信息。调用setMaxRecycledViews(int viewType, int max)时,当用于复用的mScrap中viewType对应的ViewHolder个数超过maxNum时,会从列表末尾开始丢弃超过的部分。调用getRecycledView(int viewType)方法时从mScrap中移除并返回viewType对应的List的末尾项
    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        private int mAttachCount = 0;
    
        //丢弃所有视图
        public void clear() {
            for (int i = 0; i < mScrap.size(); i++) {
                ScrapData data = mScrap.valueAt(i);
                data.mScrapHeap.clear();
            }
        }
    
        //设置丢弃前要在池中持有的视图持有人的最大数量
        public void setMaxRecycledViews(int viewType, int max) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mMaxScrap = max;
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            while (scrapHeap.size() > max) {
                scrapHeap.remove(scrapHeap.size() - 1);
            }
        }
    
        //返回给定视图类型的RecycledViewPool所持有的当前视图数
        public int getRecycledViewCount(int viewType) {
            return getScrapDataForType(viewType).mScrapHeap.size();
        }
    
        //从池中获取指定类型的ViewHolder,如果没有指定类型的ViewHolder,则获取{@Codenull}
        @Nullable
        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }
    
        //池持有的视图持有者总数
        int size() {
            int count = 0;
            for (int i = 0; i < mScrap.size(); i++) {
                ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
                if (viewHolders != null) {
                    count += viewHolders.size();
                }
            }
            return count;
        }
    
        //向池中添加一个废视图保存器。
        //如果那个ViewHolder类型的池已经满了,它将立即被丢弃。
        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }
    
        long runningAverage(long oldAverage, long newValue) {
            if (oldAverage == 0) {
                return newValue;
            }
            return (oldAverage / 4 * 3) + (newValue / 4);
        }
    
        void factorInCreateTime(int viewType, long createTimeNs) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mCreateRunningAverageNs = runningAverage(
                    scrapData.mCreateRunningAverageNs, createTimeNs);
        }
    
        void factorInBindTime(int viewType, long bindTimeNs) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mBindRunningAverageNs = runningAverage(
                    scrapData.mBindRunningAverageNs, bindTimeNs);
        }
    
        boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
            long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
        }
    
        boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
            long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
        }
    
        void attach(Adapter adapter) {
            mAttachCount++;
        }
    
        void detach() {
            mAttachCount--;
        }
    
    
        //分离旧适配器并附加新适配器。如果它只附加了一个适配器,并且新适配器使用与oldAdapter不同的ViewHolder,
        //则RecycledViewPool将清除其缓存。
        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,boolean compatibleWithPrevious) {
            if (oldAdapter != null) {
                detach();
            }
            if (!compatibleWithPrevious && mAttachCount == 0) {
                clear();
            }
            if (newAdapter != null) {
                attach(newAdapter);
            }
        }
    
        private ScrapData getScrapDataForType(int viewType) {
            ScrapData scrapData = mScrap.get(viewType);
            if (scrapData == null) {
                scrapData = new ScrapData();
                mScrap.put(viewType, scrapData);
            }
            return scrapData;
        }
    }
    
  • ViewCacheExtension
    • ViewCacheExtension是一个由开发者控制的可以作为View缓存的帮助类。调用Recycler.getViewForPosition(int)方法获取View时,Recycler先检查attachedscrap和一级缓存,如果没有则检查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),如果没有则检查RecyclerViewPool。注意:Recycler不会在这个类中做缓存View的操作,是否缓存View完全由开发者控制。
    public abstract static class ViewCacheExtension {
        abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
    }
    
  • Recycler
    • 后续再深入分析

7.2 Recyclerview.getLayoutPosition()问题

  • 在RecycleView中的相关方法中,有两种类型的位置
    • 布局位置:从LayoutManager的角度看,条目在最新布局计算中的位置。
      • 返回布局位置的方法使用最近一次布局运算后的位置,如getLayoutPosition()和findViewHolderForLayoutPosition(int)。这些位置包含了最近一次布局运算后的变化。你可以根据这些位置来与用户正在屏幕上看到的保持一致。比如,你有一个条目列表,当用户请求第5个条目时,你可以使用这些方法来匹配用户看到的。
    • 适配器位置:从适配器的角度看,条目在是适配器中的位置。
      • 另外一系列方法与AdapterPosition关联,比如getAdapterPosition()和findViewHolderForAdapterPosition(int)。当你想获得条目在更新后的适配器中的位置使用这些方法,即使这些位置变化还没反映到布局中。比如,你想访问适配器中条目的位置时,就应该使用getAdapterPosition()。注意,notifyDataSetChanged()已经被调用而且还没计算新布局,这些方法或许不能够计算适配器位置。所以,你要小心处理这些方法返回NO_POSITION和null的情况。
    • 注意: 这两种类型的位置是等同的,除非在分发adapter.notify*事件和更新布局时。
  • 关于两者的区别
    • 网上查了一些资料,发现相关内容很少,最后在stackoverflow上终于看到有大神这样解释两者的区别
    • 具体区别就是adapter和layout的位置会有时间差(<16ms), 如果你改变了Adapter的数据然后刷新视图, layout需要过一段时间才会更新视图, 在这段时间里面, 这两个方法返回的position会不一样。
      • 在notifyDataSetChanged之后并不能马上获取Adapter中的position, 要等布局结束之后才能获取到
      • 在notifyItemInserted之后,Layout不能马上获取到新的position,因为布局还没更新(需要<16ms的时间刷新视图), 所以只能获取到旧的,但是Adapter中的position就可以马上获取到最新的position。
    public final int getAdapterPosition() {
        if (mOwnerRecyclerView == null) {
            return NO_POSITION;
        }
        return mOwnerRecyclerView.getAdapterPositionFor(this);
    }
    
    public final int getLayoutPosition() {
        return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
    }
    
  • 可能会导致的错误
    • 这种情况有点难以复现,在 ViewHolder 中处理 item 的点击事件的时候,发现多个 item 同时点击就会出现闪退,debug 看到 position = -1
    • 解决办法:使用 ViewHolder#getLayoutPosition() 获取 position,而不要通过 ViewHolder#getAdapterPosition() 来获取 position 的

8.RecyclerView嵌套方案滑动冲突解决方案

8.1 如何判断RecyclerView控件滑动到顶部和底部

  • 有一种使用场景,购物商城的购物车页面,当RecyclerView滑动到顶部时,让刷新控件消费事件;当RecyclerView滑动到底部时,让下一页控件[猜你喜欢]消费事件。
    • 代码如下所示:
    public class VerticalRecyclerView extends RecyclerView {
    
        private float downX;
        private float downY;
        /** 第一个可见的item的位置 */
        private int firstVisibleItemPosition;
        /** 第一个的位置 */
        private int[] firstPositions;
        /** 最后一个可见的item的位置 */
        private int lastVisibleItemPosition;
        /** 最后一个的位置 */
        private int[] lastPositions;
        private boolean isTop;
        private boolean isBottom;
    
        public VerticalRecyclerView(Context context) {
            this(context, null);
        }
    
        public VerticalRecyclerView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public VerticalRecyclerView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager != null) {
                if (layoutManager instanceof GridLayoutManager) {
                    lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
                    firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
                } else if (layoutManager instanceof LinearLayoutManager) {
                    lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
                    firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                    StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                    if (lastPositions == null) {
                        lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                        firstPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                    }
                    staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
                    staggeredGridLayoutManager.findFirstVisibleItemPositions(firstPositions);
                    lastVisibleItemPosition = findMax(lastPositions);
                    firstVisibleItemPosition = findMin(firstPositions);
                }
            } else {
                throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
            }
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = ev.getX();
                    downY = ev.getY();
                    //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = ev.getX() - downX;
                    float dy = ev.getY() - downY;
                    boolean allowParentTouchEvent;
                    if (Math.abs(dy) > Math.abs(dx)) {
                        if (dy > 0) {
                            //位于顶部时下拉,让父View消费事件
                            allowParentTouchEvent = isTop = firstVisibleItemPosition == 0 && getChildAt(0).getTop() >= 0;
                        } else {
                            //位于底部时上拉,让父View消费事件
                            int visibleItemCount = layoutManager.getChildCount();
                            int totalItemCount = layoutManager.getItemCount();
                            allowParentTouchEvent = isBottom = visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight();
                        }
                    } else {
                        //水平方向滑动
                        allowParentTouchEvent = true;
                    }
                    getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent);
            }
            return super.dispatchTouchEvent(ev);
    
        }
    
        private int findMax(int[] lastPositions) {
            int max = lastPositions[0];
            for (int value : lastPositions) {
                if (value >= max) {
                    max = value;
                }
            }
            return max;
        }
    
        private int findMin(int[] firstPositions) {
            int min = firstPositions[0];
            for (int value : firstPositions) {
                if (value < min) {
                    min = value;
                }
            }
            return min;
        }
    
        public boolean isTop() {
            return isTop;
        }
    
        public boolean isBottom() {
            return isBottom;
        }
    }
    

8.2 RecyclerView嵌套RecyclerView条目自动上滚的Bug

  • RecyclerViewA嵌套RecyclerViewB 进入页面自动跳转到RecyclerViewB上面页面会自动滚动。
    • 两种解决办法
    • 一,recyclerview去除焦点
      • recyclerview.setFocusableInTouchMode(false);
      • recyclerview.requestFocus();
    • 二,在代码里面 让处于ScrollView或者RecyclerView1 顶端的某个控件获得焦点即可
      • 比如顶部的一个textview
      • tv.setFocusableInTouchMode(true);
      • tv.requestFocus();

8.3 ScrollView嵌套RecyclerView滑动冲突

  • 第一种方式:
    • 重写父控件,让父控件 ScrollView 直接拦截滑动事件,不向下分发给 RecyclerView,具体是定义一个ScrollView子类,重写其 onInterceptTouchEvent()方法
    public class NoNestedScrollview extends NestedScrollView {
    
        private int downX;
        private int downY;
        private int mTouchSlop;
        
        public NoNestedScrollview(Context context) {
            super(context);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        public NoNestedScrollview(Context context, AttributeSet attrs) {
            super(context, attrs);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        public NoNestedScrollview(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent e) {
            int action = e.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    downX = (int) e.getRawX();
                    downY = (int) e.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //判断是否滑动,若滑动就拦截事件
                    int moveY = (int) e.getRawY();
                    if (Math.abs(moveY - downY) > mTouchSlop) {
                        return true;
                    }
                    break;
                default:
                    break;
            }
            return super.onInterceptTouchEvent(e);
        }
    }
    
  • 第二种解决方式
    • a.禁止RecyclerView滑动
    recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){
        @Override
        public boolean canScrollVertically() {
            return false;
        }
        
        @Override
        public boolean canScrollHorizontally() {
            return super.canScrollHorizontally();
        }
    });
    
    recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){
        @Override
        public boolean canScrollVertically() {
            return false;
        }
    });
    
    • b.重写LayoutManager
      • 代码设置LayoutManager.setScrollEnabled(false);
    public class ScrollLayoutManager extends LinearLayoutManager {
         
        private boolean isScrollEnable = true;
         
        public ScrollLayoutManager(Context context) {
            super(context);
        }
         
        public ScrollLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }
         
        public ScrollLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
         
        @Override
        public boolean canScrollVertically() {
            return isScrollEnable && super.canScrollVertically();
        }
         
        /**
         * 设置 RecyclerView 是否可以垂直滑动
         * @param isEnable
         */
        public void setScrollEnable(boolean isEnable) {
            this.isScrollEnable = isEnable;
        }
    }
    
  • 可能会出现的问题
    • 虽然上面两种方式解决了滑动冲突,但是有的手机上出现了RecyclerView会出现显示不全的情况。
    • 针对这种情形,使用网上的方法一种是使用 RelativeLayout 包裹 RecyclerView 并设置属性:android:descendantFocusability="blocksDescendants"
      • android:descendantFocusability="blocksDescendants",该属>性是当一个view 获取焦点时,定义 ViewGroup 和其子控件直接的关系,常用来>解决父控件的焦点或者点击事件被子空间获取。
      • beforeDescendants: ViewGroup会优先其子控件获取焦点
      • afterDescendants: ViewGroup只有当其子控件不需要获取焦点时才获取焦点
      • blocksDescendants: ViewGroup会覆盖子类控件而直接获得焦点
    • 相关代码案例:https://github.com/yangchong211/LifeHelper
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:descendantFocusability="blocksDescendants">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_hot_review"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:foregroundGravity="center" />
    </RelativeLayout>
    

8.4 viewPager嵌套水平RecyclerView横向滑动到底后不滑动ViewPager

  • 继承RecyclerView,重写dispatchTouchEvent,根据ACTION_MOVE的方向判断是否调用getParent().requestDisallowInterceptTouchEvent去阻止父view拦截点击事件
    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        /*---解决垂ViewPager嵌套直RecyclerView嵌套水平RecyclerView横向滑动到底后不滑动ViewPager start ---*/ 
        ViewParent parent = this; 
        while(!((parent = parent.getParent()) instanceof ViewPager));
        // 循环查找viewPager 
        parent.requestDisallowInterceptTouchEvent(true); 
        return super.dispatchTouchEvent(ev); 
    }
    

9.RecyclerView复杂布局封装库案例

9.1 能够实现业务的需求和功能

  • 1.1 支持上拉加载,下拉刷新,可以自定义foot底部布局,支持添加多个自定义header头部布局。
  • 1.2 支持切换不同的状态,比如加载中[目前是ProgressBar,加载成功,加载失败,加载错误等不同布局状态。当然也可以自定义这些状态的布局
  • 1.3 支持复杂界面使用,比如有的页面包含有轮播图,按钮组合,横向滑动,还有复杂list,那么用这个控件就可以搞定。
  • 1.4 已经用于实际开发项目投资界,新芽,沙丘大学中……
  • 1.5 轻量级侧滑删除菜单,直接嵌套item布局即可使用,使用十分简单。
  • 1.6 支持插入或者删除某条数据,支持CoordinatorLayout炫酷的效果
  • 1.7 支持粘贴头部的需求效果
  • 1.8 RecyclerView实现条目Item拖拽排序与滑动删除

9.2 具备的优势分析

  • 自定义支持上拉加载更多,下拉刷新,支持自由切换状态【加载中,加载成功,加载失败,没网络等状态】的控件,拓展功能[支持长按拖拽,侧滑删除]可以选择性添加 。具体使用方法,可以直接参考demo。
  • 轻量级侧滑删除菜单,支持recyclerView,listView,直接嵌套item布局即可使用,整个侧滑菜单思路是:跟随手势将item向左滑动

10.针对阿里VLayout代码分析

11.版本更新说明

  • v1.0.0 2016年5月5日
  • v1.1.0 更新于2017年2月1日
  • v1.1.1 更新于2017年6月9日
  • v2.0.0 更新于2018年9月26日

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 明天就要去代账公司实习了,想想有点小激动。意味着每天六点半起床的生活就要来临,对于每天睡到自然醒的我确实是个挑战。...
    布尔乔亚yyy阅读 310评论 1 0
  • 一个人生活,照顾好自己,习惯那种如影随形的孤独,好像总归是一件令人钦佩的事。一个人住,一个人吃饭,一个人旅行,当我...
    脱缰_阅读 574评论 0 3
  • Good morning! 在苹苹老师的指导下,小朋友们都勇敢的做自我介绍,表现多棒呀。 很好的自我介绍,能帮助小...
    红黄蓝图图班阅读 296评论 0 0
  • 我们面对面,你转过身,向背对着我的方向,走远了! 我还能追得上么? 恋爱不是两个人的事,影响的因素太多了。为什么我...
    安枫阅读 230评论 0 0