Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd3d8dc
start to implement sticky header behavior
AlexNi245 Aug 11, 2019
a184345
first implementation of sticky header logic.
AlexNi245 Aug 14, 2019
13e7aff
increase height of header element and set backgroundcolor to white
AlexNi245 Aug 14, 2019
792a6b9
finish implementation of sticky header implementation. This feature w…
AlexNi245 Aug 17, 2019
b4764f7
remove unnecessary files
AlexNi245 Aug 17, 2019
c41dd38
optimize imports
AlexNi245 Aug 17, 2019
a26a895
apply changes to java doc
AlexNi245 Aug 17, 2019
7a30070
remove unnecessary TAG field and replace nested if statement with && …
AlexNi245 Aug 18, 2019
9012f43
set visibility from ActivityViewHeaderHolder back to protected
AlexNi245 Aug 18, 2019
b61f9e0
format ActivityListAdapter
AlexNi245 Aug 18, 2019
4ea9667
unit test for isHeader(int pos)
AlexNi245 Aug 19, 2019
0ac5a8b
add static import for com.owncloud.android.lib.resources.activities.m…
AlexNi245 Aug 19, 2019
cdd5d38
add getHeaderPositionForItem unit test
AlexNi245 Aug 19, 2019
6ffa9d0
remove duplicate entry of setContentView
AlexNi245 Aug 19, 2019
52a7ffe
add license text
AlexNi245 Aug 19, 2019
f4e964e
change class name of ActivityListItemDecoration to StickyHeaderItemDe…
AlexNi245 Aug 19, 2019
1dd2a5a
change naming of Canvas c to Canvas canvas
AlexNi245 Aug 19, 2019
5a3db5e
replace do while loop in getHeaderPositionForItem with while loop
AlexNi245 Aug 19, 2019
fa11565
fixed issue Avoid reassigning parameters such as 'itemPosition'
AlexNi245 Aug 19, 2019
9624019
activity header has now the same font size as a activity element
AlexNi245 Aug 19, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public void showFiles(boolean onDeviceOnly) {
private void setupContent() {
emptyContentIcon.setImageResource(R.drawable.ic_activity_light_grey);
emptyContentProgressBar.getIndeterminateDrawable().setColorFilter(ThemeUtils.primaryAccentColor(this),
PorterDuff.Mode.SRC_IN);
PorterDuff.Mode.SRC_IN);

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

recyclerView.setLayoutManager(layoutManager);
recyclerView.addItemDecoration(new StickyHeaderItemDecoration(adapter));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

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

// synchronize loading state when item count changes
if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5)
&& nextPageUrl != null && !nextPageUrl.isEmpty()) {
&& nextPageUrl != null && !nextPageUrl.isEmpty()) {
// Almost reached the end, continue to load new activities
mActionListener.loadActivities(nextPageUrl);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Nextcloud Android client application
*

* Copyright (C) 2019 Sevastyan Savanyuk
* Copyright (C) 2019 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.activities;

import android.graphics.Canvas;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.owncloud.android.ui.adapter.StickyHeaderAdapter;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

public class StickyHeaderItemDecoration extends RecyclerView.ItemDecoration {
private final StickyHeaderAdapter adapter;


public StickyHeaderItemDecoration(StickyHeaderAdapter stickyHeaderAdapter) {
this.adapter = stickyHeaderAdapter;
}

@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);

View topChild = parent.getChildAt(0);
if (topChild == null) {
return;
}
int topChildPosition = parent.getChildAdapterPosition(topChild);

if (topChildPosition == RecyclerView.NO_POSITION) {
return;
}
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
fixLayoutSize(parent, currentHeader);
int contactPoint = currentHeader.getBottom();
View childInContact = getChildInContact(parent, contactPoint);

if (childInContact == null) {
return;
}

if (adapter.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(canvas, currentHeader, childInContact);
return;
}

drawHeader(canvas, currentHeader);
}

private void drawHeader(Canvas canvas, View header) {
canvas.save();
canvas.translate(0, 0);
header.draw(canvas);
canvas.restore();
}

private void moveHeader(Canvas canvas, View currentHeader, View nextHeader) {
canvas.save();
canvas.translate(0, nextHeader.getTop() - currentHeader.getHeight());
currentHeader.draw(canvas);
canvas.restore();
}

private View getChildInContact(RecyclerView parent, int contactPoint) {
View childInContact = null;
for (int i = 0; i < parent.getChildCount(); i++) {
View currentChild = parent.getChildAt(i);
if (currentChild.getBottom() > contactPoint && currentChild.getTop() <= contactPoint) {
childInContact = currentChild;
break;
}
}
return childInContact;
}

private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
int headerPosition = adapter.getHeaderPositionForItem(itemPosition);
int layoutId = adapter.getHeaderLayout(itemPosition);
View header = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
header.setBackgroundColor(Color.WHITE);
adapter.bindHeaderData(header, headerPosition);
return header;
}

private void fixLayoutSize(ViewGroup parent, View view) {

// Specs for parent (RecyclerView)
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);

// Specs for children (headers)
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);

view.measure(childWidthSpec, childHeightSpec);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
/**
* Adapter for the activity view
*/
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter {

static final int HEADER_TYPE = 100;
static final int ACTIVITY_TYPE = 101;
Expand Down Expand Up @@ -173,11 +173,10 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
}

