讓你明明白白的使用RecyclerView——SnapHelper詳解

簡(jiǎn)介

RecyclerView在24.2.0版本中新增了SnapHelper這個(gè)輔助類,用于輔助RecyclerView在滾動(dòng)結(jié)束時(shí)將Item對(duì)齊到某個(gè)位置。特別是列表橫向滑動(dòng)時(shí),很多時(shí)候不會(huì)讓列表滑到任意位置,而是會(huì)有一定的規(guī)則限制,這時(shí)候就可以通過SnapHelper來定義對(duì)齊規(guī)則了。

SnapHelper是一個(gè)抽象類,官方提供了一個(gè)LinearSnapHelper的子類,可以讓RecyclerView滾動(dòng)停止時(shí)相應(yīng)的Item停留中間位置。25.1.0版本中官方又提供了一個(gè)PagerSnapHelper的子類,可以使RecyclerView像ViewPager一樣的效果,一次只能滑一頁,而且居中顯示。

這兩個(gè)子類使用方式也很簡(jiǎn)單,只需要?jiǎng)?chuàng)建對(duì)象之后調(diào)用attachToRecyclerView()附著到對(duì)應(yīng)的RecyclerView對(duì)象上就可以了。

new LinearSnapHelper().attachToRecyclerView(mRecyclerView);
//或者
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);

原理剖析

Fling操作

首先來了解一個(gè)概念,手指在屏幕上滑動(dòng)RecyclerView然后松手,RecyclerView中的內(nèi)容會(huì)順著慣性繼續(xù)往手指滑動(dòng)的方向繼續(xù)滾動(dòng)直到停止,這個(gè)過程叫做Fling。Fling操作從手指離開屏幕瞬間被觸發(fā),在滾動(dòng)停止時(shí)結(jié)束。

三個(gè)抽象方法

SnapHelper是一個(gè)抽象類,它有三個(gè)抽象方法:

  • public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)
    

    該方法會(huì)根據(jù)觸發(fā)Fling操作的速率(參數(shù)velocityX和參數(shù)velocityY)來找到RecyclerView需要滾動(dòng)到哪個(gè)位置,該位置對(duì)應(yīng)的ItemView就是那個(gè)需要進(jìn)行對(duì)齊的列表項(xiàng)。我們把這個(gè)位置稱為targetSnapPosition,對(duì)應(yīng)的View稱為targetSnapView。如果找不到targetSnapPosition,就返回RecyclerView.NO_POSITION。

  • public abstract View findSnapView(LayoutManager layoutManager)
    

    該方法會(huì)找到當(dāng)前l(fā)ayoutManager上最接近對(duì)齊位置的那個(gè)view,該view稱為SanpView,對(duì)應(yīng)的position稱為SnapPosition。如果返回null,就表示沒有需要對(duì)齊的View,也就不會(huì)做滾動(dòng)對(duì)齊調(diào)整。

  • public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
    

    這個(gè)方法會(huì)計(jì)算第二個(gè)參數(shù)對(duì)應(yīng)的ItemView當(dāng)前的坐標(biāo)與需要對(duì)齊的坐標(biāo)之間的距離。該方法返回一個(gè)大小為2的int數(shù)組,分別對(duì)應(yīng)x軸和y軸方向上的距離。

SnapView.png

attachToRecyclerView()

現(xiàn)在來看attachToRecyclerView()這個(gè)方法,SnapHelper正是通過該方法附著到RecyclerView上,從而實(shí)現(xiàn)輔助RecyclerView滾動(dòng)對(duì)齊操作。源碼如下:

   public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
      //如果SnapHelper之前已經(jīng)附著到此RecyclerView上,不用進(jìn)行任何操作
        if (mRecyclerView == recyclerView) {
            return;
        }
      //如果SnapHelper之前附著的RecyclerView和現(xiàn)在的不一致,清理掉之前RecyclerView的回調(diào)
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
      //更新RecyclerView對(duì)象引用
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
          //設(shè)置當(dāng)前RecyclerView對(duì)象的回調(diào)
            setupCallbacks();
          //創(chuàng)建一個(gè)Scroller對(duì)象,用于輔助計(jì)算fling的總距離,后面會(huì)涉及到
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
          //調(diào)用snapToTargetExistingView()方法以實(shí)現(xiàn)對(duì)SnapView的對(duì)齊滾動(dòng)處理
            snapToTargetExistingView();
        }
    }

可以看到,在attachToRecyclerView()方法中會(huì)清掉SnapHelper之前保存的RecyclerView對(duì)象的回調(diào)(如果有的話),對(duì)新設(shè)置進(jìn)來的RecyclerView對(duì)象設(shè)置回調(diào),然后初始化一個(gè)Scroller對(duì)象,最后調(diào)用snapToTargetExistingView()方法對(duì)SnapView進(jìn)行對(duì)齊調(diào)整。

