RecyclerView左右聯(lián)動(仿拼多多搜索界面商品聯(lián)動)

最近在一個交流群里看到有個朋友問兩個RecyclerView左右聯(lián)動效果怎么實現(xiàn)酌伊,看了他的效果圖就感覺很熟悉跑筝,發(fā)現(xiàn)是自己去年9月份寫過的一個界面痰催,于是乎回顧了當(dāng)初所寫的代碼,并寫了一個Demo蜻懦。

拼多多效果圖:

拼多多效果圖.gif

看圖分析需求:

  1. 左邊item點擊選中時,顯示狀態(tài)改變夕晓,右邊將相應(yīng)的分類內(nèi)容滑動到最頂部顯示宛乃。
  2. 右邊滑動時,左邊item跟隨著滑動改變選中狀態(tài)蒸辆,具體為右邊所看見第一個item內(nèi)容列表的顯示高度小于自身一半時征炼,左邊自動跳轉(zhuǎn)到下一個item。

實現(xiàn)思路:

圖片分析.png
  1. 如上圖所示躬贡,整體界面可用三個RecyclerView實現(xiàn)谆奥,1,2關(guān)聯(lián)滑動拂玻,3為嵌套內(nèi)容顯示酸些。
  2. 左邊item點擊實現(xiàn)為在數(shù)據(jù)源中添加一個isSelected布爾值,在holder中通過判斷isSelected是否為true來改變背景顏色和字體顏色檐蚜,每次點擊后改變isSelected值魄懂,然后調(diào)用notifyItemChanged(position)方法刷新界面。
  3. 接下來就考慮左邊點擊時右邊跟隨著滑動熬甚,RecyclerView中滑動到指定position的方法有scrollToPosition(position)和smoothScrollToPosition(position)兩種逢渔,兩者區(qū)別為前者類似于直接跳到position位置,后者有一個滑動過程乡括,故此我們選擇后者來實現(xiàn)滑動效果肃廓。
  4. 右邊監(jiān)聽滑動,設(shè)置addOnScrollListener在onScrolled()方法中獲取第一個可見item诲泌,判斷item的顯示高度來改變左邊item所選中的position盲赊。

所遇到的問題

  1. 在點擊后,右邊滑動時敷扫,會再次刷新左邊選中狀態(tài)哀蘑,例:當(dāng)前選中為第一項诚卸,當(dāng)點擊第五項時,第五項設(shè)為選中狀態(tài)绘迁,右邊會將第五個item滑動到頂部顯示合溺,不過在滑動的時候會觸發(fā)onScrolled()方法,這使得左邊依次的刷新1缀台、2棠赛、3、4膛腐、5個item的選中狀態(tài)睛约。
    解決辦法:設(shè)置一個點擊狀態(tài)變量clicked,當(dāng)是點擊時將clicked設(shè)為true哲身,然后在onScrolled判斷若是點擊的條件下辩涝,不觸發(fā)左邊的關(guān)聯(lián)滑動,最后在onScrollStateChanged(RecyclerView recyclerView, int newState)方法中勘天,判斷clicked是否為真且newState為停止?fàn)顟B(tài)時將點擊狀態(tài)設(shè)為flase怔揩。
    Tips:newState有三種狀態(tài):SCROLL_STATE_IDLE:停止?fàn)顟B(tài);SCROLL_STATE_DRAGGING:手指拖動狀態(tài)误辑;SCROLL_STATE_SETTLING:手指拖動后剛抬起手指的狀態(tài)沧踏;
  2. 調(diào)用smoothScrollToPosition(position)時,item有時會在頂部巾钉,有時會在底部翘狱,而要求是item每次都滑動到頂部。
    解決辦法:當(dāng)初處理這個問題時使用了其他方法砰苍,不過我實現(xiàn)后還是不行潦匈,最后在源碼中找到了解決方案。首先赚导,思考問題茬缩,為什么會有時出現(xiàn)到頂部,有時出現(xiàn)在底部呢吼旧,經(jīng)過多次探索凰锡,發(fā)現(xiàn)smoothScrollToPosition(position)的方法的目的是將該position的item顯示在界面上。具體的效果可理解為若該position的item的top和bottom都在界面上圈暗,那么將不會滑動掂为,若只有top在界面上,那么會向上滑動员串,將bottom顯示出來即可勇哗,若只有bottom在界面上,那么會向下滑動寸齐,將top也展示在界面上欲诺,若兩者都不在界面上抄谐,會自動計算上下滑動的距離,選擇較小的值滑動扰法。那么我們可不可以只讓它滑動到頂部呢蛹含,去源碼中找找具體的實現(xiàn)吧。
    首先進入smoothScrollToPosition(position)方法中,發(fā)現(xiàn)主要是調(diào)用了mLayout.smoothScrollToPosition(this, mState, position)方法塞颁。
 public void smoothScrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.smoothScrollToPosition(this, mState, position);
    }

