Android從零擼美團(四) - 美團首頁佈局解析及實現 - Banner+自定義View+SmartRefreshLayout下拉刷新上拉加載更多

這是【從零擼美團】系列文章第四篇。 項目地址:github.com/cachecats/L…前端

Android從零擼美團(一) - 統一管理 Gradle 依賴 提取到單獨文件中java

Android從零擼美團(二) - 仿美團下拉刷新自定義動畫android

Android從零擼美團(三) - Android多標籤tab滑動切換 - 自定義View快速實現高度定製封裝git

仿美團開源項目總體架構和首頁其實早就完成了,前段時間家裏各類事情搞得心力交瘁,停更了一段時間。甚至一度動搖繼續這個項目的決心,由於最近在學前端,在技術的深度和廣度之間一直糾結搖擺不定。一個聲音是繼續完成這個項目,把安卓玩的更深刻一些;另外一個聲音是趕忙學前端吧,抓緊拓展技術棧,不要在這個項目上浪費太多精力。github

思來想去仍是繼續完成項目吧,本身開的項目跪着也要走完 〒▽〒數據庫

最後肯定了繼續寫項目和學前端同時進行的戰略方針~數組

老規矩,先上圖,再 分析原理 --> 準備材料 --> 具體實現 三步走一步步的搞定。bash

在這裏插入圖片描述

1、分析

相比於普通的應用,美團、去哪兒這樣的平臺性 App 的首頁仍是至關複雜的,簡直想把全世界都包進去~網絡

在這裏插入圖片描述

剛開始看可能以爲眼花繚亂,但仔細觀察,能夠把它抽象成六個模塊:架構

  1. 最上面的輪播廣告條,裏面包含若干個廣告圖片自動無限輪播。暫時稱之爲 Banner(注意這幾個模塊起的英文名對應着代碼中的模塊名)。
  2. 輪播條下面的美食、電影/演出、酒店住宿、休閒娛樂、外賣等五個大模塊入口,暫時稱之爲大模塊 BigModule。
  3. 再往下相似 GridView 的兩排小圖標,KTV、周邊遊……暫時稱之爲小模塊 SmallModule。
  4. 小模塊下面四張廣告圖片,乍一看是沒有規則的瀑布佈局,實際上是互相對齊的簡單規則佈局。暫時稱之爲 HomeAdsView。
  5. 最後就是列表 RecyclerView 了,顯示附近團購信息。
  6. 還有一個不太明顯的,上拉刷新下拉加載更多,也算一個模塊吧。

抽絲剝繭後就是這六個模塊啦,是否是一下清爽不少?

實現思路

輪播條選用了第三方的庫:Banner, 有 5.2k 顆 star,很是優秀的庫。

大模塊 BigModule 採用代碼中動態添加 View 的方式實現,好處在於能快速響應變化,假如需求變成一行放4個圖標,只須要在 java 文件中改一句代碼就好,不用修改資源文件。

兩行小模塊 SmallModule 是 RecyclerView 實現的 GridView。

四張廣告圖片 HomeAdsView 是封裝的自定義 View,高度封裝優勢是徹底解耦,簡化了主頁的佈局,使用配置簡單,後期維護方便。

最下面的列表用的是 RecyclerView,BaseRecyclerViewAdapterHelper 做爲輔助。

下拉刷新組件用的是 SmartRefreshLayout

2、準備

主頁中用到了三個框架,在 app/build.gradle 下添加以下依賴:

//Banner
implementation "com.youth.banner:banner:1.4.10"
//BaseRecyclerViewAdapterHelper
implementation "com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30"
//SmartRefreshLayout
implementation "com.scwang.smartrefresh:SmartRefreshLayout:1.0.4"
複製代碼

注:AndroidStudio 3.0 以上用 implementation,3.0如下用 compile。 項目中還用到了不少其餘庫,如 Dagger、RxJava、ButterKnife、Glide 等,就不一一貼出來了,具體的使用方式請自行查閱資料或看本項目源碼 github.com/cachecats/L…

3、實現

項目採用 MVP 架構,主頁代碼在 app/home 目錄下的 HomeFragmentHomeFragmentPresenter 中。