snapToTargetExistingView()

該方法的作用是對(duì)SnapView進(jìn)行滾動(dòng)調(diào)整,以使得SnapView達(dá)到對(duì)齊效果。源碼如下:

    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
      //找出SnapView
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
      //計(jì)算出SnapView需要滾動(dòng)的距離
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
      //如果需要滾動(dòng)的距離不是為0,就調(diào)用smoothScrollBy()使RecyclerView滾動(dòng)相應(yīng)的距離
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

可以看到,snapToTargetExistingView()方法就是先找到SnapView,然后計(jì)算SnapView當(dāng)前坐標(biāo)到目的坐標(biāo)之間的距離,然后調(diào)用RecyclerView.smoothScrollBy()方法實(shí)現(xiàn)對(duì)RecyclerView內(nèi)容的平滑滾動(dòng),從而將SnapView移到目標(biāo)位置,達(dá)到對(duì)齊效果。RecyclerView.smoothScrollBy()這個(gè)方法的實(shí)現(xiàn)原理這里就不展開了 ,它的作用就是根據(jù)參數(shù)平滑滾動(dòng)RecyclerView的中的ItemView相應(yīng)的距離。

setupCallbacks()和destroyCallbacks()

再看下SnapHelper對(duì)RecyclerView設(shè)置了哪些回調(diào):

    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }

可以看出RecyclerView設(shè)置的回調(diào)有兩個(gè):一個(gè)是OnScrollListener對(duì)象mScrollListener.還有一個(gè)是OnFlingListener對(duì)象。由于SnapHelper實(shí)現(xiàn)了OnFlingListener接口,所以這個(gè)對(duì)象就是SnapHelper自身了.

先看下mScrollListener這個(gè)變量在怎樣實(shí)現(xiàn)的.

    private final RecyclerView.OnScrollListener mScrollListener =
            new RecyclerView.OnScrollListener() {
                boolean mScrolled = false;
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                  //mScrolled為true表示之前進(jìn)行過滾動(dòng).
                  //newState為SCROLL_STATE_IDLE狀態(tài)表示滾動(dòng)結(jié)束停下來
                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                        mScrolled = false;
                        snapToTargetExistingView();
                    }
                }

                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (dx != 0 || dy != 0) {
                        mScrolled = true;
                    }
                }
            };

該滾動(dòng)監(jiān)聽器的實(shí)現(xiàn)很簡(jiǎn)單,只是在正常滾動(dòng)停止的時(shí)候調(diào)用了snapToTargetExistingView()方法對(duì)targetView進(jìn)行滾動(dòng)調(diào)整,以確保停止的位置是在對(duì)應(yīng)的坐標(biāo)上,這就是RecyclerView添加該OnScrollListener的目的。

除了OnScrollListener這個(gè)監(jiān)聽器,還對(duì)RecyclerView還設(shè)置了OnFlingListener這個(gè)監(jiān)聽器,而這個(gè)監(jiān)聽器就是SnapHelper自身。因?yàn)镾napHelper實(shí)現(xiàn)了RecyclerView.OnFlingListener接口。我們先來看看RecyclerView.OnFlingListener這個(gè)接口。

    public static abstract class OnFlingListener {
            /**
         * Override this to handle a fling given the velocities in both x and y directions.
         * Note that this method will only be called if the associated {@link LayoutManager}
         * supports scrolling and the fling is not handled by nested scrolls first.
         *
         * @param velocityX the fling velocity on the X axis
         * @param velocityY the fling velocity on the Y axis
         *
         * @return true if the fling washandled, false otherwise.
         */
        public abstract boolean onFling(int velocityX, int velocityY);
    }

這個(gè)接口中就只有一個(gè)onFling()方法,該方法會(huì)在RecyclerView開始做fling操作時(shí)被調(diào)用。我們來看看SnapHelper怎么實(shí)現(xiàn)onFling()方法:

    @Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
      //獲取RecyclerView要進(jìn)行fling操作需要的最小速率,
      //只有超過該速率,ItemView才會(huì)有足夠的動(dòng)力在手指離開屏幕時(shí)繼續(xù)滾動(dòng)下去
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
      //這里會(huì)調(diào)用snapFromFling()這個(gè)方法,就是通過該方法實(shí)現(xiàn)平滑滾動(dòng)并使得在滾動(dòng)停止時(shí)itemView對(duì)齊到目的坐標(biāo)位置
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

注釋解釋得很清楚。看下snapFromFling()怎么操作的:

    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
      //layoutManager必須實(shí)現(xiàn)ScrollVectorProvider接口才能繼續(xù)往下操作
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
        
      //創(chuàng)建SmoothScroller對(duì)象,這個(gè)東西是一個(gè)平滑滾動(dòng)器,用于對(duì)ItemView進(jìn)行平滑滾動(dòng)操作
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        
      //通過findTargetSnapPosition()方法,以layoutManager和速率作為參數(shù),找到targetSnapPosition
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        //通過setTargetPosition()方法設(shè)置滾動(dòng)器的滾動(dòng)目標(biāo)位置
        smoothScroller.setTargetPosition(targetPosition);
        //利用layoutManager啟動(dòng)平滑滾動(dòng)器,開始滾動(dòng)到目標(biāo)位置
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

