這是【從零擼美團】系列文章第四篇。 專案地址:github.com/cachecats/L…
Android從零擼美團(一) - 統一管理 Gradle 依賴 提取到單獨檔案中
Android從零擼美團(二) - 仿美團下拉重新整理自定義動畫
Android從零擼美團(三) - Android多標籤tab滑動切換 - 自定義View快速實現高度定製封裝
仿美團開源專案整體架構和首頁其實早就完成了,前段時間家裡各種事情搞得心力交瘁,停更了一段時間。甚至一度動搖繼續這個專案的決心,因為最近在學前端,在技術的深度和廣度之間一直糾結搖擺不定。一個聲音是繼續完成這個專案,把安卓玩的更深入一些;另一個聲音是趕緊學前端吧,抓緊擴充技術棧,不要在這個專案上浪費太多精力。
思來想去還是繼續完成專案吧,自己開的專案跪著也要走完 〒▽〒
最後確定了繼續寫專案和學前端同時進行的戰略方針~
老規矩,先上圖,再 分析原理 --> 準備材料 --> 具體實現 三步走一步步的搞定。
一、分析
相比於普通的應用,美團、去哪兒這樣的平臺性 App 的首頁還是相當複雜的,簡直想把全世界都包進去~
剛開始看可能覺得眼花繚亂,但仔細觀察,可以把它抽象成六個模組:
- 最上面的輪播廣告條,裡面包含若干個廣告圖片自動無限輪播。暫時稱之為 Banner(注意這幾個模組起的英文名對應著程式碼中的模組名)。
- 輪播條下面的美食、電影/演出、酒店住宿、休閒娛樂、外賣等五個大模組入口,暫時稱之為大模組 BigModule。
- 再往下類似 GridView 的兩排小圖示,KTV、周邊遊……暫時稱之為小模組 SmallModule。
- 小模組下面四張廣告圖片,乍一看是沒有規則的瀑布佈局,其實是互相對齊的簡單規則佈局。暫時稱之為 HomeAdsView。
- 最後就是列表 RecyclerView 了,顯示附近團購資訊。
- 還有一個不太明顯的,上拉重新整理下拉載入更多,也算一個模組吧。
抽絲剝繭後就是這六個模組啦,是不是一下清爽很多?
實現思路
輪播條選用了第三方的庫:Banner, 有 5.2k 顆 star,非常優秀的庫。
大模組 BigModule 採用程式碼中動態新增 View 的方式實現,好處在於能快速響應變化,假如需求變成一行放4個圖示,只需要在 java 檔案中改一句程式碼就好,不用修改資原始檔。
兩行小模組 SmallModule 是 RecyclerView 實現的 GridView。
四張廣告圖片 HomeAdsView 是封裝的自定義 View,高度封裝優點是完全解耦,簡化了主頁的佈局,使用配置簡單,後期維護方便。
最下面的列表用的是 RecyclerView,BaseRecyclerViewAdapterHelper 作為輔助。
下拉重新整理元件用的是 SmartRefreshLayout
二、準備
主頁中用到了三個框架,在 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…
三、實現
專案採用 MVP 架構,主頁程式碼在 app/home
目錄下的 HomeFragment
和 HomeFragmentPresenter
中。
佈局檔案是 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]);
});
}
}
複製程式碼
圖片和對應的文字都是寫好的,分別放在 bigModuleDrawables
和 bigMudoleTitles
陣列中。
這個模組放了五個圖示,所以用了 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
,裡面牽扯到資料庫操作,就不在這裡貼程式碼啦。
注意個問題,RecyclerView
和 ScrollView
滑動會有衝突,需要特殊處理下,處理方法:
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
,歡迎點贊~