此處的mLayout是我們?yōu)镽ecyclerView設(shè)置的LayoutManager挣惰,所以具體的就在LinearLayoutManager中查看,在該類中找到重寫的smoothScrollToPosition方法殴边。

public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

在該方法中new了一個LinearSmoothScroller對象,然后調(diào)用startSmoothScroll方法珍语,startSmoothScroll后續(xù)中并沒有發(fā)現(xiàn)怎么計算滑動的锤岸,所以具體實現(xiàn)就在LinearSmoothScroller類中。

/**
     * Align child view's left or top with parent view's left or top
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_START = -1;

    /**
     * Align child view's right or bottom with parent view's right or bottom
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_END = 1;

    /**
     * <p>Decides if the child should be snapped from start or end, depending on where it
     * currently is in relation to its parent.</p>
     * <p>For instance, if the view is virtually on the left of RecyclerView, using
     * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_ANY = 0;

一進入LinearSmoothScroller就被這三個常量所吸引板乙,用我蹩腳的英語翻譯了下是偷,發(fā)現(xiàn)有點意思,首先看SNAP_TO_START:Align child view's left or top with parent view's left or top(將子視圖的左視圖或上視圖與父視圖的左視圖或上視圖對齊)募逞,這不就是我們想要的頂部對齊嗎蛋铆,然后就選中Ctrl+F,找尋它的蹤影放接,下一個刺啦、下一個、下一...等會兒纠脾,好像發(fā)現(xiàn)了什么B耆场!苟蹈!

/**
     * When scrolling towards a child view, this method defines whether we should align the left
     * or the right edge of the child with the parent RecyclerView.
     *
     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
     * @see #SNAP_TO_START
     * @see #SNAP_TO_END
     * @see #SNAP_TO_ANY
     */
    protected int getHorizontalSnapPreference() {
        return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
                mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

    /**
     * When scrolling towards a child view, this method defines whether we should align the top
     * or the bottom edge of the child with the parent RecyclerView.
     *
     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
     * @see #SNAP_TO_START
     * @see #SNAP_TO_END
     * @see #SNAP_TO_ANY
     */
    protected int getVerticalSnapPreference() {
        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

再次用蹩腳的英語和對代碼的理解糊渊,果斷確定了,這就是我們的解決辦法;弁选C烊蕖!重寫getVerticalSnapPreference()方法菱鸥,將返回值設(shè)為SNAP_TO_START即可宗兼。
解決方法是有了,不過還是得看看到底是怎么判斷的吧采缚,mTargetVector為一個PointF類针炉,主要是根據(jù)y坐標(biāo)的大小來返回值,那我們就看看這個mTargetVector是怎么來的吧扳抽。同樣Ctrl+F篡帕,查找mTargetVector的來源殖侵。

 protected void updateActionForInterimTarget(Action action) {
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
       ...
        mTargetVector = scrollVector;
       ...
      }

scrollVector賦值給mTargetVector,然后進入 computeScrollVectorForPosition方法镰烧。

public PointF computeScrollVectorForPosition(int targetPosition) {
        RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager instanceof ScrollVectorProvider) {
            return ((ScrollVectorProvider) layoutManager)
                    .computeScrollVectorForPosition(targetPosition);
        }
        Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
                + " does not implement " + ScrollVectorProvider.class.getCanonicalName());
        return null;
    }

發(fā)現(xiàn)返回的是layoutManager.computeScrollVectorForPosition(targetPosition)拢军,那就只有去LinearLayoutManager中找答案了。

@Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        //當(dāng)RecyclerView的item為0時
        if (getChildCount() == 0) {
            return null;
        }
      //獲取第一個可見item在Adapter數(shù)據(jù)源中的位置
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }

