Android仿閑魚首頁

先放一個效果圖

demo.gif

Demo是基于MVVM模式來編寫的,歡迎大家給予批評和指正悴侵。
其中Banner的無限輪播用了PageSnapHelper,后續(xù)RecycleView也可以實現(xiàn)更多類似ViewPage的效果了
項目鏈接:https://github.com/ly85206559/demo4Fish
如果能對你有幫助那就最好了

可以看出頁面大概可以分為這幾個部分

1.最上面是一個輪播的Banner
2.中間可能有些其他的功能列表
3.最后是Tab頁(這里是新鮮的和附近的兩個列表)

OK豌习,看到這樣的布局需求的時候可能有兩種思路

  1. 整體是一個RefreshLayout布局步责,內(nèi)嵌RecycleView,而Banner頁叠纷,其他功能列表以及TabLayout都當(dāng)成RecycleView的頭加入到RecycleView中刻帚,TabLayout下面是真正的列表項
  1. 整體還是一個RefreshLayout布局潦嘶,內(nèi)部是一個NestScrollView涩嚣,Banner頁,其他功能列表掂僵,TabLayout依次布局在NestScrollView中航厚,然后最下面布局一個FrameLayout,TabLayout切換的時候切換不同的Fragment
Demo中使用的是第一種方式锰蓬,第二種方式考慮到SwipeRefreshLayout和內(nèi)部FrameLayout的滑動會有沖突幔睬,后續(xù)再嘗試編寫

接下來考慮需要考慮的問題

  1. TabLayout需要固定到頂部
  1. 第一次加載數(shù)據(jù)的時候需要有個Loading提示,Demo中就是一個小魚的空白等待頁
  2. 因為使用一個數(shù)據(jù)集芹扭,在TabLayout來回切換的時候需要保證數(shù)據(jù)集合所在的位置是正確的(比如新鮮的這個列表當(dāng)前在Position1的位置麻顶,切換到附近的列表我滑到了Position2的位置,當(dāng)我再切回新鮮的時候需要回到Position1的位置)

下面就一些核心的代碼和思路講解一下

首先是布局舱卡,布局很簡單辅肾,SwipeRefreshLayout中包了一個FrameLayout,然后在FrameLayout中包含了一個RecycleView

<android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/layout_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/list"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </FrameLayout>
    </android.support.v4.widget.SwipeRefreshLayout>

接下來看下StickyHead如何實現(xiàn)

//正常的TabLayout布局
private TabLayout mTabLayout;
//粘性 TabLayout布局(用于固定在頂部)
private TabLayout mStickyTabLayout;
//粘性布局的Y坐標(biāo)(用戶判斷粘性布局是否顯示)
private int mStickyPositionY;
//主列表布局
private RecyclerView mHomeList;

這是變量的定義轮锥,下面的這個類是我將一些頁面邏輯涉及的變量抽離出來

public class HomeEntity extends BaseObservable {

//列表類型 0:新鮮的 1:附近的
public static final int LIST_TYPE_FRESH = 0;
public static final int LIST_TYPE_NEAR = 1;

private int bannerCount;
private int listType = LIST_TYPE_FRESH;
//新鮮的和附近的首次加載的loading狀態(tài)
private boolean refreshLoading;
private boolean nearLoading;
//首頁是否正在下拉刷新
private boolean refreshing;
//新鮮的和附近的 獲取更多的View的狀態(tài)值(用戶記錄TabLayout切換的時候矫钓,LoadingMore的狀態(tài))
private int refreshMoreStatus;
private int nearMoreStatus;
//首頁的活動更多的狀態(tài)
private int loadingMoreStatus;
}

這是變量的定義,然后初始化兩個TabLayout舍杜,主要在于需要監(jiān)聽TabLayout的切換

mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            int position = tab.getPosition();
            //設(shè)置粘貼TabLayout的選中Tab
            if (!mStickyTabLayout.getTabAt(position).isSelected()) {
                mStickyTabLayout.getTabAt(position).select();
                mViewModel.changeHomeData(position);
            }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });
mStickyTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            int position = tab.getPosition();
            if (!mTabLayout.getTabAt(position).isSelected()) {
                mTabLayout.getTabAt(position).select();
                mHomeList.stopScroll();

                //mAdapter.setEnableLoadMore(false);
                mViewModel.changeHomeData(position);
                ......
              }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    }); 

