Skip to content

Commit b36067d

Browse files
Merge pull request #4294 from AlexNi245/#2216-activity-data-divers-design
Activity data dividers
2 parents 259e106 + 9624019 commit b36067d

File tree

7 files changed

+371
-26
lines changed

7 files changed

+371
-26
lines changed

src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public void showFiles(boolean onDeviceOnly) {
165165
private void setupContent() {
166166
emptyContentIcon.setImageResource(R.drawable.ic_activity_light_grey);
167167
emptyContentProgressBar.getIndeterminateDrawable().setColorFilter(ThemeUtils.primaryAccentColor(this),
168-
PorterDuff.Mode.SRC_IN);
168+
PorterDuff.Mode.SRC_IN);
169169

170170
FileDataStorageManager storageManager = new FileDataStorageManager(getAccount(), getContentResolver());
171171
adapter = new ActivityListAdapter(this, getUserAccountManager(), this, storageManager, getCapabilities(), false);
@@ -174,6 +174,7 @@ private void setupContent() {
174174
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
175175

176176
recyclerView.setLayoutManager(layoutManager);
177+
recyclerView.addItemDecoration(new StickyHeaderItemDecoration(adapter));
177178
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
178179

179180
@Override
@@ -186,7 +187,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
186187

187188
// synchronize loading state when item count changes
188189
if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5)
189-
&& nextPageUrl != null && !nextPageUrl.isEmpty()) {
190+
&& nextPageUrl != null && !nextPageUrl.isEmpty()) {
190191
// Almost reached the end, continue to load new activities
191192
mActionListener.loadActivities(nextPageUrl);
192193
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
5+
* Copyright (C) 2019 Sevastyan Savanyuk
6+
* Copyright (C) 2019 Nextcloud GmbH
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
package com.owncloud.android.ui.activities;
22+
23+
import android.graphics.Canvas;
24+
import android.graphics.Color;
25+
import android.view.LayoutInflater;
26+
import android.view.View;
27+
import android.view.ViewGroup;
28+
29+
import com.owncloud.android.ui.adapter.StickyHeaderAdapter;
30+
31+
import androidx.annotation.NonNull;
32+
import androidx.recyclerview.widget.RecyclerView;
33+
34+
public class StickyHeaderItemDecoration extends RecyclerView.ItemDecoration {
35+
private final StickyHeaderAdapter adapter;
36+
37+
38+
public StickyHeaderItemDecoration(StickyHeaderAdapter stickyHeaderAdapter) {
39+
this.adapter = stickyHeaderAdapter;
40+
}
41+
42+
@Override
43+
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
44+
super.onDrawOver(canvas, parent, state);
45+
46+
View topChild = parent.getChildAt(0);
47+
if (topChild == null) {
48+
return;
49+
}
50+
int topChildPosition = parent.getChildAdapterPosition(topChild);
51+
52+
if (topChildPosition == RecyclerView.NO_POSITION) {
53+
return;
54+
}
55+
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
56+
fixLayoutSize(parent, currentHeader);
57+
int contactPoint = currentHeader.getBottom();
58+
View childInContact = getChildInContact(parent, contactPoint);
59+
60+
if (childInContact == null) {
61+
return;
62+
}
63+
64+
if (adapter.isHeader(parent.getChildAdapterPosition(childInContact))) {
65+
moveHeader(canvas, currentHeader, childInContact);
66+
return;
67+
}
68+
69+
drawHeader(canvas, currentHeader);
70+
}
71+
72+
private void drawHeader(Canvas canvas, View header) {
73+
canvas.save();
74+
canvas.translate(0, 0);
75+
header.draw(canvas);
76+
canvas.restore();
77+
}
78+
79+
private void moveHeader(Canvas canvas, View currentHeader, View nextHeader) {
80+
canvas.save();
81+
canvas.translate(0, nextHeader.getTop() - currentHeader.getHeight());
82+
currentHeader.draw(canvas);
83+
canvas.restore();
84+
}
85+
86+
private View getChildInContact(RecyclerView parent, int contactPoint) {
87+
View childInContact = null;
88+
for (int i = 0; i < parent.getChildCount(); i++) {
89+
View currentChild = parent.getChildAt(i);
90+
if (currentChild.getBottom() > contactPoint && currentChild.getTop() <= contactPoint) {
91+
childInContact = currentChild;
92+
break;
93+
}
94+
}
95+
return childInContact;
96+
}
97+
98+
private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
99+
int headerPosition = adapter.getHeaderPositionForItem(itemPosition);
100+
int layoutId = adapter.getHeaderLayout(itemPosition);
101+
View header = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
102+
header.setBackgroundColor(Color.WHITE);
103+
adapter.bindHeaderData(header, headerPosition);
104+
return header;
105+
}
106+
107+
private void fixLayoutSize(ViewGroup parent, View view) {
108+
109+
// Specs for parent (RecyclerView)
110+
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
111+
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
112+
113+
// Specs for children (headers)
114+
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
115+
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);
116+
117+
view.measure(childWidthSpec, childHeightSpec);
118+
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
119+
}
120+
121+
122+
}