解釋一波:getChildAt(0)獲取第一個可見item怔鳖,getPosition(View view)通過view來獲取該view在Adapter數(shù)據(jù)源中的position茉唉,為什么要這么做,那是因為RecyclerView復(fù)用的原因结执,mShouldReverseLayout為是否反轉(zhuǎn)度陆,最后判斷targetPosition和firstChildPos大小設(shè)置direction,direction的值便是我們上面看到的y值献幔。

OK懂傀,思路和解決問題都已經(jīng)說清楚了,那么就just show the code@小5乓稀!

左邊Recycle人View的實現(xiàn)

左邊主要代碼是在Adapter中寫一個設(shè)置選中狀態(tài)的函數(shù)郑兴,holder中設(shè)置選中狀態(tài)的item背景犀斋,item的布局一個TextView就Ok了。

public class LeftAdapter extends RecyclerArrayAdapter<LeftBean> {
    private int prePosition = 0;
    public LeftAdapter(Context context) {
        super(context);
    }
    public LeftAdapter(Context context, LeftBean[] objects) {
        super(context, objects);
    }
    public LeftAdapter(Context context, List<LeftBean> objects) {
        super(context, objects);
    }
    @Override
    public BaseViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) {
        return new LeftHolder(parent);
    }
    public void setSelectedPosition(int position){
        if (prePosition != position){
            //Tips:處理右邊滑動時左邊對應(yīng)的item不在屏幕上的情況
            mRecyclerView.smoothScrollToPosition(position);
            mObjects.get(prePosition).setSelected(false);
            notifyItemChanged(prePosition);
            prePosition = position;
            mObjects.get(prePosition).setSelected(true);
            notifyItemChanged(prePosition);
        }
    }
}
===============Holder代碼=================
public class LeftHolder extends BaseViewHolder<LeftBean> {
    private TextView textView;
    public LeftHolder(ViewGroup itemView) {
        this(itemView, R.layout.item_left);
        textView = $(R.id.text_menu);
    }
    public LeftHolder(ViewGroup parent, int res) {
        super(parent, res);
    }
    @Override
    public void setData(LeftBean data) {
        super.setData(data);
        textView.setText(data.getName());
        //設(shè)置選中狀態(tài)的背景
        if (data.isSelected()){
            textView.setBackgroundColor(Color.WHITE);
        }
        else {
            textView.setBackgroundColor(Color.parseColor("#747373"));
        }
    }
}

右邊RecyclerView實現(xiàn)

右邊界面實現(xiàn)就是兩個RecyclerView的嵌套情连,很簡單的叽粹,就不粘貼代碼了。不過需要注意的是嵌套的item布局高度寫成wrap_content却舀,如果是match_parent可能會造成內(nèi)容顯示不完整球榆。

RecyclerView聯(lián)動效果實現(xiàn)

在MainActivity中,左邊RecyclerView設(shè)置點擊事件禁筏,右邊RecyclerView添加滑動監(jiān)聽事件持钉。

       recyclerLeft.setLayoutManager(new LinearLayoutManager(this));recyclerLeft.setAdapter(leftAdapter);
       leftAdapter.setOnItemClickListener(new RecyclerArrayAdapter.OnItemClickListener() {
           @Override
           public void onItemClick(int position) {
               //關(guān)聯(lián)右邊滑動
               recyclerRight.smoothScrollToPosition(position);
               leftAdapter.setSelectedPosition(position);
               clicked = true;
           }
       });
       ================右邊滑動監(jiān)聽===================
       //設(shè)置能夠平滑到頂部的LayouManager
       recyclerRight.setLayoutManager(new ScrollTopLayoutManager(this));
       recyclerRight.setAdapter(rightAdapter);
       recyclerRight.addOnScrollListener(new RecyclerView.OnScrollListener() {
           @Override
           public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
               if (clicked && newState == RecyclerView.SCROLL_STATE_IDLE) {
                   clicked = false;
               }
           }
           @Override
           public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
               super.onScrolled(recyclerView, dx, dy);
               if (!clicked) {
                   LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                   int currentItem = linearLayoutManager.findFirstVisibleItemPosition();
                   View v = linearLayoutManager.findViewByPosition(currentItem);
                   //右邊滑動超過當(dāng)前item一半時左邊切換到下一個
                   if (v.getBottom() < v.getHeight() / 2 && currentItem >= 0) {
                       if (currentItem < linearLayoutManager.getItemCount() - 1){
                           leftAdapter.setSelectedPosition(currentItem + 1);
                       }
                   } else {
                       leftAdapter.setSelectedPosition(currentItem);
                   }
               }
           }
       });