這段的邏輯比較簡單新娜,就是實現(xiàn)了保持TabLayout切換狀態(tài)的統(tǒng)一,當(dāng)TabLayout切換的時候既绩,需要將StickyTabLayout所選中的Tab也設(shè)置一下概龄,mViewModel.changeHomeData(position)這句話是為了切換數(shù)據(jù),下面會分析到

接下來是StickyHead重要的代碼

mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            int[] location = new int[2];
            mTabLayout.getLocationInWindow(location);
            int count = mViewContainer.getChildCount();
            if (location[1] <= mStickyPositionY) {
                if (count == 1) {
                    mViewContainer.addView(mStickyTabLayout);
                    mBinding.layoutRefresh.setEnabled(false);
                }
            } else {
                if (count > 1) {
                    mViewContainer.removeView(mStickyTabLayout);
                    //mOffsetY = DisplayUtil.dip2px(mContainer.getContext(), 46);
                    //mRefreshPosition = mAdapter.getHeaderLayoutCount();
                    //mNearPosition = mAdapter.getHeaderLayoutCount();
                    mBinding.layoutRefresh.setEnabled(true);
                }
            }

            //if (mInitPositionY == -1) {
                //mInitPositionY = location[1];
            //}
            //mHomeListPositionY = location[1];
        }
    });

主要邏輯就是先獲取TabLayout在窗口的位置饲握,如果Y坐標(biāo)小于粘貼頭部的Y坐標(biāo)私杜,則將粘貼頭部加入到布局中來并顯示吸重,否則,將粘貼頭部布局從布局中移除歪今。判斷count這個值是為了防止重復(fù)添加和重復(fù)移除粘貼頭布局嚎幸。mBinding.layoutRefresh.setEnabled(true/false)是為了在粘貼頭部固定在頂上的時候消除掉外層SwipeRefreshLayout的下拉刷新錯誤。注釋掉的代碼會在下面再講

只需要上面的這么多代碼一個StickyHead就實現(xiàn)了寄猩,在測試的時候遇到點(diǎn)小問題嫉晶,就是焦點(diǎn)重置導(dǎo)致的RecycleView重新回到初始位置的一個錯誤,下面是暫時的解決方案
LinearLayoutManager manager = new LinearLayoutManager(mContainer.getContext()) {
        @Override
        public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused) {
            //TODO 暫時處理View焦點(diǎn)問題
            return true;
        }
    };

下面簡單說下如何實現(xiàn)首次加載新鮮的或者附近的數(shù)據(jù)的時候出現(xiàn)的一個等待頁面

主要思路是這樣的

  1. 這個等待的LoadingView是當(dāng)成RecycleView的頭加在TabLayout后面的田篇,當(dāng)數(shù)據(jù)加載完成這個LoadingView設(shè)置為不可見

  2. 因為有TabLayout會切換替废,導(dǎo)致RecycleView的數(shù)據(jù)會重新繪制,進(jìn)而導(dǎo)致RecyView會回到初始位置泊柬,所以需要記錄下RecycleView所在的位置椎镣,然后手動滑動到記錄的位置
    具體的我們還是來看代碼吧

    private int mHomeListPositionY;//用來標(biāo)識當(dāng)前RecycleView的位置
    private int mInitPositionY = -1;//初始狀態(tài)下RecycleView的Y坐標(biāo)
    //這里是RecycleView的滑動監(jiān)聽,用來記錄RecycleView的位置兽赁,這里其實是記錄了mTabLayout的位置
    mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    int[] location = new int[2];
    mTabLayout.getLocationInWindow(location);
    int count = mViewContainer.getChildCount();

             if (mInitPositionY == -1) {
                 mInitPositionY = location[1];
             }
             mHomeListPositionY = location[1];
         }
     });
    

    //這個函數(shù)就是用來手動將RecycleView滑動到正確的位置
    private void setLoadingView(boolean visible, int type) {
    int position;
    if (type == HomeEntity.LIST_TYPE_FRESH) {
    position = mRefreshPosition;
    } else {
    position = mNearPosition;
    }
    if (visible) {
    mLoadingView.setVisibility(View.VISIBLE);
    if (mStickyTabLayout.getVisibility() == View.VISIBLE) {
    LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
    layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
    }
    } else {
    mLoadingView.setVisibility(View.GONE);
    LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
    if (mViewContainer.getChildCount() > 1) {
    layoutManager.scrollToPositionWithOffset(position, mStickyTabLayout.getHeight());
    } else {
    layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
    }
    }
    }

最后來看下新鮮的和附近的加載更多時頁面的實現(xiàn)