可以看到,snapFromFling()方法會(huì)先判斷l(xiāng)ayoutManager是否實(shí)現(xiàn)了ScrollVectorProvider接口,如果沒有實(shí)現(xiàn)該接口就不允許通過該方法做滾動(dòng)操作。那為啥一定要實(shí)現(xiàn)該接口呢?待會(huì)再來解釋。接下來就去創(chuàng)建平滑滾動(dòng)器SmoothScroller的一個(gè)實(shí)例,layoutManager可以通過該平滑滾動(dòng)器來進(jìn)行滾動(dòng)操作。SmoothScroller需要設(shè)置一個(gè)滾動(dòng)的目標(biāo)位置,我們將通過findTargetSnapPosition()方法來計(jì)算得到的targetSnapPosition給它,告訴滾動(dòng)器要滾到這個(gè)位置,然后就啟動(dòng)SmoothScroller進(jìn)行滾動(dòng)操作。

但是這里有一點(diǎn)需要注意一下,默認(rèn)情況下通過setTargetPosition()方法設(shè)置的SmoothScroller只能將對(duì)應(yīng)位置的ItemView滾動(dòng)到與RecyclerView的邊界對(duì)齊,那怎么實(shí)現(xiàn)將該ItemView滾動(dòng)到我們需要對(duì)齊的目標(biāo)位置呢?就得對(duì)SmoothScroller進(jìn)行一下處理了。

看下平滑滾動(dòng)器RecyclerView.SmoothScroller,這個(gè)東西是通過createSnapScroller()方法創(chuàng)建得到的:

    @Nullable
    protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
      //同樣,這里也是先判斷l(xiāng)ayoutManager是否實(shí)現(xiàn)了ScrollVectorProvider這個(gè)接口,
      //沒有實(shí)現(xiàn)該接口就不創(chuàng)建SmoothScroller
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return null;
        }
      //這里創(chuàng)建一個(gè)LinearSmoothScroller對(duì)象,然后返回給調(diào)用函數(shù),
      //也就是說,最終創(chuàng)建出來的平滑滾動(dòng)器就是這個(gè)LinearSmoothScroller
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
          //該方法會(huì)在targetSnapView被layout出來的時(shí)候調(diào)用。
          //這個(gè)方法有三個(gè)參數(shù):
          //第一個(gè)參數(shù)targetView,就是本文所講的targetSnapView
          //第二個(gè)參數(shù)RecyclerView.State這里沒用到,先不管它
          //第三個(gè)參數(shù)Action,這個(gè)是什么東西呢?它是SmoothScroller的一個(gè)靜態(tài)內(nèi)部類,
          //保存著SmoothScroller在平滑滾動(dòng)過程中一些信息,比如滾動(dòng)時(shí)間,滾動(dòng)距離,差值器等
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
             //calculateDistanceToFinalSnap()方法上面解釋過,
             //得到targetSnapView當(dāng)前坐標(biāo)到目的坐標(biāo)之間的距離
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                        targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
              //通過calculateTimeForDeceleration()方法得到做減速滾動(dòng)所需的時(shí)間
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                  //調(diào)用Action的update()方法,更新SmoothScroller的滾動(dòng)速率,使其減速滾動(dòng)到停止
                  //這里的這樣做的效果是,此SmoothScroller用time這么長(zhǎng)的時(shí)間以mDecelerateInterpolator這個(gè)差值器的滾動(dòng)變化率滾動(dòng)dx或者dy這么長(zhǎng)的距離
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

          //該方法是計(jì)算滾動(dòng)速率的,返回值代表滾動(dòng)速率,該值會(huì)影響剛剛上面提到的
          //calculateTimeForDeceleration()的方法的返回返回值,
          //MILLISECONDS_PER_INCH的值是100,也就是說該方法的返回值代表著每dpi的距離要滾動(dòng)100毫秒
            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

通過以上的分析可以看到,createSnapScroller()創(chuàng)建的是一個(gè)LinearSmoothScroller,并且在創(chuàng)建該LinearSmoothScroller的時(shí)候主要考慮兩個(gè)方面:

  • 第一個(gè)是滾動(dòng)速率,由calculateSpeedPerPixel()方法決定;
  • 第二個(gè)是在滾動(dòng)過程中,targetView即將要進(jìn)入到視野時(shí),將勻速滾動(dòng)變換為減速滾動(dòng),然后一直滾動(dòng)目的坐標(biāo)位置,使?jié)L動(dòng)效果更真實(shí),這是由onTargetFound()方法決定。