佈局文件是 fragment_home.xml,佈局代碼以下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <!--下拉刷新組件-->
    <com.scwang.smartrefresh.layout.SmartRefreshLayout
        android:id="@+id/smartRefreshLayout_home"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <!--Banner輪播條-->
                <com.youth.banner.Banner
                    android:id="@+id/home_banner"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    app:image_scale_type="center_crop"
                    app:scroll_time="500" />

                <!--5個大模塊佈局-->
                <LinearLayout
                    android:id="@+id/ll_big_module_fragment_home"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="15dp"
                    android:layout_marginTop="15dp"
                    android:orientation="horizontal" />

                <!--分割線-->
                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:layout_marginLeft="20dp"
                    android:layout_marginRight="20dp"
                    android:background="@color/dividerColorF0" />

                <!--兩行小模塊佈局 RecyclerView實現的GridView -->
                <android.support.v7.widget.RecyclerView
                    android:id="@+id/recyclerview_little_module"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="10dp" />

                <!--四個廣告封裝的自定義View-->
                <com.cachecats.meituan.widget.HomeAdsView
                    android:id="@+id/home_ads_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />

                <!--團購列表-->
                <android.support.v7.widget.RecyclerView
                    android:id="@+id/recycler_view_shops"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />

            </LinearLayout>
        </ScrollView>
    </com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>
複製代碼

佈局解析

最外層用 LinearLayout 包裹,接下來是下拉刷新組件 SmartRefreshLayout,由於要實現整個主頁的刷新。而後是滾動組件 ScrollView,由於要總體滑動。因爲 ScrollView 裏只能包含一個子 View,因此在裏面又包了層 LinearLayout 。接下來就是五個分模塊的具體佈局啦。

1. Banner輪播條

添加Banner依賴後,在佈局文件中添加 Banner佈局,並設置控件高度、圖片裁剪模式、滾動時間等參數,而後在 HomeFragment 中初始化:

public void initBanner() {
        //設置banner的各類屬性
        banner.setBannerStyle(BannerConfig.CIRCLE_INDICATOR)
                .setImageLoader(new GlideImageLoader())
                .setImages(presenter.getBannerImages()) //從Presenter中取出圖片資源
                .setBannerAnimation(Transformer.Default)
                .isAutoPlay(true)
                .setDelayTime(3000)
                .setIndicatorGravity(BannerConfig.CENTER)
                .start();
    }
複製代碼

HomeFragmentPresenter

/**
     * 獲取Banner的圖片資源
     *
     * @return
     */
    @Override
    public List<Integer> getBannerImages() {
        List<Integer> mBannerImages = new ArrayList<>();
        mBannerImages.add(R.mipmap.banner1);
        mBannerImages.add(R.mipmap.banner2);
        mBannerImages.add(R.mipmap.banner3);
        mBannerImages.add(R.mipmap.banner4);
        mBannerImages.add(R.mipmap.banner5);
        mBannerImages.add(R.mipmap.banner6);
        return mBannerImages;
    }
複製代碼

另外若是想增長體驗的話,能夠在生命週期的 onStart 方法中開啓自動播放,在 onStop 方法中關閉自動播放。

@Override
    public void onStart() {
        super.onStart();
        //增長banner的體驗
        banner.startAutoPlay();
    }

    @Override
    public void onStop() {
        super.onStop();
        //增長banner的體驗
        banner.stopAutoPlay();
    }
複製代碼

Banner 的官方文檔中有詳細使用方法。

2. 大模塊 BigModule 實現

在主頁佈局中用一個 LinearLayout 做爲佔位,並肯定這個模塊的位置。具體的內容在代碼中動態添加,方便後期維護修改。 由於作了高度的封裝,因此代碼多些,但用起來很方便。 先上代碼吧:

HomeFragment 是 View 層,按 MVP 分層思想,不該包含具體的邏輯,因此只向外暴露一個共有方法,用於添加自定義 View IconTitleView 到 佔位的 LinearLayout

/**
     * 往根佈局上添加View
     */
    @Override
    public void addViewToBigModule(IconTitleView iconTitleView) {
        llBigModule.addView(iconTitleView);
    }
複製代碼

具體的添加邏輯在 HomeFragmentPresenter 中:

//大模塊的圖片數組
    private static final int[] bigModuleDrawables = {
            R.mipmap.homepage_icon_light_food_b,
            R.mipmap.homepage_icon_light_movie_b,
            R.mipmap.homepage_icon_light_hotel_b,
            R.mipmap.homepage_icon_light_amusement_b,
            R.mipmap.homepage_icon_light_takeout_b,
    };

    //大模塊的標題數組
    private static final String[] bigMudoleTitles = {
            "美食", "電影/演出", "酒店住宿", "休閒娛樂", "外賣"
    };

    /**
     * 初始化banner下面的5個大模塊
     */
    private void initBigModule() {
        for (int i = 0; i < 5; i++) {
            IconTitleView iconTitleView = IconTitleView.newInstance(mContext, bigModuleDrawables[i], bigMudoleTitles[i]);
            // 設置寬高和權重weight,使每一個View佔用相同的寬度
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
            iconTitleView.setLayoutParams(lp);

            // 往根佈局上添加View
            mFragment.addViewToBigModule(iconTitleView);

            //給View添加點擊事件
            int finalI = i;
            iconTitleView.setOnClickListener((view) -> {
                Logger.d(bigMudoleTitles[finalI]);
                ToastUtils.show(bigMudoleTitles[finalI]);
            });
        }
    }