if (activity.getRichSubjectElement() != null &&
!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
activityViewHolder.subject.setVisibility(View.VISIBLE);
activityViewHolder.subject.setMovementMethod(LinkMovementMethod.getInstance());
activityViewHolder.subject.setText(addClickablePart(activity.getRichSubjectElement()),
TextView.BufferType.SPANNABLE);
activityViewHolder.subject.setText(addClickablePart(activity.getRichSubjectElement()), TextView.BufferType.SPANNABLE);
activityViewHolder.subject.setVisibility(View.VISIBLE);
} else if (!TextUtils.isEmpty(activity.getSubject())) {
activityViewHolder.subject.setVisibility(View.VISIBLE);
Expand All @@ -198,8 +197,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
}

if (activity.getRichSubjectElement() != null &&
activity.getRichSubjectElement().getRichObjectList().size() > 0) {

activity.getRichSubjectElement().getRichObjectList().size() > 0) {
activityViewHolder.list.setVisibility(View.VISIBLE);
activityViewHolder.list.removeAllViews();

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

private void downloadIcon(String icon, ImageView itemViewType) {
GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(context)
.using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class)
.from(Uri.class)
.as(SVG.class)
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
.sourceEncoder(new StreamEncoder())
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
.decoder(new SvgDecoder())
.placeholder(R.drawable.ic_activity)
.error(R.drawable.ic_activity)
.animate(android.R.anim.fade_in)
.listener(new SvgSoftwareLayerSetter<>());
.using(Glide.buildStreamModelLoader(Uri.class, context), InputStream.class)
.from(Uri.class)
.as(SVG.class)
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
.sourceEncoder(new StreamEncoder())
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
.decoder(new SvgDecoder())
.placeholder(R.drawable.ic_activity)
.error(R.drawable.ic_activity)
.animate(android.R.anim.fade_in)
.listener(new SvgSoftwareLayerSetter<>());

Uri uri = Uri.parse(icon);
requestBuilder
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.load(uri)
.into(itemViewType);
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.load(uri)
.into(itemViewType);
}

private SpannableStringBuilder addClickablePart(RichElement richElement) {
Expand Down Expand Up @@ -418,7 +416,7 @@ private int getThumbnailDimension() {
CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
if ((System.currentTimeMillis() - modificationTimestamp) < DateUtils.WEEK_IN_MILLIS) {
return DisplayUtils.getRelativeDateTimeString(context, modificationTimestamp, DateUtils.DAY_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS, 0);
DateUtils.WEEK_IN_MILLIS, 0);
} else {
String pattern = "EEEE, MMMM d";
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
Expand All @@ -428,6 +426,37 @@ CharSequence getHeaderDateString(Context context, long modificationTimestamp) {
}
}


@Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = itemPosition;
while (headerPosition >= 0) {
if (this.isHeader(headerPosition)) {
break;
}
headerPosition -= 1;
}
return headerPosition;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always have trouble reading do while loops, as they are so rarely used.
Can this maybe transformed in a regular while loop?



@Override
public int getHeaderLayout(int headerPosition) {
return R.layout.activity_list_item_header;
}

@Override
public void bindHeaderData(View header, int headerPosition) {
TextView textView = header.findViewById(R.id.title_header);
String headline = (String) values.get(headerPosition);
textView.setText(headline);
}

@Override
public boolean isHeader(int itemPosition) {
return this.getItemViewType(itemPosition) == HEADER_TYPE;
}

protected class ActivityViewHolder extends RecyclerView.ViewHolder {

private final ImageView activityIcon;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Nextcloud Android client application
*

* Copyright (C) 2019 Sevastyan Savanyuk
* Copyright (C) 2019 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also licence here, please


import android.view.View;

import com.owncloud.android.ui.activities.StickyHeaderItemDecoration;

public interface StickyHeaderAdapter {
/**
* This method gets called by {@link StickyHeaderItemDecoration} to fetch the position of the header item in the adapter
* that is used for (represents) item at specified position.
* @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
* @return int. Position of the header item in the adapter.
*/
int getHeaderPositionForItem(int itemPosition);

/**
* This method gets called by {@link StickyHeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
* @param headerPosition int. Position of the header item in the adapter.
* @return int. Layout resource id.
*/
int getHeaderLayout(int headerPosition);

/**
* This method gets called by {@link StickyHeaderItemDecoration} to setup the header View.
* @param header View. Header to set the data on.
* @param headerPosition int. Position of the header item in the adapter.
*/
void bindHeaderData(View header, int headerPosition);

/**
* This method gets called by {@link StickyHeaderItemDecoration} to verify whether the item represents a header.
* @param itemPosition int.
* @return true, if item at the specified adapter's position represents a header.
*/
boolean isHeader(int itemPosition);
}
4 changes: 2 additions & 2 deletions src/main/res/layout/activity_list_item_header.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
<TextView
android:id="@+id/title_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="60dp"
android:layout_marginLeft="@dimen/standard_list_item_size"
android:layout_marginStart="@dimen/standard_list_item_size"
android:layout_marginTop="10dp"
android:text="@string/placeholder_filename"
android:textSize="@dimen/activity_list_item_title_header_text_size"/>

</LinearLayout>
</LinearLayout>
2 changes: 1 addition & 1 deletion src/main/res/values/dims.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
<dimen name="activity_icon_height">32dp</dimen>
<dimen name="activity_icon_layout_right_end_margin">24dp</dimen>
<dimen name="activity_list_item_grid_layout_left_start_margin">-3dp</dimen>
<dimen name="activity_list_item_title_header_text_size">20sp</dimen>
<dimen name="activity_list_item_title_header_text_size">16sp</dimen>
<dimen name="activity_list_layout_recycler_view_margin">-3dp</dimen>
<dimen name="activity_row_layout_height">48dp</dimen>
<dimen name="notification_icon_width">32dp</dimen>
Expand Down
Loading