src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
/**
8585
* Adapter for the activity view
8686
*/
87-
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
87+
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter {
8888

8989
static final int HEADER_TYPE = 100;
9090
static final int ACTIVITY_TYPE = 101;
@@ -173,11 +173,10 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
173173
}
174174

175175
if (activity.getRichSubjectElement() != null &&
176-
!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
176+
!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
177177
activityViewHolder.subject.setVisibility(View.VISIBLE);
178178
activityViewHolder.subject.setMovementMethod(LinkMovementMethod.getInstance());
179-
activityViewHolder.subject.setText(addClickablePart(activity.getRichSubjectElement()),
180-
TextView.BufferType.SPANNABLE);
179+
activityViewHolder.subject.setText(addClickablePart(activity.getRichSubjectElement()), TextView.BufferType.SPANNABLE);
181180
activityViewHolder.subject.setVisibility(View.VISIBLE);
182181
} else if (!TextUtils.isEmpty(activity.getSubject())) {
183182
activityViewHolder.subject.setVisibility(View.VISIBLE);
@@ -198,8 +197,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
198197
}
199198

200199
if (activity.getRichSubjectElement() != null &&
201-
activity.getRichSubjectElement().getRichObjectList().size() > 0) {
202-
200+
activity.getRichSubjectElement().getRichObjectList().size() > 0) {
203201
activityViewHolder.list.setVisibility(View.VISIBLE);
204202
activityViewHolder.list.removeAllViews();
205203

@@ -325,23 +323,23 @@ private void setBitmap(OCFile file, ImageView fileIcon, boolean isDetailView) {
325323

326324
private void downloadIcon(String icon, ImageView itemViewType) {
327325
GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(context)
328-
.using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class)
329-
.from(Uri.class)
330-
.as(SVG.class)
331-
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
332-
.sourceEncoder(new StreamEncoder())
333-
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
334-
.decoder(new SvgDecoder())
335-
.placeholder(R.drawable.ic_activity)
336-
.error(R.drawable.ic_activity)
337-
.animate(android.R.anim.fade_in)
338-
.listener(new SvgSoftwareLayerSetter<>());
326+
.using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class)
327+
.from(Uri.class)
328+
.as(SVG.class)
329+
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
330+
.sourceEncoder(new StreamEncoder())
331+
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
332+
.decoder(new SvgDecoder())
333+
.placeholder(R.drawable.ic_activity)
334+
.error(R.drawable.ic_activity)
335+
.animate(android.R.anim.fade_in)
336+
.listener(new SvgSoftwareLayerSetter<>());
339337

340338
Uri uri = Uri.parse(icon);
341339
requestBuilder
342-
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
343-
.load(uri)
344-
.into(itemViewType);
340+
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
341+
.load(uri)
342+
.into(itemViewType);
345343
}
346344

347345
private SpannableStringBuilder addClickablePart(RichElement richElement) {
@@ -418,7 +416,7 @@ private int getThumbnailDimension() {
418416
CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
419417
if ((System.currentTimeMillis() - modificationTimestamp) < DateUtils.WEEK_IN_MILLIS) {
420418
return DisplayUtils.getRelativeDateTimeString(context, modificationTimestamp, DateUtils.DAY_IN_MILLIS,
421-
DateUtils.WEEK_IN_MILLIS, 0);
419+
DateUtils.WEEK_IN_MILLIS, 0);
422420
} else {
423421
String pattern = "EEEE, MMMM d";
424422
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
@@ -428,6 +426,37 @@ CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
428426
}
429427
}
430428