剛剛不是留了一個(gè)疑問么?就是正常模式下SmoothScroller通過setTargetPosition()方法設(shè)置的ItemView只能滾動(dòng)到與RecyclerView邊緣對(duì)齊,而解決這個(gè)局限的處理方式就是在SmoothScroller的onTargetFound()方法中了。onTargetFound()方法會(huì)在SmoothScroller滾動(dòng)過程中,targetSnapView被layout出來時(shí)調(diào)用。而這個(gè)時(shí)候利用calculateDistanceToFinalSnap()方法得到targetSnapView當(dāng)前坐標(biāo)與目的坐標(biāo)之間的距離,然后通過Action.update()方法改變當(dāng)前SmoothScroller的狀態(tài),讓SmoothScroller根據(jù)新的滾動(dòng)距離、新的滾動(dòng)時(shí)間、新的滾動(dòng)差值器來滾動(dòng),這樣既能將targetSnapView滾動(dòng)到目的坐標(biāo)位置,又能實(shí)現(xiàn)減速滾動(dòng),使得滾動(dòng)效果更真實(shí)。

onTargetFound.png

從圖中可以看到,很多時(shí)候targetSnapView被layout的時(shí)候(onTargetFound()方法被調(diào)用)并不是緊挨著界面上的Item,而是會(huì)有一定的提前,這是由于RecyclerView為了優(yōu)化性能,提高流暢度,在滑動(dòng)滾動(dòng)的時(shí)候會(huì)有一個(gè)預(yù)加載的過程,提前將Item給layout出來了,這個(gè)知識(shí)點(diǎn)涉及到的內(nèi)容很多,這里做個(gè)理解就可以了,不詳細(xì)細(xì)展開了,以后有時(shí)間會(huì)專門講下RecyclerView的相關(guān)原理機(jī)制。

到了這里,整理一下前面的思路:SnapHelper實(shí)現(xiàn)了OnFlingListener這個(gè)接口,該接口中的onFling()方法會(huì)在RecyclerView觸發(fā)Fling操作時(shí)調(diào)用。在onFling()方法中判斷當(dāng)前方向上的速率是否足夠做滾動(dòng)操作,如果速率足夠大就調(diào)用snapFromFling()方法實(shí)現(xiàn)滾動(dòng)相關(guān)的邏輯。在snapFromFling()方法中會(huì)創(chuàng)建一個(gè)SmoothScroller,并且根據(jù)速率計(jì)算出滾動(dòng)停止時(shí)的位置,將該位置設(shè)置給SmoothScroller并啟動(dòng)滾動(dòng)。而滾動(dòng)的操作都是由SmoothScroller全權(quán)負(fù)責(zé),它可以控制Item的滾動(dòng)速度(剛開始是勻速),并且在滾動(dòng)到targetSnapView被layout時(shí)變換滾動(dòng)速度(轉(zhuǎn)換成減速),以讓滾動(dòng)效果更加真實(shí)。

所以,SnapHelper輔助RecyclerView實(shí)現(xiàn)滾動(dòng)對(duì)齊就是通過給RecyclerView設(shè)置OnScrollerListenerh和OnFlingListener這兩個(gè)監(jiān)聽器實(shí)現(xiàn)的

LinearSnapHelper

SnapHelper輔助RecyclerView滾動(dòng)對(duì)齊的框架已經(jīng)搭好了,子類只要根據(jù)對(duì)齊方式實(shí)現(xiàn)那三個(gè)抽象方法就可以了。以LinearSnapHelper為例,看它到底怎么實(shí)現(xiàn)SnapHelper的三個(gè)抽象方法,從而讓ItemView滾動(dòng)居中對(duì)齊:

calculateDistanceToFinalSnap()

    @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
      //水平方向滾動(dòng),則計(jì)算水平方向需要滾動(dòng)的距離,否則水平方向的滾動(dòng)距離為0
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

      //豎直方向滾動(dòng),則計(jì)算豎直方向需要滾動(dòng)的距離,否則水平方向的滾動(dòng)距離為0
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

該方法是返回第二個(gè)傳參對(duì)應(yīng)的view到RecyclerView中間位置的距離,可以支持水平方向滾動(dòng)和豎直方向滾動(dòng)兩個(gè)方向的計(jì)算。最主要的計(jì)算距離的這個(gè)方法distanceToCenter()

    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
      //找到targetView的中心坐標(biāo)
        final int childCenter = helper.getDecoratedStart(targetView) +
                (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
      //找到容器(RecyclerView)的中心坐標(biāo)
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
      //兩個(gè)中心坐標(biāo)的差值就是targetView需要滾動(dòng)的距離
        return childCenter - containerCenter;
    }

可以看到,就是計(jì)算對(duì)應(yīng)的view的中心坐標(biāo)到RecyclerView中心坐標(biāo)之間的距離,該距離就是此view需要滾動(dòng)的距離。

