From 9739c4bdef9975a1ef187fba274d2da547fca54c Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Mon, 2 Apr 2018 13:17:59 +0200 Subject: [PATCH] Implement significantly faster DayChange headers --- .../ui/chat/input/AutoCompleteAdapter.kt | 14 ++- .../chat/messages/DayChangeItemDecoration.kt | 102 ++++++++++++++++++ .../ui/chat/messages/DisplayMessage.kt | 7 +- .../ui/chat/messages/MessageAdapter.kt | 16 +-- .../ui/chat/messages/MessageListFragment.kt | 15 ++- .../ui/chat/nicks/NickListAdapter.kt | 17 +-- .../layout/widget_chatmessage_daychange.xml | 6 +- app/src/main/res/values/ids.xml | 6 ++ .../persistence/QuasselBacklogStorage.kt | 5 +- .../persistence/QuasselDatabase.kt | 102 ++---------------- 10 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt index 0be424b74..7f295e55e 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt @@ -112,11 +112,15 @@ class AutoCompleteAdapter( nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick) realname.text = data.realname - GlideApp.with(itemView) - .load(data.avatarUrl) - .apply(RequestOptions.circleCropTransform()) - .placeholder(data.fallbackDrawable) - .into(avatar) + if (data.avatarUrl != null) { + GlideApp.with(itemView) + .load(data.avatarUrl) + .apply(RequestOptions.circleCropTransform()) + .placeholder(data.fallbackDrawable) + .into(avatar) + } else { + avatar.setImageDrawable(data.fallbackDrawable) + } } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt new file mode 100644 index 000000000..660579438 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt @@ -0,0 +1,102 @@ +package de.kuschku.quasseldroid.ui.chat.messages + +import android.graphics.Canvas +import android.graphics.Rect +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import de.kuschku.quasseldroid.R +import org.threeten.bp.ZoneId +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.FormatStyle +import org.threeten.bp.temporal.ChronoUnit + +class DayChangeItemDecoration(private val adapter: MessageAdapter) : + RecyclerView.ItemDecoration() { + private val dayChangeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + private val mBounds = Rect() + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + c.save() + val left: Int + val right: Int + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + c.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom) + } else { + left = 0 + right = parent.width + } + + val childCount = parent.childCount + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + if (child.getTag(R.id.tag_daychange) == true) { + parent.getDecoratedBoundsWithMargins(child, mBounds) + val bottom = mBounds.bottom + Math.round(child.translationY) + val top = mBounds.top + Math.round(child.translationY) + val layout = child.getTag(R.id.tag_daychange_layout) as View + c.save() + c.clipRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) + c.translate(left.toFloat(), top.toFloat()) + layout.draw(c) + c.restore() + } + } + c.restore() + } + + private fun fixLayoutSize(view: View, parent: ViewGroup) { + val widthSpec = View.MeasureSpec.makeMeasureSpec( + parent.width, + View.MeasureSpec.EXACTLY + ) + val heightSpec = View.MeasureSpec.makeMeasureSpec( + parent.height, + View.MeasureSpec.UNSPECIFIED + ) + + val childWidthSpec = ViewGroup.getChildMeasureSpec( + widthSpec, + parent.paddingLeft + parent.paddingRight, + view.layoutParams.width + ) + val childHeightSpec = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop + parent.paddingBottom, + view.layoutParams.height + ) + + view.measure(childWidthSpec, childHeightSpec) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + } + + override fun getItemOffsets(outRect: Rect, v: View, parent: RecyclerView, + state: RecyclerView.State) { + adapter[parent.getChildAdapterPosition(v)]?.let { + if (it.hasDayChange) { + if (v.getTag(R.id.tag_daychange_layout) == null) { + val layout = LayoutInflater.from(parent.context).inflate( + R.layout.widget_chatmessage_daychange, parent, false + ) + val content = layout.findViewById<TextView>(R.id.combined) + content?.text = dayChangeFormatter.format( + it.content.time.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS) + ) + fixLayoutSize(layout, parent) + + v.setTag(R.id.tag_daychange_layout, layout) + v.setTag(R.id.tag_daychange_content, content) + } + v.setTag(R.id.tag_daychange, true) + val layout = v.getTag(R.id.tag_daychange_layout) as View + outRect.set(0, layout.measuredHeight, 0, 10) + } else { + v.setTag(R.id.tag_daychange, false) + } + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt index 8a3f6fb43..ab5633dfa 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt @@ -6,22 +6,25 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase data class DisplayMessage( val content: QuasselDatabase.DatabaseMessage, + val hasDayChange: Boolean, + val isFollowUp: Boolean, val isSelected: Boolean, val isExpanded: Boolean, val isMarkerLine: Boolean ) { data class Tag( val id: MsgId, + val hasDayChange: Boolean, val isFollowUp: Boolean, val isSelected: Boolean, val isExpanded: Boolean, val isMarkerLine: Boolean ) - val tag = Tag(content.messageId, content.followUp, isSelected, isExpanded, isMarkerLine) + val tag = Tag(content.messageId, hasDayChange, isFollowUp, isSelected, isExpanded, isMarkerLine) val avatarUrl = content.sender.let { Regex("[us]id(\\d+)").matchEntire(HostmaskHelper.user(it))?.groupValues?.lastOrNull()?.let { "https://www.irccloud.com/avatar-redirect/$it" } } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt index 191abda65..0cbe4e1db 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt @@ -61,7 +61,7 @@ class MessageAdapter( override fun getItemViewType(position: Int) = getItem(position)?.let { viewType(Message_Flags.of(it.content.type), Message_Flags.of(it.content.flag), - it.content.followUp) + it.isFollowUp) } ?: 0 private fun viewType(type: Message_Types, flags: Message_Flags, followUp: Boolean) = @@ -200,11 +200,15 @@ class MessageAdapter( this.itemView.isSelected = message.isSelected avatar?.let { avatarView -> - GlideApp.with(itemView) - .load(message.avatarUrl) - .apply(RequestOptions.circleCropTransform()) - .placeholder(message.fallbackDrawable) - .into(avatarView) + if (message.avatarUrl != null) { + GlideApp.with(itemView) + .load(message.avatarUrl) + .apply(RequestOptions.circleCropTransform()) + .placeholder(message.fallbackDrawable) + .into(avatarView) + } else { + avatarView.setImageDrawable(message.fallbackDrawable) + } } } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt index 8d7fa43c8..22d11cf69 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt @@ -26,7 +26,6 @@ import de.kuschku.libquassel.util.helpers.value import de.kuschku.quasseldroid.GlideApp import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.persistence.QuasselDatabase -import de.kuschku.quasseldroid.persistence.findByBufferIdPagedWithDayChange import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.settings.BacklogSettings import de.kuschku.quasseldroid.settings.MessageSettings @@ -34,6 +33,9 @@ import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.util.service.ServiceBoundFragment import de.kuschku.quasseldroid.util.ui.SpanFormatter import io.reactivex.BackpressureStrategy +import org.threeten.bp.ZoneId +import org.threeten.bp.ZonedDateTime +import org.threeten.bp.temporal.ChronoUnit import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -234,11 +236,17 @@ class MessageListFragment : ServiceBoundFragment() { fun processMessages(list: List<QuasselDatabase.DatabaseMessage>, selected: Set<MsgId>, expanded: Set<MsgId>, markerLine: MsgId?): List<DisplayMessage> { var previous: QuasselDatabase.DatabaseMessage? = null + var previousDate: ZonedDateTime? = null return list.asReversed().map { - it.followUp = previous?.sender == it.sender + val date = it.time.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS) + val isSameDay = previousDate?.isEqual(date) ?: false + val isFollowUp = previous?.sender == it.sender && isSameDay previous = it + previousDate = date DisplayMessage( content = it, + hasDayChange = !isSameDay, + isFollowUp = isFollowUp, isSelected = selected.contains(it.messageId), isExpanded = expanded.contains(it.messageId), isMarkerLine = markerLine == it.messageId @@ -253,7 +261,7 @@ class MessageListFragment : ServiceBoundFragment() { .toLiveData().switchMapNotNull { (buffer, selected, expanded, markerLine) -> database.filtered().listen(accountId, buffer).switchMapNotNull { filtered -> LivePagedListBuilder( - database.message().findByBufferIdPagedWithDayChange(buffer, filtered).mapByPage { + database.message().findByBufferIdPaged(buffer, filtered).mapByPage { processMessages(it, selected.keys, expanded, markerLine.orNull()) }, PagedList.Config.Builder() @@ -368,6 +376,7 @@ class MessageListFragment : ServiceBoundFragment() { val preloader = RecyclerViewPreloader(Glide.with(this), preloadModelProvider, sizeProvider, 10) messageList.addOnScrollListener(preloader) + messageList.addItemDecoration(DayChangeItemDecoration(adapter)) return view } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt index 4f07b965b..92d862977 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt @@ -86,12 +86,15 @@ class NickListAdapter( nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick) realname.text = data.realname - - GlideApp.with(itemView) - .load(data.avatarUrl) - .apply(RequestOptions.circleCropTransform()) - .placeholder(data.fallbackDrawable) - .into(avatar) + if (data.avatarUrl != null) { + GlideApp.with(itemView) + .load(data.avatarUrl) + .apply(RequestOptions.circleCropTransform()) + .placeholder(data.fallbackDrawable) + .into(avatar) + } else { + avatar.setImageDrawable(data.fallbackDrawable) + } } } @@ -99,4 +102,4 @@ class NickListAdapter( const val VIEWTYPE_ACTIVE = 0 const val VIEWTYPE_AWAY = 1 } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/widget_chatmessage_daychange.xml b/app/src/main/res/layout/widget_chatmessage_daychange.xml index fedd0454c..eda4795f9 100644 --- a/app/src/main/res/layout/widget_chatmessage_daychange.xml +++ b/app/src/main/res/layout/widget_chatmessage_daychange.xml @@ -7,6 +7,10 @@ android:orientation="vertical" android:textAppearance="?android:attr/textAppearanceListItemSmall"> + <Space + android:layout_width="match_parent" + android:layout_height="@dimen/message_vertical" /> + <View android:layout_width="match_parent" android:layout_height="1dp" @@ -33,4 +37,4 @@ android:textStyle="bold" tools:text="27.03.2018" /> </LinearLayout> -</LinearLayout> \ No newline at end of file +</LinearLayout> diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 000000000..07001df1e --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="tag_daychange" type="id" /> + <item name="tag_daychange_layout" type="id" /> + <item name="tag_daychange_content" type="id" /> +</resources> diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt index 7ee165112..2c79f758b 100644 --- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt +++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt @@ -29,8 +29,7 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage { bufferId = message.bufferInfo.bufferId, sender = message.sender, senderPrefixes = message.senderPrefixes, - content = message.content, - followUp = false + content = message.content ) ) } @@ -48,4 +47,4 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage { db.message().clearMessages() } -} \ No newline at end of file +} diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt index a72a63436..857f984fa 100644 --- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt +++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt @@ -2,7 +2,6 @@ package de.kuschku.quasseldroid.persistence import android.arch.lifecycle.LiveData import android.arch.paging.DataSource -import android.arch.persistence.db.SimpleSQLiteQuery import android.arch.persistence.db.SupportSQLiteDatabase import android.arch.persistence.db.SupportSQLiteQuery import android.arch.persistence.room.* @@ -17,7 +16,7 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase.Filtered import io.reactivex.Flowable import org.threeten.bp.Instant -@Database(entities = [DatabaseMessage::class, Filtered::class], version = 4) +@Database(entities = [DatabaseMessage::class, Filtered::class], version = 5) @TypeConverters(DatabaseMessage.MessageTypeConverters::class) abstract class QuasselDatabase : RoomDatabase() { abstract fun message(): MessageDao @@ -32,8 +31,7 @@ abstract class QuasselDatabase : RoomDatabase() { var bufferId: Int, var sender: String, var senderPrefixes: String, - var content: String, - var followUp: Boolean + var content: String ) { class MessageTypeConverters { @TypeConverter @@ -177,6 +175,12 @@ abstract class QuasselDatabase : RoomDatabase() { "ALTER TABLE message ADD followUp INT DEFAULT 0 NOT NULL;" ) } + }, + object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("drop table message;") + database.execSQL("create table message (messageId INTEGER not null primary key, time INTEGER not null, type INTEGER not null, flag INTEGER not null, bufferId INTEGER not null, sender TEXT not null, senderPrefixes TEXT not null, content TEXT not null);") + } } ).build() } @@ -197,93 +201,3 @@ fun QuasselDatabase.MessageDao.clearMessages( ) { this.clearMessages(bufferId, idRange.first, idRange.last) } - -fun QuasselDatabase.MessageDao.findByBufferIdPagedWithDayChange(bufferId: Int, type: Int) = - this.findMessagesRawPaged(SimpleSQLiteQuery(""" -SELECT t.* -FROM - ( - SELECT - messageId, - time, - type, - flag, - bufferId, - sender, - senderPrefixes, - content, - followUp - FROM message - WHERE bufferId = ? - AND type & ~? > 0 - UNION ALL - SELECT DISTINCT - strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * -1000 AS messageId, - strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000 AS time, - 8192 AS type, - 0 AS flag, - ? AS bufferId, - '' AS sender, - '' AS senderPrefixes, - '' AS content, - 0 AS followUp - FROM message - WHERE bufferId = ? - AND type & ~? > 0 - ) t -ORDER BY TIME - DESC, messageId - DESC - """, arrayOf(bufferId, type, bufferId, bufferId, type))) - -fun QuasselDatabase.MessageDao.findByBufferIdPagedWithDayChangeSlow(bufferId: Int, type: Int) = - this.findMessagesRawPaged(SimpleSQLiteQuery(""" -SELECT t.* -FROM - ( - SELECT - messageId, - time, - type, - flag, - bufferId, - sender, - senderPrefixes, - content, - (SELECT 1 - FROM - (SELECT * - FROM message m - WHERE m.messageId < message.messageId - AND bufferId = ? - AND type & ~? > 0 - ORDER BY m.messageId - DESC - LIMIT 1) t - WHERE t.sender = message.sender - AND strftime('%s', date(datetime(t.time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000 = - strftime('%s', date(datetime(message.time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000 - AND t.type = message.type - ) = 1 AS followUp - FROM message - WHERE bufferId = ? - AND type & ~? > 0 - UNION ALL - SELECT DISTINCT - strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * -1000 AS messageId, - strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000 AS time, - 8192 AS type, - 0 AS flag, - ? AS bufferId, - '' AS sender, - '' AS senderPrefixes, - '' AS content, - 0 AS followUp - FROM message - WHERE bufferId = ? - AND type & ~? > 0 - ) t -ORDER BY TIME - DESC, messageId - DESC - """, arrayOf(bufferId, type, bufferId, type, bufferId, bufferId, type))) -- GitLab