Skip to content

Commit 7ad1bc5

Browse files
tobiasKaminskyalperozturk96
authored andcommitted
Show avatars in activity list
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
1 parent 1b1d9c2 commit 7ad1bc5

File tree

8 files changed

+307
-29
lines changed

8 files changed

+307
-29
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
5+
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
6+
* SPDX-License-Identifier: GPL-3.0-or-later
7+
*/
8+
package com.nextcloud.utils.text;
9+
10+
import android.graphics.drawable.Drawable;
11+
12+
import androidx.annotation.NonNull;
13+
import thirdparties.fresco.BetterImageSpan;
14+
15+
public class Spans {
16+
17+
public static class MentionChipSpan extends BetterImageSpan {
18+
public String id;
19+
public CharSequence label;
20+
21+
public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, CharSequence label) {
22+
super(drawable, verticalAlignment);
23+
this.id = id;
24+
this.label = label;
25+
}
26+
27+
public String getId() {
28+
return this.id;
29+
}
30+
31+
public CharSequence getLabel() {
32+
return this.label;
33+
}
34+
35+
public void setId(String id) {
36+
this.id = id;
37+
}
38+
39+
public void setLabel(CharSequence label) {
40+
this.label = label;
41+
}
42+
43+
public boolean equals(final Object o) {
44+
if (o == this) {
45+
return true;
46+
}
47+
if (!(o instanceof MentionChipSpan)) {
48+
return false;
49+
}
50+
final MentionChipSpan other = (MentionChipSpan) o;
51+
if (!other.canEqual((Object) this)) {
52+
return false;
53+
}
54+
final Object this$id = this.getId();
55+
final Object other$id = other.getId();
56+
if (this$id == null ? other$id != null : !this$id.equals(other$id)) {
57+
return false;
58+
}
59+
final Object this$label = this.getLabel();
60+
final Object other$label = other.getLabel();
61+
62+
return this$label == null ? other$label == null : this$label.equals(other$label);
63+
}
64+
65+
protected boolean canEqual(final Object other) {
66+
return other instanceof MentionChipSpan;
67+
}
68+
69+
public int hashCode() {
70+
final int PRIME = 59;
71+
int result = 1;
72+
final Object $id = this.getId();
73+
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
74+
final Object $label = this.getLabel();
75+
return result * PRIME + ($label == null ? 43 : $label.hashCode());
76+
}
77+
78+
public String toString() {
79+
return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")";
80+
}
81+
}
82+
}

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

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import android.content.res.Resources;
1717
import android.graphics.Color;
1818
import android.graphics.PorterDuff;
19+
import android.graphics.drawable.Drawable;
1920
import android.os.Handler;
2021
import android.os.Looper;
2122
import android.text.Spannable;
@@ -24,7 +25,6 @@
2425
import android.text.TextUtils;
2526
import android.text.format.DateFormat;
2627
import android.text.format.DateUtils;
27-
import android.text.method.LinkMovementMethod;
2828
import android.text.style.ClickableSpan;
2929
import android.text.style.ForegroundColorSpan;
3030
import android.text.style.StyleSpan;
@@ -35,10 +35,12 @@
3535
import android.widget.LinearLayout;
3636
import android.widget.TextView;
3737

38+
import com.google.android.material.chip.ChipDrawable;
3839
import com.nextcloud.client.account.CurrentAccountProvider;
3940
import com.nextcloud.client.network.ClientFactory;
4041
import com.nextcloud.common.NextcloudClient;
4142
import com.nextcloud.utils.GlideHelper;
43+
import com.nextcloud.utils.text.Spans;
4244
import com.owncloud.android.MainApp;
4345
import com.owncloud.android.R;
4446
import com.owncloud.android.databinding.ActivityListItemBinding;
@@ -61,11 +63,13 @@
6163

6264
import androidx.annotation.NonNull;
6365
import androidx.recyclerview.widget.RecyclerView;
66+
import thirdparties.fresco.BetterImageSpan;
6467