findSnapView()

    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

尋找SnapView,這里的目的坐標(biāo)就是RecyclerView中間位置坐標(biāo),可以看到會(huì)根據(jù)layoutManager的布局方式(水平布局方式或者豎向布局方式)區(qū)分計(jì)算,但最終都是通過findCenterView()方法來找snapView的。

    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
      //找到RecyclerView的中心坐標(biāo)
        final int center;
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

      //遍歷當(dāng)前l(fā)ayoutManager中所有的ItemView
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
          //ItemView的中心坐標(biāo)
            int childCenter = helper.getDecoratedStart(child) +
                    (helper.getDecoratedMeasurement(child) / 2);
          //計(jì)算此ItemView與RecyclerView中心坐標(biāo)的距離
            int absDistance = Math.abs(childCenter - center);

            //對(duì)比每個(gè)ItemView距離到RecyclerView中心點(diǎn)的距離,找到那個(gè)最靠近中心的ItemView然后返回
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

注釋解釋得很清楚,就不重復(fù)了。

findTargetSnapPosition()

@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
      //判斷l(xiāng)ayoutManager是否實(shí)現(xiàn)了RecyclerView.SmoothScroller.ScrollVectorProvider這個(gè)接口
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return RecyclerView.NO_POSITION;
        }

        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }
        
      //找到snapView
        final View currentView = findSnapView(layoutManager);
        if (currentView == null) {
            return RecyclerView.NO_POSITION;
        }

        final int currentPosition = layoutManager.getPosition(currentView);
        if (currentPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }
        
        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        // 通過ScrollVectorProvider接口中的computeScrollVectorForPosition()方法
        // 來確定layoutManager的布局方向
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            return RecyclerView.NO_POSITION;
        }

        int vDeltaJump, hDeltaJump;
        if (layoutManager.canScrollHorizontally()) {
          //layoutManager是橫向布局,并且內(nèi)容超出一屏,canScrollHorizontally()才返回true
          //估算fling結(jié)束時(shí)相對(duì)于當(dāng)前snapView位置的橫向位置偏移量
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
          //vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
          //不能橫向滾動(dòng),橫向位置偏移量當(dāng)然就為0
            hDeltaJump = 0;
        }
        
      //豎向的原理同上
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }
        
      //根據(jù)layoutManager的橫豎向布局方式,最終橫向位置偏移量和豎向位置偏移量二選一,作為fling的位置偏移量
        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
        if (deltaJump == 0) {
            return RecyclerView.NO_POSITION;
        }
        //當(dāng)前位置加上偏移位置,就得到fling結(jié)束時(shí)的位置,這個(gè)位置就是targetPosition
        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {
            targetPos = 0;
        }
        if (targetPos >= itemCount) {
            targetPos = itemCount - 1;
        }
        return targetPos;
    }

RecyclerView的layoutManager很靈活,有兩種布局方式(橫向布局和縱向布局),每種布局方式有兩種布局方向(正向布局和反向布局)。這個(gè)方法在計(jì)算targetPosition的時(shí)候把布局方式和布局方向都考慮進(jìn)去了。布局方式可以通過layoutManager.canScrollHorizontally()/layoutManager.canScrollVertically()來判斷,布局方向就通過RecyclerView.SmoothScroller.ScrollVectorProvider這個(gè)接口中的computeScrollVectorForPosition()方法來判斷。

所以SnapHelper為了適配layoutManager的各種情況,特意要求只有實(shí)現(xiàn)了RecyclerView.SmoothScroller.ScrollVectorProvider接口的layoutManager才能使用SnapHelper進(jìn)行輔助滾動(dòng)對(duì)齊。官方提供的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager都實(shí)現(xiàn)了這個(gè)接口,所以都支持SnapHelper。

這幾個(gè)方法在計(jì)算位置的時(shí)候用的是OrientationHelper這個(gè)工具類,它是LayoutManager用于測(cè)量child的一個(gè)輔助類,可以根據(jù)Layoutmanager的布局方式和布局方向來計(jì)算得到ItemView的大小位置等信息。

從源碼中可以看到findTargetSnapPosition()會(huì)先找到fling操作被觸發(fā)時(shí)界面上的snapView(因?yàn)?code>findTargetSnapPosition()方法是在onFling()方法中被調(diào)用的),得到對(duì)應(yīng)的snapPosition,然后通過estimateNextPositionDiffForFling()方法估算位置偏移量,snapPosition加上位置偏移量就得到最終滾動(dòng)結(jié)束時(shí)的位置,也就是targetSnapPosition。

這里有一個(gè)點(diǎn)需要注意一下,就是在找targetSnapPosition之前是需要先找一個(gè)參考位置的,該參考位置就是snapPosition了。這是因?yàn)楫?dāng)前界面上不同的ItemView位置相差比較大,用snapPosition作參考位置,會(huì)使得參考位置加上位置偏移量得到的targetSnapPosition最接近目的坐標(biāo)位置,從而讓后續(xù)的坐標(biāo)對(duì)齊調(diào)整更加自然。