複製代碼

圖片和對應的文字都是寫好的,分別放在 bigModuleDrawablesbigMudoleTitles 數組中。 這個模塊放了五個圖標,因此用了 for 循環五次,每次按下標取出上面兩個數組中存入的圖片和文字資源,經過

IconTitleView iconTitleView = IconTitleView.newInstance(mContext, bigModuleDrawables[i], bigMudoleTitles[i]);
複製代碼

實例化一個 IconTitleView 對象,並添加到 LinearLayout上

// 往根佈局上添加View
mFragment.addViewToBigModule(iconTitleView);
複製代碼

注意這幾行代碼:

// 設置寬高和權重weight,使每一個View佔用相同的寬度
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.WRAP_CONTENT,
        LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
iconTitleView.setLayoutParams(lp);
複製代碼

必定要給每一個 iconTitleView 設置權重,這樣纔會讓5個圖標占用相同的寬度。

自定義 View IconTitleView 的實現:

package com.cachecats.meituan.widget;

import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.cachecats.meituan.R;

import butterknife.BindView;
import butterknife.ButterKnife;

/**
 * 上圖片下標題的簡單分模塊佈局自定義View
 */

public class IconTitleView extends LinearLayout {

    @BindView(R.id.iv_icon_title)
    ImageView iv;
    @BindView(R.id.tv_icon_title)
    TextView tv;

    private Context context;

    public IconTitleView(Context context) {
        this(context, null, 0);
        this.context = context;
    }

    public IconTitleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public IconTitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        View view = View.inflate(context, R.layout.view_icon_title, this);
        ButterKnife.bind(view);

    }

    public static IconTitleView newInstance(Context context, int imageResource, String title) {
        IconTitleView iconTitleView = new IconTitleView(context);
        iconTitleView.setImageView(imageResource);
        iconTitleView.setTitleText(title);
        return iconTitleView;
    }

    private void setImageView(int drawable) {
        Glide.with(context).load(drawable).into(iv);
    }

    private void setTitleText(String title) {
        tv.setText(title);
    }
}
複製代碼

IconTitleView的佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:gravity="center"
    >

    <ImageView
        android:id="@+id/iv_icon_title"
        android:layout_width="50dp"
        android:layout_height="50dp"
        />

    <TextView
        android:id="@+id/tv_icon_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/gray666"
        android:textSize="12sp"
        />

</LinearLayout>
複製代碼

這個是組合自定義View,比較簡單,就很少說啦。

3. 兩行圖標的小模塊 SmallModule

RecyclerView 實現的 GridView 佈局,直接上代碼吧。

/**
     * 初始化小模塊的RecyclerView
     */
    private void initLittleModuleRecyclerView() {

        GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 5);
        //設置LayoutManager
        littleModuleRecyclerView.setLayoutManager(gridLayoutManager);
        //設置分割器
        littleModuleRecyclerView.addItemDecoration(new HomeGridDecoration(12));
        //設置動畫
        littleModuleRecyclerView.setItemAnimator(new DefaultItemAnimator());

        //設置Adapter
        List<IconTitleModel> iconTitleModels = presenter.getIconTitleModels();
        LittleModuleAdapter littleModuleAdapter = new LittleModuleAdapter(
                R.layout.view_icon_title_small, iconTitleModels);

        littleModuleRecyclerView.setAdapter(littleModuleAdapter);
        //設置item點擊事件
        littleModuleAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
                ToastUtils.show(iconTitleModels.get(position).getTitle());
            }
        });

    }
複製代碼

LittleModuleAdapter.java

public class LittleModuleAdapter extends BaseQuickAdapter<IconTitleModel, BaseViewHolder> {

    private List<IconTitleModel> list;

    public LittleModuleAdapter(int layoutResId, @Nullable List<IconTitleModel> data) {
        super(layoutResId, data);
        list = data;
    }

    @Override
    protected void convert(BaseViewHolder helper, IconTitleModel item) {
        //設置圖片
        helper.setImageResource(R.id.iv_icon_title, item.getIconResource());
        //設置標題
        helper.setText(R.id.tv_icon_title, item.getTitle());
    }
}
複製代碼

都是 RecyclerView 的基本知識,就再也不贅述了。

4. 四個廣告封裝的 HomeAdsView

HomeAdsView.java

public class HomeAdsView extends LinearLayout {

    @BindView(R.id.ads_1)
    ImageView ads1;
    @BindView(R.id.ads_2)
    ImageView ads2;
    @BindView(R.id.ads_3)
    ImageView ads3;
    @BindView(R.id.ads_4)
    ImageView ads4;