6568
/**
6669
* Adapter for the activity view.
6770
*/
68-
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter {
71+
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter,
72+
DisplayUtils.AvatarGenerationListener {
6973

7074
static final int HEADER_TYPE = 100;
7175
static final int ACTIVITY_TYPE = 101;
@@ -147,9 +151,8 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
147151

148152
if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
149153
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
150-
activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance());
151-
activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()),
152-
TextView.BufferType.SPANNABLE);
154+
activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()));
155+
153156
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
154157
} else if (!TextUtils.isEmpty(activity.getSubject())) {
155158
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
@@ -275,6 +278,17 @@ private ImageView createThumbnailNew(PreviewObject previewObject, List<RichObjec
275278
return imageView;
276279
}
277280

281+
private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text) {
282+
ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
283+
chip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
284+
chip.setLayoutDirection(context.getResources().getConfiguration().getLayoutDirection());
285+
chip.setText(text);
286+
chip.setChipIconResource(R.drawable.accent_circle);
287+
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
288+
289+
return chip;
290+
}
291+
278292
private SpannableStringBuilder addClickablePart(RichElement richElement) {
279293
String text = richElement.getRichSubject();
280294
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
@@ -286,28 +300,53 @@ private SpannableStringBuilder addClickablePart(RichElement richElement) {
286300
final String clickString = text.substring(idx1 + 1, idx2 - 1);
287301
final RichObject richObject = searchObjectByName(richElement.getRichObjectList(), clickString);
288302
if (richObject != null) {
289-
String name = richObject.getName();
290-
ssb.replace(idx1, idx2, name);
291-
text = ssb.toString();
292-
idx2 = idx1 + name.length();
293-
ssb.setSpan(new ClickableSpan() {
294-
@Override
295-
public void onClick(@NonNull View widget) {
296-
activityListInterface.onActivityClicked(richObject);
297-
}
298-
299-
@Override
300-
public void updateDrawState(@NonNull TextPaint ds) {
301-
ds.setUnderlineText(false);
302-
}
303-
}, idx1, idx2, 0);
304-
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0);
305-
ssb.setSpan(
306-
new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)),
307-
idx1,
308-
idx2,
309-
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
310-
);
303+
if ("user".equals(richObject.getType())) {
304+
String name = richObject.getName();
305+
306+
ChipDrawable drawableForChip = getDrawableForMentionChipSpan(R.xml.chip_others, name);
307+
308+
Spans.MentionChipSpan mentionChipSpan = new Spans.MentionChipSpan(drawableForChip,
309+
BetterImageSpan.ALIGN_CENTER,
310+
richObject.getId(),
311+
name
312+
);
313+
314+
DisplayUtils.setAvatar(
315+
currentAccountProvider.getUser(),
316+
richObject.getId(),
317+
name,
318+
this,
319+
context.getResources().getDimension(R.dimen.avatar_icon_radius),
320+
context.getResources(),
321+
drawableForChip,
322+
context
323+
);
324+
325+
ssb.setSpan(mentionChipSpan, idx1, idx2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
326+
} else {
327+
String name = richObject.getName();
328+
ssb.replace(idx1, idx2, name);
329+
text = ssb.toString();
330+
idx2 = idx1 + name.length();
331+
ssb.setSpan(new ClickableSpan() {
332+
@Override
333+
public void onClick(@NonNull View widget) {
334+
activityListInterface.onActivityClicked(richObject);
335+
}
336+
337+
@Override
338+
public void updateDrawState(@NonNull TextPaint ds) {
339+
ds.setUnderlineText(false);
340+
}
341+
}, idx1, idx2, 0);
342+
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0);
343+
ssb.setSpan(
344+
new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)),
345+
idx1,
346+
idx2,
347+
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
348+
);
349+
}
311350
}
312351
idx1 = text.indexOf('{', idx2);
313352
}
@@ -396,6 +435,16 @@ public boolean isHeader(int itemPosition) {
396435
return this.getItemViewType(itemPosition) == HEADER_TYPE;
397436
}
398437