ScrollTopLayoutManager的實現(xiàn)

ScrollTopLayoutManager繼承自LinearLayoutManager,重寫了smoothScrollToPosition方法篱昔,并設(shè)置滑動到頂部的SmoothScroller每强。

public class ScrollTopLayoutManager extends LinearLayoutManager {

   public ScrollTopLayoutManager(Context context) {
       super(context);
   }

   public ScrollTopLayoutManager(Context context, int orientation, boolean reverseLayout) {
       super(context, orientation, reverseLayout);
   }

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

   @Override
   public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
       TopLinearSmoothScroller topLinearSmoothScroller = new TopLinearSmoothScroller(recyclerView.getContext());
       topLinearSmoothScroller.setTargetPosition(position);
       startSmoothScroll(topLinearSmoothScroller);
   }

   private static class TopLinearSmoothScroller extends LinearSmoothScroller {

       public TopLinearSmoothScroller(Context context) {
           super(context);
       }

       @Override
       protected int getVerticalSnapPreference() {
           //滑動到頂部
           return SNAP_TO_START;
       }
   }
}

最后在加入模擬數(shù)據(jù),渲染在界面上就實現(xiàn)該效果了州刽。

private void initData() {
        if (null == leftAdapter){
            ArrayList<LeftBean> leftBeans = new ArrayList<>();
            for (int i = 0; i < 10; i++){
                leftBeans.add(new LeftBean("我是左邊第"+ i + "個"));
                //默認(rèn)第一個為選中狀態(tài)
                if (i == 0){
                    leftBeans.get(0).setSelected(true);
                }
            }
            leftAdapter = new LeftAdapter(this,leftBeans);
        }
        if (null == rightAdapter){
            ArrayList<RightBean> rightBeans = new ArrayList<>();
            for (int i = 0; i < 10; i++){
                ArrayList<InnerBean> innerBeans = new ArrayList<>();
                for (int j = 0; j < 5; j++){
                    innerBeans.add(new InnerBean("我是里邊第"+ j + "個"));
                }
                rightBeans.add(new RightBean("我是右邊第"+ i + "個",innerBeans));
            }
            rightAdapter = new RightAdapter(this,rightBeans);
        }
    }

Demo效果圖:

Demo效果圖.gif

該Demo的數(shù)據(jù)較為簡單空执,并沒有添加實用數(shù)據(jù),此次Demo中所用的Adapter為EasyRecyclerView中的穗椅,具體可看鏈接https://github.com/Jude95/EasyRecyclerView辨绊。

所有的代碼已經(jīng)上傳到GitHub了,代碼地址:https://github.com/ysls/Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匹表,一起剝皮案震驚了整個濱河市门坷,隨后出現(xiàn)的幾起案子宣鄙,更是在濱河造成了極大的恐慌,老刑警劉巖默蚌,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冻晤,死亡現(xiàn)場離奇詭異,居然都是意外死亡绸吸,警方通過查閱死者的電腦和手機鼻弧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锦茁,“玉大人攘轩,你說我怎么就攤上這事÷肓” “怎么了撑刺?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長握玛。 經(jīng)常有香客問我,道長甫菠,這世上最難降的妖魔是什么挠铲? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮寂诱,結(jié)果婚禮上拂苹,老公的妹妹穿的比我還像新娘。我一直安慰自己痰洒,他們只是感情好瓢棒,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丘喻,像睡著了一般脯宿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上泉粉,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天连霉,我揣著相機與錄音,去河邊找鬼嗡靡。 笑死跺撼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讨彼。 我是一名探鬼主播歉井,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哈误!你這毒婦竟也來了哩至?” 一聲冷哼從身側(cè)響起躏嚎,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎憨募,沒想到半個月后紧索,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡菜谣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年珠漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尾膊。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡媳危,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冈敛,到底是詐尸還是另有隱情待笑,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布抓谴,位于F島的核電站暮蹂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏癌压。R本人自食惡果不足惜仰泻,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滩届。 院中可真熱鬧集侯,春花似錦、人聲如沸帜消。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泡挺。三九已至辈讶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娄猫,已是汗流浹背幅恋。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工阶淘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓芹壕,卻偏偏與公主長得像还惠,于是被迫代替她去往敵國和親您宪。 傳聞我的和親對象是個殘疾皇子膀跌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

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