看下estimateNextPositionDiffForFling()方法怎么估算位置偏移量的:

    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper, int velocityX, int velocityY) {
      //計(jì)算滾動(dòng)的總距離,這個(gè)距離受到觸發(fā)fling時(shí)的速度的影響
        int[] distances = calculateScrollDistance(velocityX, velocityY);
      //計(jì)算每個(gè)ItemView的長(zhǎng)度
        float distancePerChild = computeDistancePerChild(layoutManager, helper);
        if (distancePerChild <= 0) {
            return 0;
        }
      //這里其實(shí)就是根據(jù)是橫向布局還是縱向布局,來取對(duì)應(yīng)布局方向上的滾動(dòng)距離
        int distance =
                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
      //distance的正負(fù)值符號(hào)表示滾動(dòng)方向,數(shù)值表示滾動(dòng)距離。橫向布局方式,內(nèi)容從右往左滾動(dòng)為正;豎向布局方式,內(nèi)容從下往上滾動(dòng)為正
      // 滾動(dòng)距離/item的長(zhǎng)度=滾動(dòng)item的個(gè)數(shù),這里取計(jì)算結(jié)果的整數(shù)部分
      if (distance > 0) {
            return (int) Math.floor(distance / distancePerChild);
        } else {
            return (int) Math.ceil(distance / distancePerChild);
        }
    }

可以看到就是用滾動(dòng)總距離除以itemview的長(zhǎng)度,從而估算得到需要滾動(dòng)的item數(shù)量,此數(shù)值就是位置偏移量。而滾動(dòng)距離是通過SnapHelper的calculateScrollDistance()方法得到的,ItemView的長(zhǎng)度是通過computeDistancePerChild()方法計(jì)算出來。

看下這兩個(gè)方法:

private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
                                          OrientationHelper helper) {
        View minPosView = null;
        View maxPosView = null;
        int minPos = Integer.MAX_VALUE;
        int maxPos = Integer.MIN_VALUE;
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return INVALID_DISTANCE;
        }

        //循環(huán)遍歷layoutManager的itemView,得到最小position和最大position,以及對(duì)應(yīng)的view
        for (int i = 0; i < childCount; i++) {
            View child = layoutManager.getChildAt(i);
            final int pos = layoutManager.getPosition(child);
            if (pos == RecyclerView.NO_POSITION) {
                continue;
            }
            if (pos < minPos) {
                minPos = pos;
                minPosView = child;
            }
            if (pos > maxPos) {
                maxPos = pos;
                maxPosView = child;
            }
        }
        if (minPosView == null || maxPosView == null) {
            return INVALID_DISTANCE;
        }
        //最小位置和最大位置肯定就是分布在layoutManager的兩端,但是無法直接確定哪個(gè)在起點(diǎn)哪個(gè)在終點(diǎn)(因?yàn)橛姓聪虿季郑?        //所以取兩者中起點(diǎn)坐標(biāo)小的那個(gè)作為起點(diǎn)坐標(biāo)
        //終點(diǎn)坐標(biāo)的取值一樣的道理
        int start = Math.min(helper.getDecoratedStart(minPosView),
                helper.getDecoratedStart(maxPosView));
        int end = Math.max(helper.getDecoratedEnd(minPosView),
                helper.getDecoratedEnd(maxPosView));
        //終點(diǎn)坐標(biāo)減去起點(diǎn)坐標(biāo)得到這些itemview的總長(zhǎng)度
        int distance = end - start;
        if (distance == 0) {
            return INVALID_DISTANCE;
        }
        // 總長(zhǎng)度 / itemview個(gè)數(shù) = itemview平均長(zhǎng)度
        return 1f * distance / ((maxPos - minPos) + 1);
    }

可以發(fā)現(xiàn)computeDistancePerChild()方法也用總長(zhǎng)度除以ItemView個(gè)數(shù)的方式來得到ItemView平均長(zhǎng)度,并且也支持了layoutManager不同的布局方式和布局方向。

    public int[] calculateScrollDistance(int velocityX, int velocityY) {
        int[] outDist = new int[2];
        //mGravityScroller是一個(gè)Scroller,通過fling()方法模擬fling操作,通過將起點(diǎn)位置都置為0,此時(shí)得到的終點(diǎn)位置就是滾動(dòng)的距離
        mGravityScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        outDist[0] = mGravityScroller.getFinalX();
        outDist[1] = mGravityScroller.getFinalY();
        return outDist;
    }