438+
@Override
439+
public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
440+
((ChipDrawable) callContext).setChipIcon(avatarDrawable);
441+
}
442+
443+
@Override
444+
public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
445+
return true;
446+
}
447+
399448
protected class ActivityViewHolder extends RecyclerView.ViewHolder {
400449

401450
ActivityListItemBinding binding;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015-present, Facebook, Inc. and its affiliates.
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
package thirdparties.fresco
7+
8+
import android.graphics.Canvas
9+
import android.graphics.Paint
10+
import android.graphics.Rect
11+
import android.graphics.drawable.Drawable
12+
import android.text.style.ReplacementSpan
13+
import androidx.annotation.IntDef
14+
15+
/**
16+
* A better implementation of image spans that also supports centering images against the text.
17+
*
18+
* In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with
19+
* `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`.
20+
*
21+
* There are 2 main differences between BetterImageSpan and ImageSpan:
22+
* 1. Pass in ALIGN_CENTER to center images against the text.
23+
* 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text:
24+
* DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE
25+
* which can lead to unnecessary whitespace.
26+
*/
27+
open class BetterImageSpan @JvmOverloads constructor(
28+
val drawable: Drawable,
29+
@param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE
30+
) : ReplacementSpan() {
31+
@Suppress("Detekt.SpreadOperator")
32+
@IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER])
33+
@Retention(AnnotationRetention.SOURCE)
34+
annotation class BetterImageSpanAlignment
35+
36+
private var mWidth = 0
37+
private var mHeight = 0
38+
private var mBounds: Rect? = null
39+
private val mFontMetricsInt = Paint.FontMetricsInt()
40+
41+
init {
42+
updateBounds()
43+
}
44+
45+
/**
46+
* Returns the width of the image span and increases the height if font metrics are available.
47+
*/
48+
override fun getSize(
49+
paint: Paint,
50+
text: CharSequence,
51+
start: Int,
52+
end: Int,
53+
fontMetrics: Paint.FontMetricsInt?
54+
): Int {
55+
updateBounds()
56+
if (fontMetrics == null) {
57+
return mWidth
58+
}
59+
val offsetAbove = getOffsetAboveBaseline(fontMetrics)
60+
val offsetBelow = mHeight + offsetAbove
61+
if (offsetAbove < fontMetrics.ascent) {
62+
fontMetrics.ascent = offsetAbove
63+
}
64+
if (offsetAbove < fontMetrics.top) {
65+
fontMetrics.top = offsetAbove
66+
}
67+
if (offsetBelow > fontMetrics.descent) {
68+
fontMetrics.descent = offsetBelow
69+
}
70+
if (offsetBelow > fontMetrics.bottom) {
71+
fontMetrics.bottom = offsetBelow
72+
}
73+
return mWidth
74+
}
75+
76+
override fun draw(
77+
canvas: Canvas,
78+
text: CharSequence,
79+
start: Int,
80+
end: Int,
81+
x: Float,
82+
top: Int,
83+
y: Int,
84+
bottom: Int,
85+
paint: Paint
86+
) {
87+
paint.getFontMetricsInt(mFontMetricsInt)
88+
val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt)
89+
canvas.translate(x, iconTop.toFloat())
90+
drawable.draw(canvas)
91+
canvas.translate(-x, -iconTop.toFloat())
92+
}
93+
94+
private fun updateBounds() {
95+
mBounds = drawable.bounds
96+
mWidth = mBounds!!.width()
97+
mHeight = mBounds!!.height()
98+
}
99+
100+
private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int = when (mAlignment) {
101+
ALIGN_BOTTOM -> fm.descent - mHeight
102+
ALIGN_CENTER -> {
103+
val textHeight = fm.descent - fm.ascent
104+
val offset = (textHeight - mHeight) / 2
105+
fm.ascent + offset
106+
}
107+
108+
ALIGN_BASELINE -> -mHeight
109+
else -> -mHeight
110+
}
111+
112+
companion object {
113+
const val ALIGN_BOTTOM = 0
114+
const val ALIGN_BASELINE = 1
115+
const val ALIGN_CENTER = 2
116+
}
117+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Nextcloud Talk - Android Client
3+
~
4+
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
5+
~ SPDX-License-Identifier: GPL-3.0-or-later
6+
-->
7+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
8+
android:shape="oval">
9+
<solid android:color="@color/colorPrimary" />
10+
</shape>

app/src/main/res/layout/activity_list_item.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434

3535
<TextView
3636
android:id="@+id/subject"
37-
android:layout_width="wrap_content"
38-
android:layout_height="wrap_content"
37+
android:layout_width="match_parent"
38+
android:layout_height="0dp"
39+
android:layout_weight="1"
3940
android:ellipsize="end"
4041
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
4142
android:paddingTop="@dimen/standard_padding"
43+
android:paddingBottom="@dimen/standard_margin"
4244
android:paddingEnd="@dimen/zero"
4345
android:textAppearance="?android:attr/textAppearanceListItem"
4446
android:textSize="@dimen/two_line_primary_text_size"

0 commit comments

Comments
 (0)