前段时间有朋友问我怎么使用DelegationAdapter实现每日优鲜的一种效果,安装每日优鲜之后发现一个比较炫酷的效果,如下图所示。
通过查看它的布局发现,整体是使用RecyclerView
来实现的,头部的“平价好菜”是一个条目,下面的每种菜也是一个条目。那么“洋葱”和“油菜”后面的背景是怎么设置上去的呢?
思路
通过以上可以看到,背景不在任意一个条目上,是画在RecyclerView
上面的。画在RecyclerView上
的,大家想到了什么?没错就是ItemDecoration
,可能有的朋友不太了解ItemDecoration
,这里我简单的说下。
ItemDecoration
ItemDecoration
通过字面意思就可以理解到,是条目装饰,即可以在条目底层、上层绘制一些装饰物,来看一下都提供了哪些方法。
public abstract static class ItemDecoration {
// 在条目绘制之前绘制装饰,即在RecyclerView条目的底层展示。
public void onDraw(Canvas c, RecyclerView parent, State state) {
}
// 在条目绘制之后绘制装饰,即在RecyclerView条目的上层展示。
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
}
// 重新设置条目的边距
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
}
}
是不是非常简单,仅仅提供了三个方法,重新设置条目边距,在条目底层绘制,在条目上层绘制。比如这里的,我们要重新设置蔬菜条目的边距,这样“洋葱”条目下面的内容才会被看到,否则我们在“洋葱”条目底层绘制了装饰也无法看到。
关于ItemDecoration
的具体讲解,网上非常多,这里不展开来讲,不太了解的朋友可以先去查下资料。
实现
既然我们有了一种实现方案,那么就开始搞吧。俗话说,兵马未动,粮草先行。这里我们要现有接口数据以及实体Bean对象,这是根基,否则后面会寸步难行。
接口数据格式
定义接口数据,最简单的方式是什么呢?向每日优鲜致敬,我管抓别人接口、反编译别人的APP叫致敬,为什么是致敬呢?我理解是从别人那获取方案,是向别人学习,要怀有敬畏和感恩。那么,我们就抓它的接口吧。
去除标识响应状态,额外的条目数据之后,精简下接口大致这样:
{
"cellList": [
{
"bgImage": "https://j-image.missfresh.cn/mis_img_20190313192600772.png?mryxw=1125&mryxh=852",
"cellType": 9,
"secondBanner": {
"path": "https://j-image.missfresh.cn/mis_img_20190313192555728.png?mryxw=1125&mryxh=270"
}
},
{
"cellType": 7,
"normalProduct": {
"cartImage": "https://j-image.missfresh.cn/img_20170425134548759.png",
"image": "https://fms-image.missfresh.cn/3bfd39e84df242d8a24e405be3f16705.jpg",
"name": "樱桃小番茄500g*1盒",
"subtitle": "点亮沙拉的红色甜心",
"price": 490
}
},
{
"cellType": 7,
"normalProduct": {
"cartImage": "https://j-image.missfresh.cn/img_20170425134548759.png",
"image": "https://image.missfresh.cn/6ca7c143db584168860cf7bd0a758fed.jpg",
"name": "平价胡萝卜500g",
"subtitle": "兔仙女都爱胡萝北",
"price": 250
}
}
]
}
通过观察接口我们发现,有个cellType
字段用于标识条目类型,如果cellType=9
就是上面的整条样式,如果cellType=7
就是下面的蔬菜条目样式。既然有了数据,那么实体Bean就很简单了。
public class Products {
public List<CellItem> cellList;
public static class CellItem {
public int cellType;
public SecondBanner secondBanner;
public String bgImage;
public NormalProduct normalProduct;
}
public static class SecondBanner {
public String path;
}
public static class NormalProduct {
public String image;
public String name;
public String subtitle;
public int price;
public String cartImage;
}
}
RecyclerView架子
activity_main
布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#f8f8f8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Adapter编写
通过上面的实体Bean定义,我们知道这里有两种类型的条目,并且是通过cellType
进行区分的,我们可以通过getItemViewType
返回不同的类型进行区分。
public class ProductsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Products.CellItem> dataItems = new ArrayList<>();
public void setProducts(List<Products.CellItem> dataItems) {
this.dataItems = dataItems;
notifyDataSetChanged();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
}
@Override
public int getItemViewType(int position) {
return dataItems.get(position).cellType;
}
@Override
public int getItemCount() {
return dataItems.size();
}
}
接下来就是onCreateViewHolder
,在不同的ViewType
的时候需要有不同的ViewHolder
。
再把这个图拿过来,顶部的“评价好菜”比较简单,就一张图片。
layout_second_banner_item
布局
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iv_image"
android:layout_width="match_parent"
android:layout_height="95dp"
android:scaleType="fitXY" />
SecondBannerViewHolder
代码
public class SecondBannerViewHolder extends RecyclerView.ViewHolder {
ImageView image;
public SecondBannerViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.iv_image);
}
}
layout_normal_product_item
布局,也不是特别复杂。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="257dp"
android:background="@drawable/shape_normal_product_bg">
<ImageView
android:id="@+id/iv_image"
android:layout_width="138dp"
android:layout_height="138dp"
android:layout_marginTop="18dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="9dp"
android:layout_marginRight="10dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="#ff474245"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_image" />
<TextView
android:id="@+id/tv_sub_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="3dp"
android:layout_marginRight="10dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="#ff969696"
android:textSize="12.0sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_name" />
<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="8dp"
android:ellipsize="start"
android:includeFontPadding="false"
android:singleLine="true"
android:textColor="#f44089"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_sub_title" />
<ImageView
android:id="@+id/iv_cart"
android:layout_width="49dp"
android:layout_height="49dp"
android:layout_marginTop="6dp"
android:layout_marginRight="10dp"
android:scaleType="fitXY"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_sub_title" />
</android.support.constraint.ConstraintLayout>
NormalProductViewHolder
代码
public class NormalProductViewHolder extends RecyclerView.ViewHolder {
ImageView image;
TextView name;
TextView subTitle;
TextView price;
ImageView cart;
public NormalProductViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.iv_image);
name = itemView.findViewById(R.id.tv_name);
subTitle = itemView.findViewById(R.id.tv_sub_title);
price = itemView.findViewById(R.id.tv_price);
cart = itemView.findViewById(R.id.iv_cart);
}
}
OK,这两种类型的ViewHolder
就算完成了,剩下的只要在onCreateViewHolder
中调用就行了。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 9) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_second_banner_item, parent, false);
return new SecondBannerViewHolder(view);
} else if (viewType == 7) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_normal_product_item, parent, false);
return new NormalProductViewHolder(view);
} else {
// 不可能到达
return null;
}
}
剩下的就是绑定数据onBindViewHolder
,两种对应的类型绑定对应的数据。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Context context = holder.itemView.getContext();
Products.CellItem item = dataItems.get(position);
if (holder instanceof SecondBannerViewHolder) {
Glide.with(context)
.load(item.secondBanner.path)
.into(((SecondBannerViewHolder) holder).image);
} else if (holder instanceof NormalProductViewHolder) {
NormalProductViewHolder normalProductHolder = (NormalProductViewHolder) holder;
Glide.with(context)
.load(item.normalProduct.image)
.into(normalProductHolder.image);
normalProductHolder.name.setText(item.normalProduct.name);
normalProductHolder.subTitle.setText(item.normalProduct.subtitle);
normalProductHolder.price.setText("¥" + ((float) item.normalProduct.price / 100));
Glide.with(context)
.load(item.normalProduct.cartImage)
.into(normalProductHolder.cart);
}
}
在MainActivity中初始化RecyclerView以及数据
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recycler_view);
GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(layoutManager);
ProductsAdapter adapter = new ProductsAdapter();
recyclerView.setAdapter(adapter);
String productListStr = LocalFileUtils.getStringFormAsset(this, "products.json");
Products products = new Gson().fromJson(productListStr, Products.class);
adapter.setProducts(products.cellList);
}
}
满怀激动的运行了一把,咦,这是什么玩意儿。是我们没有设置cellType=9
是进行通栏显示。
设置cellType=9
通栏显示
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return adapter.getItemViewType(position) == 9 ? 2 : 1;
}
});
这样是不是有点那么回事了,这样我们的架子算是搭好了。
条目间距设置
通过我们上面搭的架子,发现是没有条目的分割线的,再把我们要实现的效果拿过来,通过肉眼查看,感觉蔬菜条目距离左右边距10dp,条目之间距离6dp。
BackgroundItemDecoration
条目装饰,设置蔬菜条目距离左右边距10dp,条目之间距离6dp,下方6dp。
public class BackgroundItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
Context context = view.getContext();
if (!isTargetItem(view, parent)) {
super.getItemOffsets(outRect, view, parent, state);
} else if (isLeftItem(view, parent)) {
outRect.set(dp2px(context, 10), 0, dp2px(context, 3), dp2px(context, 6));
} else {
outRect.set(dp2px(context, 3), 0, dp2px(context, 10), dp2px(context, 6));
}
}
/**
* 判断是否是蔬菜条目
*
* @param view
* @param parent
* @return
*/
private boolean isTargetItem(View view, RecyclerView parent) {
boolean target = false;
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter != null && adapter.getItemViewType(parent.getChildAdapterPosition(view)) == 7) {
target = true;
}
return target;
}
private boolean isLeftItem(View view, RecyclerView parent) {
boolean isLeft;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
int spanIndex = ((GridLayoutManager) layoutManager).getSpanSizeLookup()
.getSpanIndex(parent.getChildAdapterPosition(view),
((GridLayoutManager) layoutManager).getSpanCount());
isLeft = spanIndex == 0;
} else {
isLeft = true;
}
return isLeft;
}
private int dp2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
}
来看下效果,间距有啦,剩下的就是悲剧的绘制了。