mirror of
https://github.com/nextcloud/android.git
synced 2025-07-23 01:02:01 +00:00
82
app/src/main/java/com/nextcloud/utils/text/Spans.java
Normal file
82
app/src/main/java/com/nextcloud/utils/text/Spans.java
Normal file
@ -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 third_parties.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() + ")";
|
||||
}
|
||||
}
|
||||
}
|
@ -16,8 +16,10 @@ import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
@ -40,9 +42,11 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.StreamEncoder;
|
||||
import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
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.text.Spans;
|
||||
import com.owncloud.android.MainApp;
|
||||
import com.owncloud.android.R;
|
||||
import com.owncloud.android.databinding.ActivityListItemBinding;
|
||||
@ -64,9 +68,13 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.XmlRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import third_parties.fresco.BetterImageSpan;
|
||||
|
||||
/**
|
||||
* Adapter for the activity view.
|
||||
@ -154,6 +162,13 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||
activityViewHolder.binding.subject.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
activityViewHolder.binding.subject.setText(addClickablePart(activity.getRichSubjectElement()),
|
||||
TextView.BufferType.SPANNABLE);
|
||||
|
||||
activityViewHolder.binding.subject.setText(searchAndReplaceWithMentionSpan("actor",
|
||||
activity.getRichSubjectElement().getRichSubject(),
|
||||
"1",
|
||||
"label",
|
||||
R.xml.chip_others));
|
||||
|
||||
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
|
||||
} else if (!TextUtils.isEmpty(activity.getSubject())) {
|
||||
activityViewHolder.binding.subject.setVisibility(View.VISIBLE);
|
||||
@ -288,6 +303,65 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||
.into(itemViewType);
|
||||
}
|
||||
|
||||
/**
|
||||
* c&p from Talk: DisplayUtils:227
|
||||
*
|
||||
* @return Spannable
|
||||
*/
|
||||
private Spannable searchAndReplaceWithMentionSpan(
|
||||
String key,
|
||||
String text,
|
||||
String id,
|
||||
String label,
|
||||
@XmlRes int chipXmlRes) {
|
||||
Spannable spannableString = new SpannableString(text);
|
||||
String stringText = text.toString();
|
||||
String keyWithBrackets = "{" + key + "}";
|
||||
Matcher m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
|
||||
.matcher(spannableString);
|
||||
ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View view) {
|
||||
//EventBus.getDefault().post(new UserMentionClickEvent(id));
|
||||
}
|
||||
};
|
||||
|
||||
int lastStartIndex = 0;
|
||||
Spans.MentionChipSpan mentionChipSpan;
|
||||
|
||||
while (m.find()) {
|
||||
int start = stringText.indexOf(m.group(), lastStartIndex);
|
||||
int end = start + m.group().length();
|
||||
lastStartIndex = end;
|
||||
Drawable drawableForChip = getDrawableForMentionChipSpan(
|
||||
chipXmlRes
|
||||
);
|
||||
mentionChipSpan = new Spans.MentionChipSpan(
|
||||
drawableForChip,
|
||||
BetterImageSpan.ALIGN_CENTER,
|
||||
id,
|
||||
label
|
||||
);
|
||||
spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
// if (chipXmlRes == R.xml.chip_you) {
|
||||
// spannableString.setSpan(
|
||||
// viewThemeUtils.talk.themeForegroundColorSpan(context),
|
||||
// start,
|
||||
// end,
|
||||
// Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
// );
|
||||
// }
|
||||
// if ("user" == type && conversationUser.userId != id && !isFederated) {
|
||||
// spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
// }
|
||||
}
|
||||
return spannableString;
|
||||
}
|
||||
|
||||
private Drawable getDrawableForMentionChipSpan(int chipResource) {
|
||||
return ChipDrawable.createFromResource(context, chipResource);
|
||||
}
|
||||
|
||||
private SpannableStringBuilder addClickablePart(RichElement richElement) {
|
||||
String text = richElement.getRichSubject();
|
||||
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
|
||||
|
120
app/src/main/java/third_parties/fresco/BetterImageSpan.kt
Normal file
120
app/src/main/java/third_parties/fresco/BetterImageSpan.kt
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package third_parties.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() {
|
||||
@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 {
|
||||
return 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
|
||||
}
|
||||
}
|
@ -32,17 +32,29 @@
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
|
||||
android:paddingTop="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
tools:text="@string/placeholder_filename" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="username" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subject"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
|
||||
android:paddingTop="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
tools:text="@string/placeholder_filename" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message"
|
||||
|
@ -461,4 +461,8 @@
|
||||
<item name="postSplashScreenTheme">@style/Theme.ownCloud.Launcher</item>
|
||||
</style>
|
||||
|
||||
<style name="ChipIncomingTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
|
||||
<item name="android:textColor">#de000000</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
13
app/src/main/res/xml/chip_others.xml
Normal file
13
app/src/main/res/xml/chip_others.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?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
|
||||
-->
|
||||
<chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:textAppearance="@style/ChipIncomingTextAppearance"
|
||||
app:chipBackgroundColor="#deffffff"
|
||||
app:chipCornerRadius="@dimen/standard_padding"
|
||||
app:chipStrokeWidth="@dimen/zero"
|
||||
app:closeIconEnabled="false" />
|
Reference in New Issue
Block a user