calculateScrollDistance()是SnapHelper中的方法,它使用到的mGravityScroller是一個(gè)在attachToRecyclerView()中初始化的Scroller對(duì)象,通過Scroller.fling()方法模擬fling操作,將fling的起點(diǎn)位置為設(shè)置為0,此時(shí)得到的終點(diǎn)位置就是fling的距離。這個(gè)距離會(huì)有正負(fù)符號(hào)之分,表示滾動(dòng)的方向。

現(xiàn)在明白了吧,LinearSnapHelper的主要功能就是通過實(shí)現(xiàn)SnapHelper的三個(gè)抽象方法,從而實(shí)現(xiàn)輔助RecyclerView滾動(dòng)Item對(duì)齊中心位置。

自定義SnapHelper

經(jīng)過了以上分析,了解了SnapHelper的工作原理之后,自定義SnapHelper也就更加自如了。現(xiàn)在來看下Google Play主界面的效果。

googleplay.gif

可以看到該效果是一個(gè)類似Gallery的橫向列表滑動(dòng)控件,很明顯可以用RecyclerView來實(shí)現(xiàn),而滾動(dòng)后的ItemView是對(duì)齊RecyclerView的左邊緣位置,這種對(duì)齊效果當(dāng)仍不讓就使用了SnapHelper來實(shí)現(xiàn)了。這里就主要講下這個(gè)SnapHelper怎么實(shí)現(xiàn)的。

創(chuàng)建一個(gè)GallerySnapHelper繼承SnapHelper實(shí)現(xiàn)它的三個(gè)抽象方法:

  • calculateDistanceToFinalSnap():計(jì)算SnapView當(dāng)前位置與目標(biāo)位置的距離

        @Override
        public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
            int[] out = new int[2];
            if (layoutManager.canScrollHorizontally()) {
                out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
            } else {
                out[0] = 0;
            }
            return out;
        }
      //targetView的start坐標(biāo)與RecyclerView的paddingStart之間的差值
      //就是需要滾動(dòng)調(diào)整的距離
        private int distanceToStart(View targetView, OrientationHelper helper) {
            return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
        }
    

    ?

  • findSnapView():找到當(dāng)前時(shí)刻的SnapView。

      @Override
          public View findSnapView(RecyclerView.LayoutManager layoutManager) {
              return findStartView(layoutManager, getHorizontalHelper(layoutManager));
          }
      
      private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
            if (layoutManager instanceof LinearLayoutManager) {
              //找出第一個(gè)可見的ItemView的位置
                int firstChildPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                if (firstChildPosition == RecyclerView.NO_POSITION) {
                    return null;
                }
              //找到最后一個(gè)完全顯示的ItemView,如果該ItemView是列表中的最后一個(gè)
                  //就說明列表已經(jīng)滑動(dòng)最后了,這時(shí)候就不應(yīng)該根據(jù)第一個(gè)ItemView來對(duì)齊了
                  //要不然由于需要跟第一個(gè)ItemView對(duì)齊最后一個(gè)ItemView可能就一直無法完全顯示,
                  //所以這時(shí)候直接返回null表示不需要對(duì)齊
                if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
                    return null;
                }
              
                View firstChildView = layoutManager.findViewByPosition(firstChildPosition);
              //如果第一個(gè)ItemView被遮住的長(zhǎng)度沒有超過一半,就取該ItemView作為snapView
              //超過一半,就把下一個(gè)ItemView作為snapView
                if (helper.getDecoratedEnd(firstChildView) >= helper.getDecoratedMeasurement(firstChildView) / 2 && helper.getDecoratedEnd(firstChildView) > 0) {
                    return firstChildView;
                } else {
                    return layoutManager.findViewByPosition(firstChildPosition + 1);
                }
            } else {
                return null;
            }
        }
    
  • findTargetSnapPosition(): 在觸發(fā)fling時(shí)找到targetSnapPosition。

        @Override
        public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                                          int velocityY) {
            if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
                return RecyclerView.NO_POSITION;
            }
    
            final int itemCount = layoutManager.getItemCount();
            if (itemCount == 0) {
                return RecyclerView.NO_POSITION;
            }
    
            final View currentView = findSnapView(layoutManager);
            if (currentView == null) {
                return RecyclerView.NO_POSITION;
            }
    
            final int currentPosition = layoutManager.getPosition(currentView);
            if (currentPosition == RecyclerView.NO_POSITION) {
                return RecyclerView.NO_POSITION;
            }
    
            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
    
            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
            if (vectorForEnd == null) {
                return RecyclerView.NO_POSITION;
            }
    
            int deltaJump;
            if (layoutManager.canScrollHorizontally()) {
                deltaJump = estimateNextPositionDiffForFling(layoutManager,
                        getHorizontalHelper(layoutManager), velocityX, 0);
                if (vectorForEnd.x < 0) {
                    deltaJump = -deltaJump;
                }
            } else {
                deltaJump = 0;
            }
    
            if (deltaJump == 0) {
                return RecyclerView.NO_POSITION;
            }
            int targetPos = currentPosition + deltaJump;
            if (targetPos < 0) {
                targetPos = 0;
            }
            if (targetPos >= itemCount) {
                targetPos = itemCount - 1;
            }
            return targetPos;
        }
    

