在项目中app的build.gradle添加以下依赖:
//RefreshLayout
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0'
implementation 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.0'
//glide用于加载item中的图片
implementation 'com.github.bumptech.glide:glide:4.7.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'
首先来绘制一个简单的列表页布局:
<?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="vertical">
<com.scwang.smartrefresh.layout.SmartRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
</android.support.v7.widget.RecyclerView>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>
然后绘制一下RecycleView中需要加载的item布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="150dp" >
<RelativeLayout
android:id="@+id/notice"
android:layout_width="match_parent"
android:layout_height="140dp"
android:background="#FFFFFF">
<TextView
android:id="@+id/title"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:gravity="top"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:text="发布一条很重要的公告"
android:textColor="#000000"
android:textSize="20dp"/>
<TextView
android:id="@+id/description"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginLeft="10dp"
android:text="今天宿舍发布一条很重要的公告啊,我是公告的简介呢!今天宿舍发布一条很重要的公告啊,我是公告的简介呢!"
android:layout_below="@+id/title"
android:textSize="14dp"/>
<ImageView
android:id="@+id/img"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:scaleType="fitCenter"
>
</ImageView>
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="10dp"
android:text="2020/02/02 15:30"
android:textSize="16dp"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp">
</View>
</LinearLayout>
以下是item效果图:
在绘制一个提示没有数据的空页面布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<ImageView
android:layout_width="200dp"
android:layout_height="220dp"
android:layout_gravity="center"
android:layout_marginTop="200dp"
android:background="@drawable/cry"
>
</ImageView>
<TextView
android:id="@+id/layout_tishi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="#000000"
android:textSize="26dp"
android:text="没有找到任何数据"
android:gravity="center"
>
</TextView>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
然后整理一下业务逻辑流程(这里示例中数据进行本地缓存,想了解的请看另一篇关于Banner的博文):
①打开APP后请求服务器获取相关数据,若有数据则渲染item并通过适配器加载到RecycleView中,若无数据则加载空页面布局。②下拉刷新时请求服务器数据并重置RecycleView并重新渲染加载item。③上拉加载使用分页的逻辑加载下一页数据并更新在RecycleView中,此步需要对数据进行去重,以免由于加载时有新数据导致原本下一页的数据与之前数据重复。
接下来直接贴出完整代码,重要部分在注释中说明,这里示例使用的是Fragment,请根据自己项目适当修改,这里只演示流程和基本逻辑。
public class zixun extends Fragment {
static RecyclerView recycleview;
static int haveData = 0; //当前列表数据条数
static int total = 0; //上次刷新时获取数据总条数
static List<Map<String,String>> mDatas = new ArrayList<Map<String, String>>(); //当前数据列表
static Map<String,String> noticeId = new HashMap<>(); //用于存储数据ID进行数据去重
private HomeAdapter mAdapter;
static int page = 1; //当前数据页
static int limit = 10; //每页多少条
static RefreshLayout refreshLayout;
@Override
public void onCreate(Bundle onSaveInstanceState){
super.onCreate(onSaveInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View main_tab_zixun = inflater.inflate(R.layout.main_zixun,container,false);
recycleview = main_tab_zixun.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
recycleview.setLayoutManager(layoutManager);
layoutManager.setOrientation(OrientationHelper. VERTICAL);
recycleview.setAdapter(mAdapter = new HomeAdapter());
recycleview.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL));
recycleview.setItemAnimator(new DefaultItemAnimator());
refreshLayout = (RefreshLayout)main_tab_zixun.findViewById(R.id.refreshLayout);
//设置刷新加载的动画效果
refreshLayout.setRefreshHeader(new PhoenixHeader(getContext()));
refreshLayout.setRefreshFooter(new BallPulseFooter(getContext()));
refreshLayout.setDisableContentWhenRefresh(true);
initData(); //先加载本地缓存的数据,之后在onresumen中刷新
refreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh(RefreshLayout refreshlayout) {
//刷新时重置所有数据并重新设置适配器
refreshlayout.autoRefresh();
page = 1;
mDatas.clear();
noticeId.clear();
refreshData();
}
});
refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore(RefreshLayout refreshlayout) {
//是否加载只看当前数据是否小于第一次刷新时获取的总条数
haveData = mDatas.size();
refreshlayout.autoLoadMore();
if (haveData<total){
loadData();
}else{
Toasty.info(getActivity(),"真的没有数据了,刷新试试吧",Toast.LENGTH_SHORT).show();
refreshlayout.finishLoadMore(1000);
}
}
});
return main_tab_zixun;
}
@Override
public void onResume() {
super.onResume();
refreshLayout.autoRefresh(); //刷新数据
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
protected void initData()
{
//加载本地缓存并解析,这里不做阐述,可以看另一篇关于Banner的博文
final SharedPreferences sp = getActivity().getSharedPreferences("zixun", Context.MODE_PRIVATE);
String zixun_json_list = sp.getString("jsonList",null);
if(zixun_json_list==null){
}else{
try {
JSONArray dataArray = new JSONArray(zixun_json_list);
for(int i=0;i<dataArray.length();i++){
JSONObject thisNotice = dataArray.getJSONObject(i);
Map<String,String> map = new HashMap<>();
map.put("title",thisNotice.getString("title"));
map.put("img_url",thisNotice.getString("img_url"));
map.put("description",thisNotice.getString("description"));
map.put("time",timeStamp2Date(thisNotice.getInt("create_time")+"000","yyyy-MM-dd HH:mm"));
mDatas.add(map);
noticeId.put("noticeId:"+thisNotice.getInt("id"),thisNotice.getInt("id")+"");
}
recycleview.setAdapter(mAdapter = new HomeAdapter());
} catch (JSONException e) {
e.printStackTrace();
}
}
}
protected void refreshData()
{
String host = new Defines().SERVER_HOST+"getNotice";
OkHttpUtils
.post()
.url(host)
.addParams("page",page+"")
.addParams("limit",limit+"")
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
refreshLayout.finishRefresh(1000);
Toasty.error(getActivity(), "服务器请求失败", Toast.LENGTH_SHORT, true).show();
}
@Override
public void onResponse(String response, int id) {
try {
JSONObject jsonObject = new JSONObject(response);
int code = jsonObject.getInt("code");
if (code==200) {
total = jsonObject.getInt("total");
page = jsonObject.getInt("current_page");
JSONArray dataArray = jsonObject.getJSONArray("data");
final SharedPreferences sp = getActivity().getSharedPreferences("zixun", Context.MODE_PRIVATE);
sp.edit().putString("jsonList",dataArray.toString()).commit();
for(int i=0;i<dataArray.length();i++){
JSONObject thisNotice = dataArray.getJSONObject(i);
Map<String,String> map = new HashMap<>();
map.put("title",thisNotice.getString("title"));
map.put("img_url",thisNotice.getString("img_url"));
map.put("description",thisNotice.getString("description"));
//服务器给的是时间戳,这里进行转换
map.put("time",timeStamp2Date(thisNotice.getInt("create_time")+"000","yyyy-MM-dd HH:mm"));
mDatas.add(map);
//将每条数据的ID存入map在加载时可以数据去重判断
noticeId.put("noticeId:"+thisNotice.getInt("id"),thisNotice.getInt("id")+"");
}
//重新设置适配器
recycleview.setAdapter(mAdapter = new HomeAdapter());
refreshLayout.finishRefresh(1000);
} else {
refreshLayout.finishRefresh(1000);
Toasty.error(getActivity(),jsonObject.getString("msg"), Toast.LENGTH_SHORT, true).show();
}
} catch (JSONException e) {
e.printStackTrace();
refreshLayout.finishRefresh(1000);
Toasty.error(getActivity(),"刷新失败",Toast.LENGTH_LONG).show();
}
}
});
}
protected void loadData()
{
String host = new Defines().SERVER_HOST+"getNotice";
OkHttpUtils
.post()
.url(host)
.addParams("page",(page+1)+"")
.addParams("limit",limit+"")
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
refreshLayout.finishLoadMore(1000);
Toasty.error(getActivity(), "服务器请求失败", Toast.LENGTH_SHORT, true).show();
}
@Override
public void onResponse(String response, int id) {
try {
JSONObject jsonObject = new JSONObject(response);
int code = jsonObject.getInt("code");
if (code==200) {
page = jsonObject.getInt("current_page");
JSONArray dataArray = jsonObject.getJSONArray("data");
for(int i=0;i<dataArray.length();i++){
JSONObject thisNotice = dataArray.getJSONObject(i);
Map<String,String> map = new HashMap<>();
map.put("title",thisNotice.getString("title"));
map.put("description",thisNotice.getString("description"));
map.put("img_url",thisNotice.getString("img_url"));
map.put("time",timeStamp2Date(thisNotice.getInt("create_time")+"000","yyyy-MM-dd HH:mm"));
//若数据ID已存在则不加载
if(!noticeId.containsValue(thisNotice.getInt("id")+"")){
noticeId.put("noticeId:"+thisNotice.getInt("id"),thisNotice.getInt("id")+"");
mDatas.add(map);
}
}
mAdapter.notifyDataSetChanged();
refreshLayout.finishLoadMore(1000);
} else {
refreshLayout.finishLoadMore(1000);
Toasty.error(getActivity(),jsonObject.getString("msg"), Toast.LENGTH_SHORT, true).show();
}
} catch (JSONException e) {
e.printStackTrace();
refreshLayout.finishLoadMore(1000);
Toasty.error(getActivity(),"加载失败",Toast.LENGTH_LONG).show();
}
}
});
}
//时间戳转时间字符串
public static String timeStamp2Date(String seconds,String format) {
if(seconds == null || seconds.isEmpty() || seconds.equals("null")){
return "";
}
if(format == null || format.isEmpty())
format = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(new Date(Long.valueOf(seconds)));
}
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder>
{
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
MyViewHolder holder;
//如果数据为空则加载空页面布局
if(mDatas == null || mDatas.isEmpty()){
holder = new MyViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.empty_data, parent, false));
}else{
holder = new MyViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.item_home, parent, false));
}
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, final int position)
{
//若有数据则将数据匹配到item,若没有则设置空页面布局的提示
if(!(mDatas == null || mDatas.isEmpty())){
holder.time.setText(mDatas.get(position).get("time"));
holder.titleTextView.setText(mDatas.get(position).get("title"));
holder.descriptionTextView.setText(mDatas.get(position).get("description"));
RequestOptions options = RequestOptions.centerInsideTransform().placeholder(R.drawable.loading).error(R.drawable.loading);
Glide.with(getContext()).load(mDatas.get(position).get("img_url")).apply(options).into(holder.img);
holder.relativeLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toasty.info(getActivity(),"点了第"+(position+1)+"个Item,标题是:"+mDatas.get(position).get("title"),Toast.LENGTH_SHORT).show();
}
});
}else{
holder.layout_tishi.setText("没有找到任何数据");
}
}
@Override
public int getItemCount()
{
//这里一定要进行一个三目运算,如果没有数据也要返回1以加载空页面布局,否则RecycleView将不加载任何东西
return mDatas.size()==0?1:mDatas.size();
}
class MyViewHolder extends RecyclerView.ViewHolder
{
TextView titleTextView;
TextView descriptionTextView;
TextView time;
TextView layout_tishi;
ImageView img;
RelativeLayout relativeLayout;
public MyViewHolder(View view)
{
super(view);
//定义控件,这里标题和描述的textview进行了限制,超过部分用...显示
if(mDatas == null || mDatas.isEmpty()){
layout_tishi = (TextView) view.findViewById(R.id.layout_tishi);
}else {
img = (ImageView) view.findViewById(R.id.img);
titleTextView = (TextView) view.findViewById(R.id.title);
descriptionTextView = (TextView) view.findViewById(R.id.description);
time = (TextView) view.findViewById(R.id.time);
relativeLayout = (RelativeLayout) view.findViewById(R.id.notice);
titleTextView.setSingleLine(true);
titleTextView.setEllipsize(TextUtils.TruncateAt.END);
descriptionTextView.setMaxLines(3);
descriptionTextView.setEllipsize(TextUtils.TruncateAt.END);
}
}
}
}
}
接下来通过动态图看一下实际项目中的实现效果: