版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/sinat_25074703/article/details/82981668
经过构建基础代码框架、创建StaggeredAdapter适配器、实现空界面效果之后,瀑布流的实现只是一个在StaggeredAdapter中添加ViewType类型的问题,这个类型就是FORMAL_ITEM对应正常的服务器数据,当然创建新的ViewHolder也是必须的。
7、实现StaggeredFormalViewHolder完成正常数据展示
7.1 创建StaggeredFormalViewHolder
万事开头难,先从简单的布局出发,这是只有一张商品图片和一个商品描述的简单展示,采用垂直线性布局,如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/product_img"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:layout_gravity="center_horizontal"
android:scaleType="centerCrop"
android:src="@drawable/staggered_formal_img"/>
<TextView
android:id="@+id/description_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="Formal Staggered Item"
android:textSize="14sp"
android:textColor="@android:color/holo_blue_dark"/>
</LinearLayout>
与创建StaggeredEmptyViewHolder一样,右击staggered包创建StaggeredFormalViewHolder,并加载staggered_formal_item.xml文件,代码如下:
package com.edwin.idea.staggered;
import android.content.Context;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import com.edwin.idea.R;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredFormalViewHolder extends RecyclerView.ViewHolder {
private ImageView productImg;
private TextView descriptionText;
private Context mContext;
public static StaggeredFormalViewHolder newInstance(ViewGroup viewGroup){
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.staggered_formal_item, viewGroup, false); // 注意:第三个参数是false,否则会异常,详见6.1节
return new StaggeredFormalViewHolder(itemView);
}
private StaggeredFormalViewHolder(View itemView) {
super(itemView);
// 注意:mContext不要在newInstance方法里赋值,会造成内存泄漏。
mContext = itemView.getContext();
productImg = (ImageView) itemView.findViewById(R.id.product_img);
int width = getScreenWidthPx();
// 注意:每行布局有2列,以下两行代码的意思是,
// 每个图片的宽度为w = width /2 - marginLeft - marginRight,marginLeft == marginRight == 5
int margin = dip2px(mContext, 5 * 4);
int w = (width - margin) / 2;
ViewGroup.LayoutParams layoutParams = productImg.getLayoutParams();
layoutParams.width = w;
layoutParams.height = w;
productImg.setLayoutParams(layoutParams);
descriptionText = (TextView) itemView.findViewById(R.id.description_text);
}
private int getScreenWidthPx() {
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
private int getScreenHeightPx() {
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.heightPixels;
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 sp 的单位 转成为 px(像素)
*/
public static int sp2px(Context context, float spValue) {
final float scale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 在对itemView的childView添加点击监听时,可以将v.getContext()传入,因为从23.3.0开始它不再返回Activity,而是返回
* TintContextWrapper
* https://stackoverflow.com/questions/38814267/android-support-v7-widget-tintcontextwrapper-cannot-be-cast
* @param context
* @return
*/
private Activity getActivity(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
}
文件中的有几个工具方法是我们此次项目中用不到的,但是为了避免在工作中需要用到类似方法,一并添加到该文件中,方便随时使用。这里面还有一个大问题,就是图片显示使用的是ImageView,在我们的demo中这不会有什么问题,但是在实际的开发中,直接从服务器下载的图片是不可控的,大的可以达到上百兆,甚至几个G都是有可能的,这样必然引发OOM,当然一些第三方库已经帮我们解决了这个问题,后期再做介绍。
7.2创建model模型StaggeredVO,以适配服务器数据
StaggeredFormalViewHolder是为了展示服务器正常数据的,由于各个公司的服务器返回结构都是自定义的,其结构页千差万别,我们本章的目的是实现瀑布流效果,当然还有一个比较重要的原因是我也不擅长搭建服务器 ???。所以,这里直接模拟服务器最终返回结构,根据我们第一章提到的目标,我们最终至少要拿到3个元素:图片和文本描述,当然还有标签。他们对应三个字段,目前itemView未展示标签!在staggered文件夹下创建StaggeredVO.java文件,其代码如下:
package com.edwin.idea.staggered;
import java.io.Serializable;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredVO implements Serializable {
private String price;
private String description;
private String imgUrl;
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}
}
看到Serializable接口了吗,它是用来实现对象的序列化的,在开发中我们如果要使用Intent或Binder传递某个对象时,就必须对其进行序列号,否则会报异常。
有了钞票模板可以印钞,有了服务器数据模型就可以模拟数据,这个过程我们选择在StaggeredFragment.java文件中的initData中完成。
7.3模拟服务器数据
首先在StaggeredFragment.java文件中声明一个list泛型列表,如下:
List<StaggeredVO> list;
StaggeredVO模板是List列表的泛型类型,保证创建的数据都是符合要求的,比如我们只要美元。然后在initData方法中模拟50条服务器数据,如下:
@Override
protected void initData() {
list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
StaggeredVO staggeredVO = new StaggeredVO();
staggeredVO.setPrice("$ " + i); // 注意这里是有价格的,只是没显示
if (i % 2 == 1) {
staggeredVO.setDescription("Staggered Formal Item " + i);
}
list.add(staggeredVO);
}
}
这样,服务器数据就模拟结束了,接下来完成数据的展示工作吧!
7.4完成正常数据展示,实现瀑布流效果
在本章开篇时提到,瀑布流的实现只是一个在StaggeredAdapter中添加新的ViewType类型的问题,这个新的类型和服务器数据模板StaggeredVO、StaggeredFormalViewHolder、每一条模拟数据时一一对应的。
首先,在StaggeredAdapter.java中创建静态常量FORMAL_ITEM对应StaggeredFormalViewHolder,代码如下:
private static final int FORMAL_ITEM = 1000;
private static final int EMPTY_ITEM = 1001;
当然,我们之前已经创建了一个空的ItemType类型,对应代码中的EMPTY_ITEM,那么如何区分这两个类型呢?
还是要从空的情况入手,通常有如下两个条件:
- list == null; 代码中没有对list 赋值,比如在StaggeredFragment文件中initData()是空实现
- list != null, 但是list.size() == 0;服务器什么都没返回
这样,我们区分开了这两个类型,就可以添加新的ViewType了。
还记得 6.2 节空数据展示时提到的RecyclerView.Adapter的4个方法的实现周期吗?如下图:
接着,我们就根据这四个方法的加载顺序来添加FORMAL_ITEM。
在StaggeredAdapter.java中声明list列表代表服务器数据,代码如下:
private List<StaggeredVO> list = new ArrayList<>(50);
修改getItemCount()方法,将上面的1、2条件引入,代码如下:
@Override
public int getItemCount() {
Log.d(TAG, "=====getItemCount");
if (list == null || list.size() == 0) {
// 空也要占一个item
return 1;
} else {
return list.size();
}
}
修改getItemViewType方法,同样引入1、2条件,代码如下:
@Override
public int getItemViewType(int position) {
Log.d(TAG, "===getItemViewType");
if (list == null || list.size() == 0) {
// 空也要占一个item
return EMPTY_ITEM;
} else {
return FORMAL_ITEM;
}
}
注意,这里的position对应当前加载的1条服务器数据,对此你有什么想法吗?通过调用list.get(position)会有美妙的未来
修改onCreateViewHolder方法,代码如下:
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.d(TAG, "======onCreateViewHolder");
if (viewType == FORMAL_ITEM) {
return StaggeredFormalViewHolder.newInstance(parent);
}
return StaggeredEmptyViewHolder.newInstance(parent);
}
由于getItemViewType已经返回了不同的ViewType类型,这里就不必引入条件1、2了。如果再添加类型,只需要加else if条件返回对应ViewHolder就可以了,你想到这一层了吗?
修改onBindViewHolder,还记得它的作用吗?四个字:滑动更新。代码如下:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
Log.d(TAG, "======onBindViewHolder");
StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (list == null || list.size() == 0) {
// 空也要占一个item
layoutParams.setFullSpan(true); ((StaggeredEmptyViewHolder)holder).itemView.setLayoutParams(layoutParams);
} else {
layoutParams.setFullSpan(false);
((StaggeredFormalViewHolder)holder).onBind(list.get(position));
}
}
最后一行代码报异常了,原因是我们并未在StaggeredFormalViewHolder创建onBind()方法。这里有两点需要注意:
- position对应的是当前某条服务器数据
- onBindViewHolder更新的是整个itemView的UI
基于以上两点,我们需要将整条数据传给StaggeredFormalViewHolder,每一条数据对应一个StaggeredVO实例,那么在StaggeredFormalViewHolder.java文件中添加onBind(StaggeredVO vo)方法如下:
/**
* 刷新itemView并对其子view填充数据
* @param vo
*/
public void onBind(StaggeredVO vo) {
if (!TextUtils.isEmpty(vo.getImgUrl())) {
productImg.setImageURI(Uri.parse(vo.getImgUrl()));
}
if (!TextUtils.isEmpty(vo.getDescription())) {
descriptionText.setText(vo.getDescription());
descriptionText.setVisibility(View.VISIBLE);
} else {
descriptionText.setVisibility(View.GONE);
}
}
StaggeredFormalViewHolder.java的代码如下:
package com.edwin.idea.staggered;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import com.edwin.idea.R;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredFormalViewHolder extends RecyclerView.ViewHolder {
private ImageView productImg;
private TextView descriptionText;
private Context mContext;
public static StaggeredFormalViewHolder newInstance(ViewGroup viewGroup){
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.staggered_formal_item, viewGroup, false);
return new StaggeredFormalViewHolder(itemView);
}
private StaggeredFormalViewHolder(View itemView) {
super(itemView);
mContext = itemView.getContext();
productImg = (ImageView) itemView.findViewById(R.id.product_img);
int width = getScreenWidthPx();
int margin = dip2px(mContext, 5 * 4);
int w = (width - margin) / 2;
ViewGroup.LayoutParams layoutParams = productImg.getLayoutParams();
layoutParams.width = w;
layoutParams.height = w;
productImg.setLayoutParams(layoutParams);
descriptionText = (TextView) itemView.findViewById(R.id.description_text);
}
/**
* 刷新itemView并对其子view填充数据
* @param vo
*/
public void onBind(StaggeredVO vo) {
if (!TextUtils.isEmpty(vo.getImgUrl())) {
productImg.setImageURI(Uri.parse(vo.getImgUrl()));
}
if (!TextUtils.isEmpty(vo.getDescription())) {
descriptionText.setText(vo.getDescription());
descriptionText.setVisibility(View.VISIBLE);
} else {
descriptionText.setVisibility(View.GONE);
}
}
private int getScreenWidthPx() {
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
private int getScreenHeightPx() {
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.heightPixels;
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 sp 的单位 转成为 px(像素)
*/
public static int sp2px(Context context, float spValue) {
final float scale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 在对itemView的childView添加点击监听时,可以将v.getContext()传入,因为从23.3.0开始它不再返回Activity,而是返回
* TintContextWrapper
* https://stackoverflow.com/questions/38814267/android-support-v7-widget-tintcontextwrapper-cannot-be-cast
* @param context
* @return
*/
private Activity getActivity(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
}
修改了以上4个方法解决了异常之后,我们只需要将服务器模拟数据list设置给adapter的list就可以完成数据展示了。这是多么大的一个难题啊,我突然间不知道怎么完成了,你会吗?
好像是在StaggeredAdapter类中添加一个setData方法,然后在StaggeredFragment中调用它就好了。
在StaggeredAdapter类中,选中list字段Command + N(或者右键鼠标,点击Generate),在弹出的GenerateMenu列表中选中Setter选项,产生setList方法,将setList改成setData即可。
private List<StaggeredVO> list = new ArrayList<>(50);
/**
* 将服务器数据设置给Adapter
* @param list
*/
public void setData(List<StaggeredVO> list) {
this.list = list;
}
修改StaggeredFormalViewHolder.java后的代码如下:
package com.edwin.idea.staggered;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredAdapter extends RecyclerView.Adapter {
private static final String TAG = StaggeredAdapter.class.getSimpleName();
private static final int FORMAL_ITEM = 1000;
private static final int EMPTY_ITEM = 1001;
private List<StaggeredVO> list = new ArrayList<>(50);
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.d(TAG, "======onCreateViewHolder");
if (viewType == FORMAL_ITEM) {
return StaggeredFormalViewHolder.newInstance(parent);
}
return StaggeredEmptyViewHolder.newInstance(parent);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
Log.d(TAG, "======onBindViewHolder");
StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (list == null || list.size() == 0) {
// 空也要占一个item
layoutParams.setFullSpan(true);
((StaggeredEmptyViewHolder)holder).itemView.setLayoutParams(layoutParams);
} else {
layoutParams.setFullSpan(false);
// 这里的itemView是RecyclerView.ViewHolder的final public字段,可以直接使用
// 它就是StaggeredFormalViewHolder对应布局文件staggered_formal_item.xml的LinearLayout
((StaggeredFormalViewHolder) holder).itemView.setSelected(position == selectedIndex);
((StaggeredFormalViewHolder)holder).onBind(list.get(position));
}
}
@Override
public int getItemCount() {
Log.d(TAG, "=====getItemCount");
if (list == null || list.size() == 0) {
// 空也要占一个item
return 1;
} else {
return list.size();
}
}
@Override
public int getItemViewType(int position) {
Log.d(TAG, "===getItemViewType");
if (list == null || list.size() == 0) {
// 空也要占一个item
return EMPTY_ITEM;
} else {
return FORMAL_ITEM;
}
}
/**
* 将服务器数据设置给Adapter
* @param list
*/
public void setData(List<StaggeredVO> list) {
this.list = list;
}
}
最后,在StaggeredFragment.java的onCreate()方法中调用setData(list)将服务器数据设置给StaggeredAdapter代码如下:
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewGroupRoot = (ViewGroup) inflater.inflate(R.layout.fragment_staggered, null);
recyclerView = (RecyclerView) viewGroupRoot.findViewById(R.id.recycler_view);
staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
staggeredAdapter = new StaggeredAdapter();
initData();//别忘了调用initData()模拟服务器数据,实际中是从服务器拿来的
staggeredAdapter.setData(list);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
recyclerView.setAdapter(staggeredAdapter);
return viewGroupRoot;
}
注意:别忘了调用initData()模拟服务器数据,否则显示的是空页面
StaggeredFragment.java代码如下:
package com.edwin.idea.staggered;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.edwin.idea.AbstractFragment;
import com.edwin.idea.R;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Edwin,CHEN on 2018/9/18.
*/
public class StaggeredFragment extends AbstractFragment {
ViewGroup viewGroupRoot;
RecyclerView recyclerView;
StaggeredGridLayoutManager staggeredGridLayoutManager;
StaggeredAdapter staggeredAdapter;
List<StaggeredVO> list;
public static Fragment newInstance(){
return new StaggeredFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewGroupRoot = (ViewGroup) inflater.inflate(R.layout.fragment_staggered, null);
recyclerView = (RecyclerView) viewGroupRoot.findViewById(R.id.recycler_view);
staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
staggeredAdapter = new StaggeredAdapter();
initData();
staggeredAdapter.setData(list);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
recyclerView.setAdapter(staggeredAdapter);
return viewGroupRoot;
}
@Override
protected void initData() {
list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
StaggeredVO staggeredVO = new StaggeredVO();
staggeredVO.setPrice("$ " + i);
if (i % 2 == 1) {
staggeredVO.setDescription("Staggered Formal Item " + i);
}
list.add(staggeredVO);
}
}
}
好了,Ctrl + R运行一下吧,下图应该就是你心心念的瀑布流效果了~
7.6 添加点击事件
对于事件开发中,每一个itemView的点击事件是必不可少的,其实实现起来相当简单。只需要在onBindViewHolder中对itemView添加OnClickListener监听事件,代码如下:
((StaggeredFormalViewHolder) holder).itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "position" + position, Toast.LENGTH_SHORT).show();
}
});
7.7 添加选中事件
有事为了凸显当前选中的itemView,做一些颜色上的特殊展示也是必须的,这个主要通过itemView.setSelected()方法来完成,该方法会触发view的invalidate完成重绘,更改后onBindViewHolder代码如下:
private int selectedIndex; // 添加当前被选中index
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
Log.d(TAG, "======onBindViewHolder");
StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (list == null || list.size() == 0) {
// 空也要占一个item
layoutParams.setFullSpan(true);
((StaggeredEmptyViewHolder) holder).itemView.setSelected(position == selectedIndex);
((StaggeredEmptyViewHolder)holder).itemView.setLayoutParams(layoutParams);
} else {
layoutParams.setFullSpan(false);
((StaggeredFormalViewHolder) holder).itemView.setSelected(position == selectedIndex);
((StaggeredFormalViewHolder)holder).onBind(list.get(position));
((StaggeredFormalViewHolder) holder).itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (selectedIndex != position) {
selectedIndex = position;
}
Toast.makeText(v.getContext(), "position = " + position, Toast.LENGTH_SHORT).show();
}
});
}
}
可以通过更改itemView的背景色来完成验证哦~
有诗云: