Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 82 additions & 0 deletions app/src/main/java/com/nextcloud/utils/text/Spans.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.utils.text;

import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;
import thirdparties.fresco.BetterImageSpan;

public class Spans {

public static class MentionChipSpan extends BetterImageSpan {
public String id;
public CharSequence label;

public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, CharSequence label) {
super(drawable, verticalAlignment);
this.id = id;
this.label = label;
}

public String getId() {
return this.id;
}

public CharSequence getLabel() {
return this.label;
}

public void setId(String id) {
this.id = id;
}

public void setLabel(CharSequence label) {
this.label = label;
}

public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof MentionChipSpan)) {
return false;
}
final MentionChipSpan other = (MentionChipSpan) o;
if (!other.canEqual((Object) this)) {
return false;
}
final Object this$id = this.getId();
final Object other$id = other.getId();
if (this$id == null ? other$id != null : !this$id.equals(other$id)) {
return false;
}
final Object this$label = this.getLabel();
final Object other$label = other.getLabel();

return this$label == null ? other$label == null : this$label.equals(other$label);
}

protected boolean canEqual(final Object other) {
return other instanceof MentionChipSpan;
}

public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $id = this.getId();
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
final Object $label = this.getLabel();
return result * PRIME + ($label == null ? 43 : $label.hashCode());
}

public String toString() {
return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.text.Spannable;
Expand All @@ -24,7 +25,6 @@
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
Expand All @@ -35,10 +35,12 @@
import android.widget.LinearLayout;
import android.widget.TextView;

import com.google.android.material.chip.ChipDrawable;
import com.nextcloud.client.account.CurrentAccountProvider;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.common.NextcloudClient;
import com.nextcloud.utils.GlideHelper;
import com.nextcloud.utils.text.Spans;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.databinding.ActivityListItemBinding;
Expand All @@ -61,11 +63,13 @@

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import thirdparties.fresco.BetterImageSpan;

/**
* Adapter for the activity view.
*/
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter {
public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements StickyHeaderAdapter,
DisplayUtils.AvatarGenerationListener {

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

if (!TextUtils.isEmpty(activity.getRichSubjectElement().getRichSubject())) {
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance());
activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()),
TextView.BufferType.SPANNABLE);
activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()));

activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
} else if (!TextUtils.isEmpty(activity.getSubject())) {
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
Expand Down Expand Up @@ -275,6 +278,17 @@ private ImageView createThumbnailNew(PreviewObject previewObject, List<RichObjec
return imageView;
}

private ChipDrawable getDrawableForMentionChipSpan(int chipResource, String text) {
ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
chip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
chip.setLayoutDirection(context.getResources().getConfiguration().getLayoutDirection());
chip.setText(text);
chip.setChipIconResource(R.drawable.accent_circle);
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());

return chip;
}

private SpannableStringBuilder addClickablePart(RichElement richElement) {
String text = richElement.getRichSubject();
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
Expand All @@ -286,28 +300,53 @@ private SpannableStringBuilder addClickablePart(RichElement richElement) {
final String clickString = text.substring(idx1 + 1, idx2 - 1);
final RichObject richObject = searchObjectByName(richElement.getRichObjectList(), clickString);
if (richObject != null) {
String name = richObject.getName();
ssb.replace(idx1, idx2, name);
text = ssb.toString();
idx2 = idx1 + name.length();
ssb.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
activityListInterface.onActivityClicked(richObject);
}

@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(false);
}
}, idx1, idx2, 0);
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0);
ssb.setSpan(
new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)),
idx1,
idx2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
if ("user".equals(richObject.getType())) {
String name = richObject.getName();

ChipDrawable drawableForChip = getDrawableForMentionChipSpan(R.xml.chip_others, name);

Spans.MentionChipSpan mentionChipSpan = new Spans.MentionChipSpan(drawableForChip,
BetterImageSpan.ALIGN_CENTER,
richObject.getId(),
name
);

DisplayUtils.setAvatar(
currentAccountProvider.getUser(),
richObject.getId(),
name,
this,
context.getResources().getDimension(R.dimen.avatar_icon_radius),
context.getResources(),
drawableForChip,
context
);

ssb.setSpan(mentionChipSpan, idx1, idx2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} else {
String name = richObject.getName();
ssb.replace(idx1, idx2, name);
text = ssb.toString();
idx2 = idx1 + name.length();
ssb.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
activityListInterface.onActivityClicked(richObject);
}

@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(false);
}
}, idx1, idx2, 0);
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), idx1, idx2, 0);
ssb.setSpan(
new ForegroundColorSpan(context.getResources().getColor(R.color.text_color)),
idx1,
idx2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
idx1 = text.indexOf('{', idx2);
}
Expand Down Expand Up @@ -396,6 +435,16 @@ public boolean isHeader(int itemPosition) {
return this.getItemViewType(itemPosition) == HEADER_TYPE;
}

@Override
public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
((ChipDrawable) callContext).setChipIcon(avatarDrawable);
}

@Override
public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
return true;
}

protected class ActivityViewHolder extends RecyclerView.ViewHolder {

ActivityListItemBinding binding;
Expand Down
117 changes: 117 additions & 0 deletions app/src/main/java/third_parties/fresco/BetterImageSpan.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2015-present, Facebook, Inc. and its affiliates.
* SPDX-License-Identifier: MIT
*/

package thirdparties.fresco

import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import androidx.annotation.IntDef

/**
* A better implementation of image spans that also supports centering images against the text.
*
* In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with
* `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`.
*
* There are 2 main differences between BetterImageSpan and ImageSpan:
* 1. Pass in ALIGN_CENTER to center images against the text.
* 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text:
* DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE
* which can lead to unnecessary whitespace.
*/
open class BetterImageSpan @JvmOverloads constructor(
val drawable: Drawable,
@param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE
) : ReplacementSpan() {
@Suppress("Detekt.SpreadOperator")
@IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER])
@Retention(AnnotationRetention.SOURCE)
annotation class BetterImageSpanAlignment

private var mWidth = 0
private var mHeight = 0
private var mBounds: Rect? = null
private val mFontMetricsInt = Paint.FontMetricsInt()

init {
updateBounds()
}

/**
* Returns the width of the image span and increases the height if font metrics are available.
*/
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fontMetrics: Paint.FontMetricsInt?
): Int {
updateBounds()
if (fontMetrics == null) {
return mWidth
}
val offsetAbove = getOffsetAboveBaseline(fontMetrics)
val offsetBelow = mHeight + offsetAbove
if (offsetAbove < fontMetrics.ascent) {
fontMetrics.ascent = offsetAbove
}
if (offsetAbove < fontMetrics.top) {
fontMetrics.top = offsetAbove
}
if (offsetBelow > fontMetrics.descent) {
fontMetrics.descent = offsetBelow
}
if (offsetBelow > fontMetrics.bottom) {
fontMetrics.bottom = offsetBelow
}
return mWidth
}

override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
paint.getFontMetricsInt(mFontMetricsInt)
val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt)
canvas.translate(x, iconTop.toFloat())
drawable.draw(canvas)
canvas.translate(-x, -iconTop.toFloat())
}

private fun updateBounds() {
mBounds = drawable.bounds
mWidth = mBounds!!.width()
mHeight = mBounds!!.height()
}

private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int = when (mAlignment) {
ALIGN_BOTTOM -> fm.descent - mHeight
ALIGN_CENTER -> {
val textHeight = fm.descent - fm.ascent
val offset = (textHeight - mHeight) / 2
fm.ascent + offset
}

ALIGN_BASELINE -> -mHeight
else -> -mHeight
}

companion object {
const val ALIGN_BOTTOM = 0
const val ALIGN_BASELINE = 1
const val ALIGN_CENTER = 2
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/accent_circle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary" />
</shape>
6 changes: 4 additions & 2 deletions app/src/main/res/layout/activity_list_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@

<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:ellipsize="end"
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
android:paddingTop="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_margin"
android:paddingEnd="@dimen/zero"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="@dimen/two_line_primary_text_size"
Expand Down
Loading
Loading