    public HomeAdsView(Context context) {
        this(context, null, 0);
    }

    public HomeAdsView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HomeAdsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.view_home_ads, this);
        ButterKnife.bind(view);
    }

    @OnClick({R.id.ads_1, R.id.ads_2, R.id.ads_3, R.id.ads_4})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.ads_1:
                onAdsClickListener.onAds1Click();
                break;
            case R.id.ads_2:
                onAdsClickListener.onAds2Click();
                break;
            case R.id.ads_3:
                onAdsClickListener.onAds3Click();
                break;
            case R.id.ads_4:
                onAdsClickListener.onAds4Click();
                break;
        }
    }

    /**
     * 設置廣告的資源id,從左到右從上到下依次排列
     * 加載本地圖片
     *
     * @param list
     */
    public void setAdsResource(List<Integer> list) {
        if (list == null || list.size() != 4) {
            return;
        }
        Glide.with(this).load(list.get(0)).into(ads1);
        Glide.with(this).load(list.get(1)).into(ads2);
        Glide.with(this).load(list.get(2)).into(ads3);
        Glide.with(this).load(list.get(3)).into(ads4);
    }

    /**
     * 設置廣告的資源id,從左到右從上到下依次排列
     * 加載網絡圖片
     *
     * @param list
     */
    public void setAdsUrl(List<String> list) {
        if (list == null || list.size() != 4) {
            return;
        }
        Glide.with(this).load(list.get(0)).into(ads1);
        Glide.with(this).load(list.get(1)).into(ads2);
        Glide.with(this).load(list.get(2)).into(ads3);
        Glide.with(this).load(list.get(3)).into(ads4);
    }

    private OnAdsClickListener onAdsClickListener;

    public interface OnAdsClickListener {
        void onAds1Click();

        void onAds2Click();

        void onAds3Click();

        void onAds4Click();
    }

    public void setOnAdsClickListener(OnAdsClickListener onAdsClickListener) {
        this.onAdsClickListener = onAdsClickListener;
    }
}
複製代碼

view_home_ads.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/ads_1"
        android:layout_width="120dp"
        android:layout_height="240dp"
        android:src="@mipmap/ads_1"
        android:layout_margin="2dp"
        android:scaleType="fitStart"
        />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="240dp"
        android:layout_weight="1"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/ads_2"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:src="@mipmap/ads_2"
            android:layout_margin="2dp"
            android:scaleType="fitStart"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">

            <ImageView
                android:id="@+id/ads_3"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:src="@mipmap/ads_3"
                android:layout_margin="2dp"
                android:scaleType="fitStart"
                />

            <ImageView
                android:id="@+id/ads_4"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:src="@mipmap/ads_4"
                android:layout_margin="2dp"
                android:scaleType="fitStart"
                />

        </LinearLayout>
    </LinearLayout>
</LinearLayout>
複製代碼

向外暴露設置圖片資源和Url地址的方法,並提供點擊事件接口。用起來很簡單:

private void initAds() {
        homeAdsView.setOnAdsClickListener(new HomeAdsView.OnAdsClickListener() {
            @Override
            public void onAds1Click() {
                ToastUtils.show("Ads1");
            }

            @Override
            public void onAds2Click() {
                ToastUtils.show("Ads2");
            }

            @Override
            public void onAds3Click() {
                ToastUtils.show("Ads3");
            }

            @Override
            public void onAds4Click() {
                ToastUtils.show("Ads4");
            }
        });
    }
複製代碼

由於圖片是寫死的,這裏只實現了點擊事件回調。

5.團購信息列表

這個也是個普通的 RecyclerView,裏面牽扯到數據庫操做,就不在這裏貼代碼啦。 注意個問題,RecyclerViewScrollView 滑動會有衝突,須要特殊處理下,處理方法:

LinearLayoutManager lm = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false) {
            @Override
            public boolean canScrollVertically() {
                return false;
            }
        };
 rvShopList.setLayoutManager(lm);
複製代碼

經過設置 LinearLayoutManager 禁止RecyclerView 垂直方向上滑動。

6.下拉刷新加載更多

SmartRefreshLayout 實現的,它的官方文檔寫的很詳細,本文重點在於解讀主頁,具體框架使用就很少說啦。


以上就是對美團首頁佈局分析及實現的過程,前四個模塊說的比較詳細,牽扯到自定義View的封裝。其實不封裝直接寫也行,但爲了後期維護起來不被人罵,仍是多花點精力封裝下吧。 團購信息列表和下拉刷新主要是普通的 RecyclerView 用法和框架整合,這類文章比較多,不明白的能夠自行查閱相關資料。

源碼地址:github.com/cachecats/L…
歡迎下載,歡迎 star,歡迎點贊~

相關文章
相關標籤/搜索