mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Clean code
This commit is contained in:
parent
88755a79b4
commit
f454078c6b
32 changed files with 430 additions and 489 deletions
|
@ -1,5 +1,6 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="160" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
|
@ -118,7 +118,7 @@ class CommonTestHelper(context: Context) {
|
|||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
//noop
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
|
|
|
@ -26,7 +26,7 @@ import im.vector.matrix.android.api.util.Optional
|
|||
*/
|
||||
interface ReadService {
|
||||
|
||||
enum class MarkAsReadParams{
|
||||
enum class MarkAsReadParams {
|
||||
READ_RECEIPT,
|
||||
READ_MARKER,
|
||||
BOTH
|
||||
|
|
|
@ -70,7 +70,6 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
|||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.database.query.whereType
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
|
|
|
@ -320,7 +320,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
// al devices =
|
||||
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
|
||||
|
||||
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
|
||||
if (!models.isNullOrEmpty()) {
|
||||
val workingCopy = models.toMutableMap()
|
||||
|
|
|
@ -36,7 +36,6 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti
|
|||
import io.realm.Realm
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.createObject
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun ChunkEntity.deleteOnCascade() {
|
||||
|
@ -110,7 +109,6 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
eventEntity: EventEntity,
|
||||
direction: PaginationDirection,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
|
||||
|
||||
val eventId = eventEntity.eventId
|
||||
if (timelineEvents.find(eventId) != null) {
|
||||
return
|
||||
|
@ -134,8 +132,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
this.senderName = roomMemberContent?.displayName
|
||||
if (roomMemberContent?.displayName != null) {
|
||||
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
||||
roomMemberContent != it &&
|
||||
it?.displayName == roomMemberContent.displayName
|
||||
it != roomMemberContent && it?.displayName == roomMemberContent.displayName
|
||||
} == null
|
||||
isUniqueDisplayName = if (isLastForward) {
|
||||
val isLiveUnique = RoomMemberSummaryEntity
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.database.model
|
|||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class CurrentStateEventEntity(var eventId: String = "",
|
||||
var root: EventEntity? = null,
|
||||
|
|
|
@ -21,9 +21,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
|||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class EventEntity(@PrimaryKey var eventId: String = "",
|
||||
|
|
|
@ -20,7 +20,6 @@ import io.realm.RealmObject
|
|||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
@Index var eventId: String = "",
|
||||
|
|
|
@ -29,7 +29,8 @@ internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: Strin
|
|||
.equalTo(CurrentStateEventEntityFields.TYPE, type)
|
||||
}
|
||||
|
||||
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String): RealmQuery<CurrentStateEventEntity> {
|
||||
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
|
||||
: RealmQuery<CurrentStateEventEntity> {
|
||||
return where(realm = realm, roomId = roomId, type = type)
|
||||
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
|
||||
}
|
||||
|
@ -49,5 +50,3 @@ private fun create(realm: Realm, roomId: String, stateKey: String, type: String)
|
|||
this.stateKey = stateKey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, e
|
|||
|
||||
internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm,
|
||||
roomId: String): RealmQuery<TimelineEventEntity> {
|
||||
return realm.where<TimelineEventEntity>().equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||
return realm.where<TimelineEventEntity>()
|
||||
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
|
||||
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
|
||||
|
|
|
@ -26,7 +26,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
|
|||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.AuthModule
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
|||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface NetworkCallbackStrategy {
|
||||
|
@ -47,7 +48,7 @@ internal class FallbackNetworkCallbackStrategy @Inject constructor(private val c
|
|||
}
|
||||
}
|
||||
|
||||
@TargetApi(android.os.Build.VERSION_CODES.N)
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy {
|
||||
|
||||
private var hasChangedCallback: (() -> Unit)? = null
|
||||
|
|
|
@ -42,10 +42,10 @@ interface NetworkConnectivityChecker {
|
|||
}
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val homeServerPinger: HomeServerPinger,
|
||||
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger,
|
||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||
private val networkCallbackStrategy: NetworkCallbackStrategy) : NetworkConnectivityChecker {
|
||||
private val networkCallbackStrategy: NetworkCallbackStrategy)
|
||||
: NetworkConnectivityChecker {
|
||||
|
||||
private val hasInternetAccess = AtomicBoolean(true)
|
||||
private val listeners = Collections.synchronizedSet(LinkedHashSet<NetworkConnectivityChecker.Listener>())
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.internal.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Binds
|
||||
import dagger.Lazy
|
||||
|
@ -194,7 +195,7 @@ internal abstract class SessionModule {
|
|||
fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider<FallbackNetworkCallbackStrategy>,
|
||||
preferredNetworkCallbackStrategy: Provider<PreferredNetworkCallbackStrategy>
|
||||
): NetworkCallbackStrategy {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
preferredNetworkCallbackStrategy.get()
|
||||
} else {
|
||||
fallbackNetworkCallbackStrategy.get()
|
||||
|
@ -241,4 +242,3 @@ internal abstract class SessionModule {
|
|||
@Binds
|
||||
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,6 @@ internal interface CapabilitiesAPI {
|
|||
/**
|
||||
* Request the versions
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_+"versions")
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
|
||||
fun getVersions(): Call<Unit>
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.homeserver
|
|||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
|||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
|
@ -36,7 +35,6 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
|
|||
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||
import im.vector.matrix.android.internal.database.query.isEventRead
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.database.query.whereType
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.draft
|
|||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
|
|
|
@ -122,5 +122,4 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||
private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean {
|
||||
return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.database.query.isReadMarkerMoreRecent
|
|||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.sync.ReadReceiptHandler
|
||||
|
|
|
@ -16,12 +16,10 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.Operation
|
||||
import androidx.work.WorkManager
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
|
|
|
@ -48,7 +48,6 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
|
||||
|
@ -57,7 +56,6 @@ import im.vector.matrix.android.internal.util.StringProvider
|
|||
import kotlinx.coroutines.launch
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.send.SendState
|
|||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
|
@ -665,7 +664,6 @@ internal class DefaultTimeline(
|
|||
val params = GetContextOfEventTask.Params(roomId, eventId)
|
||||
cancelableBag += contextOfEventTask.configureWith(params) {
|
||||
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
postSnapshot()
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
|||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
|
@ -168,13 +167,19 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
|||
|
||||
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
roomId.isBlank()
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
if (currentChunk != currentLiveChunk) {
|
||||
currentChunk.isLastForward = true
|
||||
currentLiveChunk?.deleteOnCascade()
|
||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||
latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES)
|
||||
latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
||||
realm,
|
||||
roomId,
|
||||
includesSending = true,
|
||||
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -237,7 +242,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
|||
}
|
||||
if (shouldUpdateSummary) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES)
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
||||
realm,
|
||||
roomId,
|
||||
includesSending = true,
|
||||
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
|
||||
)
|
||||
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||
}
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.sync.job
|
|||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.failure.isTokenError
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
|
|
|
@ -158,4 +158,5 @@ Formatter\.formatFileSize===1
|
|||
Formatter\.formatShortFileSize===1
|
||||
|
||||
### Use kotlin stdlib to test or compare strings
|
||||
android\.text\.TextUtils
|
||||
# DISABLED
|
||||
# android\.text\.TextUtils
|
||||
|
|
|
@ -1,445 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotx.core.platform;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.text.Layout;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A {@link android.widget.TextView} that ellipsizes more intelligently.
|
||||
* This class supports ellipsizing multiline text through setting {@code android:ellipsize}
|
||||
* and {@code android:maxLines}.
|
||||
* <p/>
|
||||
* Note: {@link android.text.TextUtils.TruncateAt#MARQUEE} ellipsizing type is not supported.
|
||||
* This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
|
||||
*/
|
||||
public class EllipsizingTextView extends AppCompatTextView {
|
||||
public static final int ELLIPSIZE_ALPHA = 0x88;
|
||||
private SpannableString ELLIPSIS = new SpannableString("\u2026");
|
||||
|
||||
private static final Pattern DEFAULT_END_PUNCTUATION
|
||||
= Pattern.compile("[\\.!?,;:\u2026]*$", Pattern.DOTALL);
|
||||
private final List<EllipsizeListener> mEllipsizeListeners = new ArrayList<>();
|
||||
private EllipsizeStrategy mEllipsizeStrategy;
|
||||
private boolean isEllipsized;
|
||||
private boolean isStale;
|
||||
private boolean programmaticChange;
|
||||
private CharSequence mFullText;
|
||||
private int mMaxLines;
|
||||
private float mLineSpacingMult = 1.0f;
|
||||
private float mLineAddVertPad = 0.0f;
|
||||
|
||||
/**
|
||||
* The end punctuation which will be removed when appending {@link #ELLIPSIS}.
|
||||
*/
|
||||
private Pattern mEndPunctPattern;
|
||||
|
||||
public EllipsizingTextView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
|
||||
public EllipsizingTextView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.textViewStyle);
|
||||
}
|
||||
|
||||
|
||||
public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs,
|
||||
new int[]{android.R.attr.maxLines, android.R.attr.ellipsize}, defStyle, 0);
|
||||
setMaxLines(a.getInt(0, Integer.MAX_VALUE));
|
||||
a.recycle();
|
||||
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION);
|
||||
final int currentTextColor = getCurrentTextColor();
|
||||
final int ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor));
|
||||
ELLIPSIS.setSpan(new ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
public void setEndPunctuationPattern(Pattern pattern) {
|
||||
mEndPunctPattern = pattern;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void addEllipsizeListener(@NonNull EllipsizeListener listener) {
|
||||
mEllipsizeListeners.add(listener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void removeEllipsizeListener(@NonNull EllipsizeListener listener) {
|
||||
mEllipsizeListeners.remove(listener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public boolean isEllipsized() {
|
||||
return isEllipsized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The maximum number of lines displayed in this {@link android.widget.TextView}.
|
||||
*/
|
||||
public int getMaxLines() {
|
||||
return mMaxLines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxLines(int maxLines) {
|
||||
super.setMaxLines(maxLines);
|
||||
mMaxLines = maxLines;
|
||||
isStale = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the last fully visible line is being ellipsized.
|
||||
*
|
||||
* @return {@code true} if the last fully visible line is being ellipsized;
|
||||
* otherwise, returns {@code false}.
|
||||
*/
|
||||
public boolean ellipsizingLastFullyVisibleLine() {
|
||||
return mMaxLines == Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineSpacing(float add, float mult) {
|
||||
mLineAddVertPad = add;
|
||||
mLineSpacingMult = mult;
|
||||
super.setLineSpacing(add, mult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(CharSequence text, BufferType type) {
|
||||
if (!programmaticChange) {
|
||||
mFullText = text instanceof Spanned ? (Spanned) text : text;
|
||||
isStale = true;
|
||||
}
|
||||
super.setText(text, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
if (ellipsizingLastFullyVisibleLine()) {
|
||||
isStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPadding(int left, int top, int right, int bottom) {
|
||||
super.setPadding(left, top, right, bottom);
|
||||
if (ellipsizingLastFullyVisibleLine()) {
|
||||
isStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
if (isStale) {
|
||||
resetText();
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ellipsized text if appropriate.
|
||||
*/
|
||||
private void resetText() {
|
||||
int maxLines = getMaxLines();
|
||||
CharSequence workingText = mFullText;
|
||||
boolean ellipsized = false;
|
||||
|
||||
if (maxLines != -1) {
|
||||
if (mEllipsizeStrategy == null) setEllipsize(null);
|
||||
workingText = mEllipsizeStrategy.processText(mFullText);
|
||||
ellipsized = !mEllipsizeStrategy.isInLayout(mFullText);
|
||||
}
|
||||
|
||||
if (!workingText.equals(getText())) {
|
||||
programmaticChange = true;
|
||||
try {
|
||||
setText(workingText);
|
||||
} finally {
|
||||
programmaticChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
isStale = false;
|
||||
if (ellipsized != isEllipsized) {
|
||||
isEllipsized = ellipsized;
|
||||
for (EllipsizeListener listener : mEllipsizeListeners) {
|
||||
listener.ellipsizeStateChanged(ellipsized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes words in the text that are longer than the view is wide to be ellipsized
|
||||
* instead of broken in the middle. Use {@code null} to turn off ellipsizing.
|
||||
* <p/>
|
||||
* Note: Method does nothing for {@link android.text.TextUtils.TruncateAt#MARQUEE}
|
||||
* ellipsizing type.
|
||||
*
|
||||
* @param where part of text to ellipsize
|
||||
*/
|
||||
@Override
|
||||
public void setEllipsize(TruncateAt where) {
|
||||
if (where == null) {
|
||||
mEllipsizeStrategy = new EllipsizeNoneStrategy();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (where) {
|
||||
case END:
|
||||
mEllipsizeStrategy = new EllipsizeEndStrategy();
|
||||
break;
|
||||
case START:
|
||||
mEllipsizeStrategy = new EllipsizeStartStrategy();
|
||||
break;
|
||||
case MIDDLE:
|
||||
mEllipsizeStrategy = new EllipsizeMiddleStrategy();
|
||||
break;
|
||||
case MARQUEE:
|
||||
default:
|
||||
mEllipsizeStrategy = new EllipsizeNoneStrategy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener that notifies when the ellipsize state has changed.
|
||||
*/
|
||||
public interface EllipsizeListener {
|
||||
void ellipsizeStateChanged(boolean ellipsized);
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for an ellipsize strategy.
|
||||
*/
|
||||
private abstract class EllipsizeStrategy {
|
||||
/**
|
||||
* Returns ellipsized text if the text does not fit inside of the layout;
|
||||
* otherwise, returns the full text.
|
||||
*
|
||||
* @param text text to process
|
||||
* @return Ellipsized text if the text does not fit inside of the layout;
|
||||
* otherwise, returns the full text.
|
||||
*/
|
||||
public CharSequence processText(CharSequence text) {
|
||||
return !isInLayout(text) ? createEllipsizedText(text) : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the text fits inside of the layout.
|
||||
*
|
||||
* @param text text to fit
|
||||
* @return {@code true} if the text fits inside of the layout;
|
||||
* otherwise, returns {@code false}.
|
||||
*/
|
||||
public boolean isInLayout(CharSequence text) {
|
||||
Layout layout = createWorkingLayout(text);
|
||||
return layout.getLineCount() <= getLinesCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a working layout with the given text.
|
||||
*
|
||||
* @param workingText text to create layout with
|
||||
* @return {@link android.text.Layout} with the given text.
|
||||
*/
|
||||
protected Layout createWorkingLayout(CharSequence workingText) {
|
||||
return new StaticLayout(workingText, getPaint(),
|
||||
getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(),
|
||||
Alignment.ALIGN_NORMAL, mLineSpacingMult,
|
||||
mLineAddVertPad, false /* includepad */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many lines of text we are allowed to display.
|
||||
*/
|
||||
protected int getLinesCount() {
|
||||
if (ellipsizingLastFullyVisibleLine()) {
|
||||
int fullyVisibleLinesCount = getFullyVisibleLinesCount();
|
||||
return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount;
|
||||
} else {
|
||||
return mMaxLines;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many lines of text we can display so their full height is visible.
|
||||
*/
|
||||
protected int getFullyVisibleLinesCount() {
|
||||
Layout layout = createWorkingLayout("");
|
||||
int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
|
||||
int lineHeight = layout.getLineBottom(0);
|
||||
return height / lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates ellipsized text from the given text.
|
||||
*
|
||||
* @param fullText text to ellipsize
|
||||
* @return Ellipsized text
|
||||
*/
|
||||
protected abstract CharSequence createEllipsizedText(CharSequence fullText);
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link EllipsizingTextView.EllipsizeStrategy} that
|
||||
* does not ellipsize text.
|
||||
*/
|
||||
private class EllipsizeNoneStrategy extends EllipsizeStrategy {
|
||||
@Override
|
||||
protected CharSequence createEllipsizedText(CharSequence fullText) {
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link EllipsizingTextView.EllipsizeStrategy} that
|
||||
* ellipsizes text at the end.
|
||||
*/
|
||||
private class EllipsizeEndStrategy extends EllipsizeStrategy {
|
||||
@Override
|
||||
protected CharSequence createEllipsizedText(CharSequence fullText) {
|
||||
Layout layout = createWorkingLayout(fullText);
|
||||
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
|
||||
int textLength = fullText.length();
|
||||
int cutOffLength = textLength - cutOffIndex;
|
||||
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
|
||||
CharSequence workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim();
|
||||
|
||||
while (!isInLayout(TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS))) {
|
||||
int lastSpace = TextUtils.lastIndexOf(workingText, ' ');
|
||||
if (lastSpace == -1) {
|
||||
break;
|
||||
}
|
||||
workingText = TextUtils.substring(workingText, 0, lastSpace).trim();
|
||||
}
|
||||
|
||||
workingText = TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS);
|
||||
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
|
||||
|
||||
if (fullText instanceof Spanned) {
|
||||
TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0);
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the end punctuation from a given text according to {@link #mEndPunctPattern}.
|
||||
*
|
||||
* @param workingText text to strip end punctuation from
|
||||
* @return Text without end punctuation.
|
||||
*/
|
||||
public String stripEndPunctuation(CharSequence workingText) {
|
||||
return mEndPunctPattern.matcher(workingText).replaceFirst("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link EllipsizingTextView.EllipsizeStrategy} that
|
||||
* ellipsizes text at the start.
|
||||
*/
|
||||
private class EllipsizeStartStrategy extends EllipsizeStrategy {
|
||||
@Override
|
||||
protected CharSequence createEllipsizedText(CharSequence fullText) {
|
||||
Layout layout = createWorkingLayout(fullText);
|
||||
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
|
||||
int textLength = fullText.length();
|
||||
int cutOffLength = textLength - cutOffIndex;
|
||||
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
|
||||
CharSequence workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim();
|
||||
|
||||
while (!isInLayout(TextUtils.concat(ELLIPSIS, workingText))) {
|
||||
int firstSpace = TextUtils.indexOf(workingText, ' ');
|
||||
if (firstSpace == -1) {
|
||||
break;
|
||||
}
|
||||
workingText = TextUtils.substring(workingText, firstSpace, workingText.length()).trim();
|
||||
}
|
||||
|
||||
workingText = TextUtils.concat(ELLIPSIS, workingText);
|
||||
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
|
||||
|
||||
if (fullText instanceof Spanned) {
|
||||
TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(),
|
||||
textLength, null, dest, 0);
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link EllipsizingTextView.EllipsizeStrategy} that
|
||||
* ellipsizes text in the middle.
|
||||
*/
|
||||
private class EllipsizeMiddleStrategy extends EllipsizeStrategy {
|
||||
@Override
|
||||
protected CharSequence createEllipsizedText(CharSequence fullText) {
|
||||
Layout layout = createWorkingLayout(fullText);
|
||||
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
|
||||
int textLength = fullText.length();
|
||||
int cutOffLength = textLength - cutOffIndex;
|
||||
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
|
||||
cutOffLength += cutOffIndex % 2; // Make it even.
|
||||
String firstPart = TextUtils.substring(
|
||||
fullText, 0, textLength / 2 - cutOffLength / 2).trim();
|
||||
String secondPart = TextUtils.substring(
|
||||
fullText, textLength / 2 + cutOffLength / 2, textLength).trim();
|
||||
|
||||
while (!isInLayout(TextUtils.concat(firstPart, ELLIPSIS, secondPart))) {
|
||||
int lastSpaceFirstPart = firstPart.lastIndexOf(' ');
|
||||
int firstSpaceSecondPart = secondPart.indexOf(' ');
|
||||
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break;
|
||||
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim();
|
||||
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length()).trim();
|
||||
}
|
||||
|
||||
SpannableStringBuilder firstDest = new SpannableStringBuilder(firstPart);
|
||||
SpannableStringBuilder secondDest = new SpannableStringBuilder(secondPart);
|
||||
|
||||
if (fullText instanceof Spanned) {
|
||||
TextUtils.copySpansFrom((Spanned) fullText, 0, firstPart.length(),
|
||||
null, firstDest, 0);
|
||||
TextUtils.copySpansFrom((Spanned) fullText, textLength - secondPart.length(),
|
||||
textLength, null, secondDest, 0);
|
||||
}
|
||||
return TextUtils.concat(firstDest, ELLIPSIS, secondDest);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package im.vector.riotx.core.platform
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.text.Layout
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextUtils.*
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import java.util.ArrayList
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* A [android.widget.TextView] that ellipsizes more intelligently.
|
||||
* This class supports ellipsizing multiline text through setting `android:ellipsize`
|
||||
* and `android:maxLines`.
|
||||
*
|
||||
*
|
||||
* Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
|
||||
* This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
|
||||
*/
|
||||
class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.textViewStyle)
|
||||
: AppCompatTextView(context, attrs, defStyle) {
|
||||
|
||||
private val ELLIPSIS = SpannableString("\u2026")
|
||||
private val ellipsizeListeners: MutableList<EllipsizeListener> = ArrayList()
|
||||
private var ellipsizeStrategy: EllipsizeStrategy? = null
|
||||
var isEllipsized = false
|
||||
private set
|
||||
private var isStale = false
|
||||
private var programmaticChange = false
|
||||
private var fullText: CharSequence? = null
|
||||
private var maxLines = 0
|
||||
private var lineSpacingMult = 1.0f
|
||||
private var lineAddVertPad = 0.0f
|
||||
/**
|
||||
* The end punctuation which will be removed when appending [.ELLIPSIS].
|
||||
*/
|
||||
private var mEndPunctPattern: Pattern? = null
|
||||
|
||||
fun setEndPunctuationPattern(pattern: Pattern?) {
|
||||
mEndPunctPattern = pattern
|
||||
}
|
||||
|
||||
fun addEllipsizeListener(listener: EllipsizeListener) {
|
||||
ellipsizeListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeEllipsizeListener(listener: EllipsizeListener) {
|
||||
ellipsizeListeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The maximum number of lines displayed in this [android.widget.TextView].
|
||||
*/
|
||||
override fun getMaxLines(): Int {
|
||||
return maxLines
|
||||
}
|
||||
|
||||
override fun setMaxLines(maxLines: Int) {
|
||||
super.setMaxLines(maxLines)
|
||||
this.maxLines = maxLines
|
||||
isStale = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the last fully visible line is being ellipsized.
|
||||
*
|
||||
* @return `true` if the last fully visible line is being ellipsized;
|
||||
* otherwise, returns `false`.
|
||||
*/
|
||||
fun ellipsizingLastFullyVisibleLine(): Boolean {
|
||||
return maxLines == Int.MAX_VALUE
|
||||
}
|
||||
|
||||
override fun setLineSpacing(add: Float, mult: Float) {
|
||||
lineAddVertPad = add
|
||||
lineSpacingMult = mult
|
||||
super.setLineSpacing(add, mult)
|
||||
}
|
||||
|
||||
override fun setText(text: CharSequence, type: BufferType) {
|
||||
if (!programmaticChange) {
|
||||
fullText = if (text is Spanned) text else text
|
||||
isStale = true
|
||||
}
|
||||
super.setText(text, type)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (ellipsizingLastFullyVisibleLine()) {
|
||||
isStale = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setPadding(left, top, right, bottom)
|
||||
if (ellipsizingLastFullyVisibleLine()) {
|
||||
isStale = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (isStale) {
|
||||
resetText()
|
||||
}
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ellipsized text if appropriate.
|
||||
*/
|
||||
private fun resetText() {
|
||||
val maxLines = maxLines
|
||||
var workingText = fullText
|
||||
var ellipsized = false
|
||||
if (maxLines != -1) {
|
||||
if (ellipsizeStrategy == null) setEllipsize(null)
|
||||
workingText = ellipsizeStrategy!!.processText(fullText)
|
||||
ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
|
||||
}
|
||||
if (workingText != text) {
|
||||
programmaticChange = true
|
||||
text = try {
|
||||
workingText
|
||||
} finally {
|
||||
programmaticChange = false
|
||||
}
|
||||
}
|
||||
isStale = false
|
||||
if (ellipsized != isEllipsized) {
|
||||
isEllipsized = ellipsized
|
||||
for (listener in ellipsizeListeners) {
|
||||
listener.ellipsizeStateChanged(ellipsized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes words in the text that are longer than the view is wide to be ellipsized
|
||||
* instead of broken in the middle. Use `null` to turn off ellipsizing.
|
||||
*
|
||||
*
|
||||
* Note: Method does nothing for [TruncateAt.MARQUEE]
|
||||
* ellipsizing type.
|
||||
*
|
||||
* @param where part of text to ellipsize
|
||||
*/
|
||||
override fun setEllipsize(where: TruncateAt?) {
|
||||
if (where == null) {
|
||||
ellipsizeStrategy = EllipsizeNoneStrategy()
|
||||
return
|
||||
}
|
||||
ellipsizeStrategy = when (where) {
|
||||
TruncateAt.END -> EllipsizeEndStrategy()
|
||||
TruncateAt.START -> EllipsizeStartStrategy()
|
||||
TruncateAt.MIDDLE -> EllipsizeMiddleStrategy()
|
||||
TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
|
||||
else -> EllipsizeNoneStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener that notifies when the ellipsize state has changed.
|
||||
*/
|
||||
interface EllipsizeListener {
|
||||
fun ellipsizeStateChanged(ellipsized: Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for an ellipsize strategy.
|
||||
*/
|
||||
private abstract inner class EllipsizeStrategy {
|
||||
/**
|
||||
* Returns ellipsized text if the text does not fit inside of the layout;
|
||||
* otherwise, returns the full text.
|
||||
*
|
||||
* @param text text to process
|
||||
* @return Ellipsized text if the text does not fit inside of the layout;
|
||||
* otherwise, returns the full text.
|
||||
*/
|
||||
fun processText(text: CharSequence?): CharSequence? {
|
||||
return if (!isInLayout(text)) createEllipsizedText(text) else text
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the text fits inside of the layout.
|
||||
*
|
||||
* @param text text to fit
|
||||
* @return `true` if the text fits inside of the layout;
|
||||
* otherwise, returns `false`.
|
||||
*/
|
||||
fun isInLayout(text: CharSequence?): Boolean {
|
||||
val layout = createWorkingLayout(text)
|
||||
return layout.lineCount <= linesCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a working layout with the given text.
|
||||
*
|
||||
* @param workingText text to create layout with
|
||||
* @return [android.text.Layout] with the given text.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
protected fun createWorkingLayout(workingText: CharSequence?): Layout {
|
||||
return StaticLayout(
|
||||
workingText,
|
||||
paint,
|
||||
width - compoundPaddingLeft - compoundPaddingRight,
|
||||
Layout.Alignment.ALIGN_NORMAL,
|
||||
lineSpacingMult,
|
||||
lineAddVertPad,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many lines of text we are allowed to display.
|
||||
*/
|
||||
protected val linesCount: Int
|
||||
get() = if (ellipsizingLastFullyVisibleLine()) {
|
||||
val fullyVisibleLinesCount = fullyVisibleLinesCount
|
||||
if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
|
||||
} else {
|
||||
maxLines
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many lines of text we can display so their full height is visible.
|
||||
*/
|
||||
protected val fullyVisibleLinesCount: Int
|
||||
get() {
|
||||
val layout = createWorkingLayout("")
|
||||
val height = height - compoundPaddingTop - compoundPaddingBottom
|
||||
val lineHeight = layout.getLineBottom(0)
|
||||
return height / lineHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates ellipsized text from the given text.
|
||||
*
|
||||
* @param fullText text to ellipsize
|
||||
* @return Ellipsized text
|
||||
*/
|
||||
protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
|
||||
}
|
||||
|
||||
/**
|
||||
* An [EllipsizingTextView.EllipsizeStrategy] that
|
||||
* does not ellipsize text.
|
||||
*/
|
||||
private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
|
||||
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
|
||||
return fullText
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [EllipsizingTextView.EllipsizeStrategy] that
|
||||
* ellipsizes text at the end.
|
||||
*/
|
||||
private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
|
||||
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
|
||||
val layout = createWorkingLayout(fullText)
|
||||
val cutOffIndex = layout.getLineEnd(maxLines - 1)
|
||||
val textLength = fullText!!.length
|
||||
var cutOffLength = textLength - cutOffIndex
|
||||
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
|
||||
var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
|
||||
while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
|
||||
val lastSpace = lastIndexOf(workingText, ' ')
|
||||
if (lastSpace == -1) {
|
||||
break
|
||||
}
|
||||
workingText = substring(workingText, 0, lastSpace).trim()
|
||||
}
|
||||
workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
|
||||
val dest = SpannableStringBuilder(workingText)
|
||||
if (fullText is Spanned) {
|
||||
copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the end punctuation from a given text according to [.mEndPunctPattern].
|
||||
*
|
||||
* @param workingText text to strip end punctuation from
|
||||
* @return Text without end punctuation.
|
||||
*/
|
||||
fun stripEndPunctuation(workingText: CharSequence?): String {
|
||||
return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [EllipsizingTextView.EllipsizeStrategy] that
|
||||
* ellipsizes text at the start.
|
||||
*/
|
||||
private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
|
||||
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
|
||||
val layout = createWorkingLayout(fullText)
|
||||
val cutOffIndex = layout.getLineEnd(maxLines - 1)
|
||||
val textLength = fullText!!.length
|
||||
var cutOffLength = textLength - cutOffIndex
|
||||
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
|
||||
var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
|
||||
while (!isInLayout(concat(ELLIPSIS, workingText))) {
|
||||
val firstSpace = indexOf(workingText, ' ')
|
||||
if (firstSpace == -1) {
|
||||
break
|
||||
}
|
||||
workingText = substring(workingText, firstSpace, workingText.length).trim()
|
||||
}
|
||||
workingText = concat(ELLIPSIS, workingText)
|
||||
val dest = SpannableStringBuilder(workingText)
|
||||
if (fullText is Spanned) {
|
||||
copySpansFrom(fullText as Spanned?, textLength - workingText.length,
|
||||
textLength, null, dest, 0)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [EllipsizingTextView.EllipsizeStrategy] that
|
||||
* ellipsizes text in the middle.
|
||||
*/
|
||||
private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
|
||||
override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
|
||||
val layout = createWorkingLayout(fullText)
|
||||
val cutOffIndex = layout.getLineEnd(maxLines - 1)
|
||||
val textLength = fullText!!.length
|
||||
var cutOffLength = textLength - cutOffIndex
|
||||
if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
|
||||
cutOffLength += cutOffIndex % 2 // Make it even.
|
||||
var firstPart = substring(
|
||||
fullText, 0, textLength / 2 - cutOffLength / 2).trim()
|
||||
var secondPart = substring(
|
||||
fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
|
||||
while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
|
||||
val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
|
||||
val firstSpaceSecondPart = secondPart.indexOf(' ')
|
||||
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
|
||||
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
|
||||
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
|
||||
}
|
||||
val firstDest = SpannableStringBuilder(firstPart)
|
||||
val secondDest = SpannableStringBuilder(secondPart)
|
||||
if (fullText is Spanned) {
|
||||
copySpansFrom(fullText as Spanned?, 0, firstPart.length,
|
||||
null, firstDest, 0)
|
||||
copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
|
||||
textLength, null, secondDest, 0)
|
||||
}
|
||||
return concat(firstDest, ELLIPSIS, secondDest)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ELLIPSIZE_ALPHA = 0x88
|
||||
private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
|
||||
}
|
||||
|
||||
init {
|
||||
val a = context.obtainStyledAttributes(attrs, intArrayOf(R.attr.maxLines, R.attr.ellipsize), defStyle, 0)
|
||||
maxLines = a.getInt(0, Int.MAX_VALUE)
|
||||
a.recycle()
|
||||
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
|
||||
val currentTextColor = currentTextColor
|
||||
val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
|
||||
ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
|
@ -33,7 +33,6 @@ import androidx.annotation.MainThread
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.Unbinder
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
|
|
|
@ -74,7 +74,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
|||
.combineLatest<List<RoomMemberSummary>, PowerLevelsContent, RoomMemberSummaries>(
|
||||
room.rx(session).liveRoomMembers(roomMemberQueryParams),
|
||||
room.rx(session)
|
||||
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS,"")
|
||||
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
|
||||
.mapOptional { it.content.toModel<PowerLevelsContent>() }
|
||||
.unwrap(),
|
||||
BiFunction { roomMembers, powerLevelsContent ->
|
||||
|
|
Loading…
Reference in a new issue