? 這個(gè)方法跟LinearSnapHelper的實(shí)現(xiàn)基本是一樣的。

就這樣實(shí)現(xiàn)三個(gè)抽象方法之后看下效果:

GallerySnapHelper1.gif

發(fā)現(xiàn)基本能像Google Play那樣進(jìn)行對(duì)齊左側(cè)邊緣。但作為一個(gè)有理想有文化有追求的程序員,怎么可以那么容易滿足呢?!極致才是最終的目標(biāo)!沒時(shí)間解釋了,快上車!

目前的效果跟Google Play中的效果主要還有兩個(gè)差異:

  1. 滾動(dòng)速度明顯慢于Google Play的橫向列表滾動(dòng)速度,導(dǎo)致滾動(dòng)起來感覺比較拖沓,看起來不是很干脆的樣子。
  2. Google Play那個(gè)橫向列表一次滾動(dòng)的個(gè)數(shù)最多就是一頁的Item個(gè)數(shù),而目前的效果滑得比較快時(shí)會(huì)滾得很遠(yuǎn)。

其實(shí)這兩個(gè)問題如果你理解了我上面所講的SnapHelper的原理,解決起來就很容易了。

對(duì)于滾動(dòng)速度偏慢的問題,由于這個(gè)fling過程是通過SnapHelper的SmoothScroller控制的,我們?cè)诜治鰟?chuàng)建SmoothScroller對(duì)象的時(shí)候就提到SmoothScroller的calculateSpeedPerPixel()方法是在定義滾動(dòng)速度的,那復(fù)寫SnapHelper的createSnapScroller()方法重新定義一個(gè)SmoothScroller不就可以了么?!

    //SnapHelper中該值為100,這里改為40
    private static final float MILLISECONDS_PER_INCH = 40f; 
    @Nullable
    protected LinearSmoothScroller createSnapScroller(final RecyclerView.LayoutManager layoutManager) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return null;
        }
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

可以看到,代碼跟SnapHelper里是一模一樣的,就只是改了MILLISECONDS_PER_INCH這個(gè)數(shù)值而已,使得calculateSpeedPerPixel()返回值變小,從而讓SmoothScroller的滾動(dòng)速度更快。

對(duì)于一次滾動(dòng)太多個(gè)Item的問題,就需要對(duì)他滾動(dòng)的個(gè)數(shù)做下限制了。那在哪里對(duì)滾動(dòng)的數(shù)量做限制呢?findTargetSnapPosition()方法里! 該方法的作用就是在尋找需要滾動(dòng)到哪個(gè)位置的,不在這里還能在哪里?!直接看代碼:

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
       ...

        //計(jì)算一屏的item數(shù)
        int deltaThreshold = layoutManager.getWidth() / getHorizontalHelper(layoutManager).getDecoratedMeasurement(currentView);

        int deltaJump;
        if (layoutManager.canScrollHorizontally()) {
            deltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
          //對(duì)估算出來的位置偏移量進(jìn)行閾值判斷,最多只能滾動(dòng)一屏的Item個(gè)數(shù)
            if (deltaJump > deltaThreshold) {
                deltaJump = deltaThreshold;
            }
            if (deltaJump < -deltaThreshold) {
                deltaJump = -deltaThreshold;
            }
            if (vectorForEnd.x < 0) {
                deltaJump = -hDeltaJump;
            }
        } else {
            deltaJump = 0;
        }

        ...
    }

可以看到就是對(duì)估算出來的位置偏移量做下大小限制而已,就這么簡(jiǎn)單!

通過這樣調(diào)整,效果已經(jīng)跟Google Play基本一樣了,我猜Google Play也是這樣做的!看效果:

GallerySnapHelper2.gif

附錄

SnapHelperDemo地址

?

?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評(píng)論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評(píng)論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介: 提供一個(gè)讓有限的窗口變成一個(gè)大數(shù)據(jù)集的靈活視圖。 術(shù)語表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,199評(píng)論 0 16
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,666評(píng)論 25 708
  • 這篇文章分三個(gè)部分,簡(jiǎn)單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比...
    LucasAdam閱讀 4,408評(píng)論 0 27
  • 三分鐘熱度是我們從小到大聽的最多的關(guān)于做事的話語。面對(duì)一個(gè)新事物,新技能,我們很可能三分鐘熱度,這現(xiàn)象的原因在于沒...
    CSir205閱讀 268評(píng)論 0 0
  • 學(xué)習(xí)阿佳老師課程之前我是一名賣化妝品男性微商 做微商都知道人脈是微商的一大命脈,我從15年到17年兩年當(dāng)中一直在網(wǎng)...
    大通哥閱讀 220評(píng)論 0 0