429+
430+
@Override
431+
public int getHeaderPositionForItem(int itemPosition) {
432+
int headerPosition = itemPosition;
433+
while (headerPosition >= 0) {
434+
if (this.isHeader(headerPosition)) {
435+
break;
436+
}
437+
headerPosition -= 1;
438+
}
439+
return headerPosition;
440+
}
441+
442+
443+
@Override
444+
public int getHeaderLayout(int headerPosition) {
445+
return R.layout.activity_list_item_header;
446+
}
447+
448+
@Override
449+
public void bindHeaderData(View header, int headerPosition) {
450+
TextView textView = header.findViewById(R.id.title_header);
451+
String headline = (String) values.get(headerPosition);
452+
textView.setText(headline);
453+
}
454+
455+
@Override
456+
public boolean isHeader(int itemPosition) {
457+
return this.getItemViewType(itemPosition) == HEADER_TYPE;
458+
}
459+
431460
protected class ActivityViewHolder extends RecyclerView.ViewHolder {
432461

433462
private final ImageView activityIcon;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
5+
* Copyright (C) 2019 Sevastyan Savanyuk
6+
* Copyright (C) 2019 Nextcloud GmbH
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
package com.owncloud.android.ui.adapter;
22+
23+
import android.view.View;
24+
25+
import com.owncloud.android.ui.activities.StickyHeaderItemDecoration;
26+
27+
public interface StickyHeaderAdapter {
28+
/**
29+
* This method gets called by {@link StickyHeaderItemDecoration} to fetch the position of the header item in the adapter
30+
* that is used for (represents) item at specified position.
31+
* @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
32+
* @return int. Position of the header item in the adapter.
33+
*/
34+
int getHeaderPositionForItem(int itemPosition);
35+
36+
/**
37+
* This method gets called by {@link StickyHeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
38+
* @param headerPosition int. Position of the header item in the adapter.
39+
* @return int. Layout resource id.
40+
*/
41+
int getHeaderLayout(int headerPosition);
42+
43+
/**
44+
* This method gets called by {@link StickyHeaderItemDecoration} to setup the header View.
45+
* @param header View. Header to set the data on.
46+
* @param headerPosition int. Position of the header item in the adapter.
47+
*/
48+
void bindHeaderData(View header, int headerPosition);
49+
50+
/**
51+
* This method gets called by {@link StickyHeaderItemDecoration} to verify whether the item represents a header.
52+
* @param itemPosition int.
53+
* @return true, if item at the specified adapter's position represents a header.
54+
*/
55+
boolean isHeader(int itemPosition);
56+
}

src/main/res/layout/activity_list_item_header.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
<TextView
77
android:id="@+id/title_header"
88
android:layout_width="match_parent"
9-
android:layout_height="wrap_content"
9+
android:layout_height="60dp"
1010
android:layout_marginLeft="@dimen/standard_list_item_size"
1111
android:layout_marginStart="@dimen/standard_list_item_size"
1212
android:layout_marginTop="10dp"
1313
android:text="@string/placeholder_filename"
1414
android:textSize="@dimen/activity_list_item_title_header_text_size"/>
1515

16-
</LinearLayout>
16+
</LinearLayout>

src/main/res/values/dims.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
<dimen name="activity_icon_height">32dp</dimen>
9696
<dimen name="activity_icon_layout_right_end_margin">24dp</dimen>
9797
<dimen name="activity_list_item_grid_layout_left_start_margin">-3dp</dimen>
98-
<dimen name="activity_list_item_title_header_text_size">20sp</dimen>
98+
<dimen name="activity_list_item_title_header_text_size">16sp</dimen>
9999
<dimen name="activity_list_layout_recycler_view_margin">-3dp</dimen>
100100
<dimen name="activity_row_layout_height">48dp</dimen>
101101
<dimen name="notification_icon_width">32dp</dimen>

0 commit comments

Comments
 (0)