這里Adapter使用了第三方BRVAH状答,所以相對LoadingMore的狀態(tài)BRVAH幫我封了一下,因為雖然是一個List刀崖,但其實是兩個列表復(fù)用一個List的惊科,所以這里的LoadingMore狀態(tài)我們需要記錄兩個,方便切換的時候列表的LoadingMore狀態(tài)是正確的亮钦,下面看下主要代碼

if (propertyId == BR.refreshLoading) {
            if (HomeEntity.LIST_TYPE_FRESH != entity.getListType()) {
                return;
            }
            if (mLoadingView.getVisibility() == View.GONE) {
                mAdapter.setEnableLoadMore(true);
            }
        } else if (propertyId == BR.nearLoading) {
            if (HomeEntity.LIST_TYPE_NEAR != entity.getListType()) {
                return;
            }
            if (mLoadingView.getVisibility() == View.GONE) {
                mAdapter.setEnableLoadMore(true);
            }
        } else if (propertyId == BR.loadingMoreStatus) {
            int status = entity.getLoadingMoreStatus();
            mAdapter.setEnableLoadMore(true);
            if (LoadMoreView.STATUS_DEFAULT == status) {
                mAdapter.loadMoreComplete();
            } else if (LoadMoreView.STATUS_END == status) {
                mAdapter.loadMoreEnd();
            } else if (LoadMoreView.STATUS_FAIL == status) {
                mAdapter.loadMoreFail();
            }
        }

BR.refreshLoading和BR.nearLoading 都是監(jiān)聽首次加載馆截,這里
if (mLoadingView.getVisibility() == View.GONE) {
mAdapter.setEnableLoadMore(true);
}
這個是為了防止首次加載顯示Loading頁面的時候又顯示了LoadingMore布局
BR.loadingMoreStatus這個就是監(jiān)聽LoadingMore的狀態(tài)來更新List的Adapter

其他的主要ViewModel代碼在HomeViewModel中。主要的幾個點(diǎn)

  1. 粘貼頭布局的邏輯
  2. TabLayout切換導(dǎo)致數(shù)據(jù)集變化以及位置的變化
  3. 加載更多的時候需要考慮TabLayout切換的問題
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜂莉,一起剝皮案震驚了整個濱河市蜡娶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌映穗,老刑警劉巖窖张,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異男公,居然都是意外死亡荤堪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進(jìn)店門枢赔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澄阳,“玉大人,你說我怎么就攤上這事踏拜∷橛” “怎么了?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵速梗,是天一觀的道長肮塞。 經(jīng)常有香客問我襟齿,道長,這世上最難降的妖魔是什么枕赵? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任猜欺,我火速辦了婚禮,結(jié)果婚禮上拷窜,老公的妹妹穿的比我還像新娘开皿。我一直安慰自己,他們只是感情好篮昧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布赋荆。 她就那樣靜靜地躺著,像睡著了一般懊昨。 火紅的嫁衣襯著肌膚如雪窄潭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天酵颁,我揣著相機(jī)與錄音嫉你,去河邊找鬼。 笑死材义,一個胖子當(dāng)著我的面吹牛均抽,可吹牛的內(nèi)容都是我干的嫁赏。 我是一名探鬼主播其掂,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼潦蝇!你這毒婦竟也來了款熬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤攘乒,失蹤者是張志新(化名)和其女友劉穎贤牛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體则酝,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殉簸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沽讹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片般卑。...
    茶點(diǎn)故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖爽雄,靈堂內(nèi)的尸體忽然破棺而出蝠检,到底是詐尸還是另有隱情,我是刑警寧澤挚瘟,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布叹谁,位于F島的核電站饲梭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏焰檩。R本人自食惡果不足惜憔涉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望析苫。 院中可真熱鬧监氢,春花似錦、人聲如沸藤违。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顿乒。三九已至议街,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間璧榄,已是汗流浹背特漩。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骨杂,地道東北人涂身。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像搓蚪,于是被迫代替她去往敵國和親蛤售。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,580評論 2 349

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,807評論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,732評論 22 665
  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案妒潭? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,737評論 1 92
  • 越來越不會說話了悴能,也越來越不喜歡說話了。 其實并不是不愿意張嘴雳灾,而是因為沒有愿意支起的耳朵漠酿。 話總是要說給愿意的人...
    墩琦兒閱讀 164評論 0 1
  • 1. 很久沒有動筆了。剛剛看到一篇文章谎亩,名為《你過分在意別人 忽視自己》炒嘲,作者拂逆之間。忽然很想借著對這篇文章的感...
    carbon小白閱讀 8,555評論 7 35