前言
上篇文章中我講述了如何實現Tabs 頻道頁面切換,接下來我們進一步實現 Tab Channel 中的新聞卡片列表渲染。
項目倉庫
簡易今日頭條 - Github
Display
具體實現
由於一種的渲染方法比較簡單,通常設計者是卡在顯示多種類型的 RecyclerView Item,在多種卡片 item 的設計這裡我只需要設計一種 DataModel
,然後在 DataModel
中添加 type
成員變量判斷是哪種類型的卡片。
以下我舉出實現的 Example,簡化的部份 Code,這裡的 Code 去掉: HTTP 請求的渲染、Pull To ReFresh、Load More。後面會再一一舉例。
layout/fragment_news_channel.xml
在需要渲染列表的地方加上 RecyclerView 組件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_news_channel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.page.newsChannel.NewsChannelFragment">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="MissingClass,MissingConstraints">
<TextView
android:id="@+id/text_view_section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_card_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="5dp"
android:nestedScrollingEnabled="true"
android:scrollbars="vertical" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
layout/no_image_card_item.xml
、layout/one_image_card_item.xml
、three_images_card_item.xml
這是我們需要渲染的三種新聞卡片,我這裡就舉出第一種卡片 no_image_card_item
,其他的可以自己去設計,不多贅述一一放出來。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:cardCornerRadius="10dp"
app:cardElevation="2dp">
<TextView
android:id="@+id/text_view_source_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image_view_card_avatar"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="10dp"
tools:ignore="MissingConstraints"
tools:srcCompat="@drawable/avatar_1"
android:contentDescription="@string/avatar" />
<TextView
android:id="@+id/text_view_card_title"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_goes_here"
app:layout_constraintBottom_toTopOf="@+id/avatar"
app:layout_constraintStart_toEndOf="@+id/avatar"
app:layout_constraintTop_toBottomOf="@+id/avatar"
tools:ignore="MissingConstraints" />
</LinearLayout>
<TextView
android:id="@+id/text_view_card_subtitle"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/subtitle_goes_here"
app:layout_constraintStart_toStartOf="@+id/card_title"
app:layout_constraintTop_toBottomOf="@+id/card_title" />
<TextView
android:id="@+id/text_view_card_bottom_text"
style="@style/TextAppearance.MaterialComponents.Overline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/comment"
app:layout_constraintStart_toStartOf="@+id/card_subtitle"
app:layout_constraintTop_toBottomOf="@+id/card_subtitle" />
</LinearLayout>
</androidx.cardview.widget.CardView>
|
ui/card/newsCardList/NewsCardItemDataModel.java
我在 ui/card/newsCardList/
創建了兩個文件:NewsCardItemDataModel.java
、NewsCardAdapter.java
,分別代表卡片的數據模型和 RecyclerView
適配器。
我這裡寫了三種構造類方法對應三種不同類型的卡片所需數據,並使用 mItemType
判斷卡片類型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
package com.example.toutiao.ui.card.newsCardList;
import java.util.ArrayList;
import java.util.Locale;
/**
* CardItemDataModel class: the card item data model in New Channel Fragment
*/
public class NewsCardItemDataModel {
public static final int NO_IMAGE_TYPE = 0;
public static final int ONE_IMAGE_TYPE = 1;
public static final int THREE_IMAGE_TYPE = 2;
private int mItemType; // cart type
private String mId; //id
private String mAvatar; // avatar
private ArrayList<String> mThreeImageDrawable; // three image
private String mImageDrawable; // one image
private String mTitle; // title
private String mSubTitle; // subtitle
private String mBottomText; // bottom text
private String mDetailUrl; // detail text to jump
// no image style constructor
public NewsCardItemDataModel(int itemType,
String id,
String newsTitle,
String newsAbstract,
int newsCommentsCount,
String newsSource,
String newsMediaAvatarUrl,
String newsSourceUrl
) {
mItemType = itemType;
mId = id;
mAvatar = newsMediaAvatarUrl;
mTitle = String.format(Locale.CHINESE, "%s", newsTitle);
mSubTitle = String.format(Locale.CHINESE, "%s", newsAbstract);
mBottomText = String.format(Locale.CHINESE, "%s %d 评论", newsSource, newsCommentsCount);
mDetailUrl = newsSourceUrl;
}
// one image style constructor
public NewsCardItemDataModel(int itemType,
String id,
String newsTitle,
String newsAbstract,
int newsCommentsCount,
String newsSource,
String newsMediaAvatarUrl,
String newsSourceUrl,
String imageDrawable
) {
mItemType = itemType;
mId = id;
mAvatar = newsMediaAvatarUrl;
mTitle = String.format(Locale.CHINESE, "%s", newsTitle);
mSubTitle = String.format(Locale.CHINESE, "%s", newsAbstract);
mBottomText = String.format(Locale.CHINESE, "%s %d 评论", newsSource, newsCommentsCount);
mDetailUrl = newsSourceUrl;
// one
mImageDrawable = imageDrawable;
}
// three image style constructor
public NewsCardItemDataModel(int itemType,
String id,
String newsTitle,
String newsAbstract,
int newsCommentsCount,
String newsSource,
String newsMediaAvatarUrl,
String newsSourceUrl,
ArrayList<String> threeImageDrawable
) {
mItemType = itemType;
mId = id;
mAvatar = newsMediaAvatarUrl;
mTitle = String.format(Locale.CHINESE, "%s", newsTitle);
mSubTitle = String.format(Locale.CHINESE, "%s", newsAbstract);
mBottomText = String.format(Locale.CHINESE, "%s %d 评论", newsSource, newsCommentsCount);
mDetailUrl = newsSourceUrl;
// three
mThreeImageDrawable = threeImageDrawable;
}
public int getItemType() {
return mItemType;
}
public void setItemType(int itemType) {
mItemType = itemType;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getAvatar() {
return mAvatar;
}
public void setAvatar(String avatar) {
mAvatar = avatar;
}
public String getBottomText() {
return mBottomText;
}
public void setBottomText(String bottomText) {
mBottomText = bottomText;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
mTitle = title;
}
public String getSubTitle() {
return mSubTitle;
}
public void setSubTitle(String subTitle) {
mSubTitle = subTitle;
}
public ArrayList<String> getThreeImageDrawable() {
return mThreeImageDrawable;
}
public void setThreeImageDrawable(ArrayList<String> threeImageDrawable) {
mThreeImageDrawable = threeImageDrawable;
}
public String getImageDrawable() {
return mImageDrawable;
}
public void setImageDrawable(String imageDrawable) {
mImageDrawable = imageDrawable;
}
public String getDetailUrl() {
return mDetailUrl;
}
public void setDetailUrl(String detailUrl) {
mDetailUrl = detailUrl;
}
}
|
ui/card/newsCardList/NewsCardAdapter.java
這是渲染卡片的 RecyclerView
適配器類,三種卡片類型就會有三種 ViewHolder
子類。
onCreateViewHolder
綁定 UI 文件,onBindViewHolder
綁定數據。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
package com.example.toutiao.ui.card.newsCardList;
// ...
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.toutiao.R;
import com.example.toutiao.activity.NewsDetailActivity;
import com.squareup.picasso.Picasso;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A card adapter to help perform to control card item's render in news channel fragment
*/
public class NewsCardAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<NewsCardItemDataModel> mDataModelList;
private final Context mContext;
public NewsCardAdapter(List<NewsCardItemDataModel> modelList, Context context) {
mDataModelList = modelList;
mContext = context;
}
@Override
public int getItemViewType(final int position) {
switch (mDataModelList.get(position).getItemType()) {
case NewsCardItemDataModel.NO_IMAGE_TYPE:
return NewsCardItemDataModel.NO_IMAGE_TYPE;
case NewsCardItemDataModel.ONE_IMAGE_TYPE:
return NewsCardItemDataModel.ONE_IMAGE_TYPE;
case NewsCardItemDataModel.THREE_IMAGE_TYPE:
return NewsCardItemDataModel.THREE_IMAGE_TYPE;
default:
return -1;
}
}
/**
* load more news and add to mDataModelList
*
* @param modelList
*/
public void setDataModelList(List<NewsCardItemDataModel> modelList) {
mDataModelList.addAll(modelList);
}
@Override
public int getItemCount() {
return mDataModelList.size();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view;
switch (viewType) {
case NewsCardItemDataModel.NO_IMAGE_TYPE:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.no_image_card_item, parent, false);
return new NoImageCardViewHolder(view);
case NewsCardItemDataModel.ONE_IMAGE_TYPE:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.one_image_card_item, parent, false);
return new OneImageCardViewHolder(view);
case NewsCardItemDataModel.THREE_IMAGE_TYPE:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.three_images_card_item, parent, false);
return new ThreeImageCardViewHolder(view);
default:
return null;
}
}
@Override
public void onBindViewHolder(@NotNull final RecyclerView.ViewHolder holder, final int position) {
NewsCardItemDataModel object = mDataModelList.get(position);
if (object != null) {
switch (object.getItemType()) {
case NewsCardItemDataModel.NO_IMAGE_TYPE:
NoImageCardViewHolder holder1 = (NoImageCardViewHolder) holder;
holder1.bindData(object, mContext);
break;
case NewsCardItemDataModel.ONE_IMAGE_TYPE:
OneImageCardViewHolder holder2 = (OneImageCardViewHolder) holder;
holder2.bindData(object, mContext);
break;
case NewsCardItemDataModel.THREE_IMAGE_TYPE:
ThreeImageCardViewHolder holder3 = (ThreeImageCardViewHolder) holder;
holder3.bindData(object, mContext);
break;
default:
break;
}
}
}
// No image style card view holder
class NoImageCardViewHolder extends RecyclerView.ViewHolder {
private final ImageView mAvatarView;
private final TextView mTitleTextView;
private final TextView mSubTitleTextView;
private final TextView mBottomTextView;
private final TextView mSourceUrlTextView;
public NoImageCardViewHolder(@NonNull View itemView) {
super(itemView);
mAvatarView = itemView.findViewById(R.id.image_view_card_avatar);
mTitleTextView = itemView.findViewById(R.id.text_view_card_title);
mSubTitleTextView = itemView.findViewById(R.id.text_view_card_subtitle);
mBottomTextView = itemView.findViewById(R.id.text_view_card_bottom_text);
mSourceUrlTextView = itemView.findViewById(R.id.text_view_source_url);
}
public void bindData(NewsCardItemDataModel dataModel, Context context) {
Picasso.get().load(dataModel.getAvatar()).into(mAvatarView);
// deal with title's length and subtitle's length
String title = dataModel.getTitle();
if (title.length() > 15) {
title = title.substring(0, 16);
title += "...";
}
mTitleTextView.setText(title);
String subTitle = dataModel.getSubTitle();
if (subTitle.length() > 70) {
subTitle = subTitle.substring(0, 69);
subTitle += "...";
}
mSubTitleTextView.setText(subTitle);
mBottomTextView.setText(dataModel.getBottomText());
mSourceUrlTextView.setText(dataModel.getDetailUrl());
}
}
// One image style card view holder
class OneImageCardViewHolder extends RecyclerView.ViewHolder {
private final ImageView mAvatarView;
private final TextView mTitleTextView;
private final TextView mSubTitleTextView;
private final TextView mBottomTextView;
private final TextView mSourceUrlTextView;
private final ImageView mCardImageView;
public OneImageCardViewHolder(@NonNull View itemView) {
super(itemView);
mAvatarView = itemView.findViewById(R.id.image_view_card_avatar);
mTitleTextView = itemView.findViewById(R.id.text_view_card_title);
mSubTitleTextView = itemView.findViewById(R.id.text_view_card_subtitle);
mBottomTextView = itemView.findViewById(R.id.text_view_card_bottom_text);
mSourceUrlTextView = itemView.findViewById(R.id.text_view_source_url);
mCardImageView = itemView.findViewById(R.id.image_view_card_image);
}
public void bindData(NewsCardItemDataModel dataModel, Context context) {
Picasso.get().load(dataModel.getAvatar()).into(mAvatarView);
Picasso.get().load(dataModel.getImageDrawable()).into(mCardImageView);
String title = dataModel.getTitle();
// deal with title's length and subtitle's length
if (title.length() > 15) {
title = title.substring(0, 14);
title += "...";
}
mTitleTextView.setText(title);
String subTitle = dataModel.getSubTitle();
if (subTitle.length() > 70) {
subTitle = subTitle.substring(0, 69);
subTitle += "...";
}
mSubTitleTextView.setText(subTitle);
mBottomTextView.setText(dataModel.getBottomText());
mSourceUrlTextView.setText(dataModel.getDetailUrl());
}
}
// Three image style card view holder
class ThreeImageCardViewHolder extends RecyclerView.ViewHolder {
private final ImageView mAvatarView;
private final TextView mTitleTextView;
private final TextView mSubTitleTextView;
private final TextView mBottomTextView;
private final TextView mSourceUrlTextView;
private final ImageView mCardImageView1;
private final ImageView mCardImageView2;
private final ImageView mCardImageView3;
public ThreeImageCardViewHolder(@NonNull View itemView) {
super(itemView);
mAvatarView = itemView.findViewById(R.id.image_view_card_avatar);
mTitleTextView = itemView.findViewById(R.id.text_view_card_title);
mSubTitleTextView = itemView.findViewById(R.id.text_view_card_subtitle);
mBottomTextView = itemView.findViewById(R.id.text_view_card_bottom_text);
mSourceUrlTextView = itemView.findViewById(R.id.text_view_source_url);
mCardImageView1 = itemView.findViewById(R.id.image_view_image_1);
mCardImageView2 = itemView.findViewById(R.id.image_view_image_2);
mCardImageView3 = itemView.findViewById(R.id.image_view_image_3);
}
public void bindData(NewsCardItemDataModel dataModel, Context context) {
Picasso.get().load(dataModel.getAvatar()).into(mAvatarView);
ArrayList<String> images = dataModel.getThreeImageDrawable();
Picasso.get().load(images.get(0)).into(mCardImageView1);
Picasso.get().load(images.get(1)).into(mCardImageView2);
Picasso.get().load(images.get(2)).into(mCardImageView3);
// deal with title's length and subtitle's length
String title = dataModel.getTitle();
if (title.length() > 15) {
title = title.substring(0, 16);
title += "...";
}
mTitleTextView.setText(title);
String subTitle = dataModel.getSubTitle();
if (subTitle.length() > 70) {
subTitle = subTitle.substring(0, 69);
subTitle += "...";
}
mSubTitleTextView.setText(subTitle);
mBottomTextView.setText(dataModel.getBottomText());
mSourceUrlTextView.setText(dataModel.getDetailUrl());
}
}
}
|
ui/page/newsChannel/newsChannelFragment.java
在需要渲染的 Fragment,添加渲染 Code。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
package com.example.toutiao.ui.page.newsChannel;
// ...
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.toutiao.R;
import com.example.toutiao.ui.card.newsCardList.NewsCardAdapter;
import com.example.toutiao.ui.card.newsCardList.NewsCardItemDataModel;
/**
* A simple {@link Fragment} subclass.
* Use the {@link NewsChannelFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class NewsChannelFragment extends Fragment {
private String mCategory;
private int mIndex;
private RecyclerView mCardListRecyclerView;
private NewsCardAdapter mCardListAdapter;
private RecyclerView.LayoutManager mCardListLayoutManager;
private final List<NewsCardItemDataModel> mCardDataModelList = new ArrayList<>();
public NewsChannelFragment() {
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*/
public static NewsChannelFragment newInstance(String category, int index) {
NewsChannelFragment fragment = new NewsChannelFragment();
Bundle bundle = new Bundle();
bundle.putString("category", category);
bundle.putInt("index", index);
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_news_channel, container, false);
// ...
for (int i = 0; i < 10; i++) {
int type = i % 3;
String newsId = i;
String newsTitle = "你好";
String newsAbstract = "我是卡片";
int newsCommentsCount = 100;
String newsSource = "https://example.com/e.png";
String newsMediaAvatarUrl = "https://example.com/e.png";
String newsSourceUrl = "www.google.com";
if (type == NO_IMAGE_TYPE) {
mCardDataModelList.add(new NewsCardItemDataModel(
NO_IMAGE_TYPE,
newsId,
newsTitle,
newsAbstract,
newsCommentsCount,
newsSource,
newsMediaAvatarUrl,
newsSourceUrl
));
} else if (type == ONE_IMAGE_TYPE) {
String middleImage = "https://example.com/e.png";
mCardDataModelList.add(new NewsCardItemDataModel(
ONE_IMAGE_TYPE,
newsId,
newsTitle,
newsAbstract,
newsCommentsCount,
newsSource,
newsMediaAvatarUrl,
newsSourceUrl,
middleImage
));
} else if (type == THREE_IMAGE_TYPE) {
ArrayList<String> newsThreeImage = new ArrayList()<>;
newsThreeImage.add("https://example.com/e.png");
newsThreeImage.add("https://example.com/e.png");
newsThreeImage.add("https://example.com/e.png");
mCardDataModelList.add(new NewsCardItemDataModel(
THREE_IMAGE_TYPE,
newsId,
newsTitle,
newsAbstract,
newsCommentsCount,
newsSource,
newsMediaAvatarUrl,
newsSourceUrl,
newsThreeImage
));
}
}
// cardList
mCardListRecyclerView = view.findViewById(R.id.recycler_view_card_list);
// use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
mCardListRecyclerView.setHasFixedSize(true);
// use a linear layout manager
mCardListLayoutManager = new LinearLayoutManager(getContext());
mCardListRecyclerView.setLayoutManager(mCardListLayoutManager);
// specify an adapter and pass in our data model list
mCardListAdapter = new NewsCardAdapter(mCardDataModelList, getContext());
mCardListRecyclerView.setAdapter(mCardListAdapter);
return view;
}
}
|
Reference