recents carousel for new home screen layout (#6707)

This commit is contained in:
Nikita Fedrunov 2022-08-09 14:31:26 +02:00 committed by GitHub
parent 6e1e31bac1
commit 6045eac87a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 3 deletions

View file

@ -74,6 +74,7 @@ ext.groups = [
'com.github.javaparser', 'com.github.javaparser',
'com.github.piasy', 'com.github.piasy',
'com.github.shyiko.klob', 'com.github.shyiko.klob',
'com.github.rubensousa',
'com.google', 'com.google',
'com.google.android', 'com.google.android',
'com.google.api.grpc', 'com.google.api.grpc',

View file

@ -427,6 +427,9 @@ dependencies {
implementation libs.airbnb.epoxyPaging implementation libs.airbnb.epoxyPaging
implementation libs.airbnb.mavericks implementation libs.airbnb.mavericks
// Snap Helper https://github.com/rubensousa/GravitySnapHelper
implementation 'com.github.rubensousa:gravitysnaphelper:2.2.2'
// Nightly // Nightly
// API-only library // API-only library
gplayImplementation libs.google.appdistributionApi gplayImplementation libs.google.appdistributionApi

View file

@ -25,17 +25,21 @@ import android.content.res.Configuration
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.StrictMode import android.os.StrictMode
import android.view.Gravity
import androidx.core.provider.FontRequest import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.recyclerview.widget.SnapHelper
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen import com.gabrielittner.threetenbp.LazyThreeTen
import com.github.rubensousa.gravitysnaphelper.GravitySnapHelper
import com.mapbox.mapboxsdk.Mapbox import com.mapbox.mapboxsdk.Mapbox
import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider import com.vanniktech.emoji.google.GoogleEmojiProvider
@ -141,8 +145,9 @@ class VectorApplication :
logInfo() logInfo()
LazyThreeTen.init(this) LazyThreeTen.init(this)
Mavericks.initialize(debugMode = false) Mavericks.initialize(debugMode = false)
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() configureEpoxy()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
val fontRequest = FontRequest( val fontRequest = FontRequest(
"com.google.android.gms.fonts", "com.google.android.gms.fonts",
@ -198,6 +203,16 @@ class VectorApplication :
Mapbox.getInstance(this) Mapbox.getInstance(this)
} }
private fun configureEpoxy() {
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
Carousel.setDefaultGlobalSnapHelperFactory(object : Carousel.SnapHelperFactory() {
override fun buildSnapHelper(context: Context?): SnapHelper {
return GravitySnapHelper(Gravity.START)
}
})
}
private fun enableStrictModeIfNeeded() { private fun enableStrictModeIfNeeded() {
if (Config.ENABLE_STRICT_MODE_LOGS) { if (Config.ENABLE_STRICT_MODE_LOGS) {
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(

View file

@ -30,6 +30,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.StateView import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
@ -43,6 +44,7 @@ import im.vector.app.features.home.room.list.RoomSummaryPagedController
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -53,7 +55,8 @@ import javax.inject.Inject
class HomeRoomListFragment @Inject constructor( class HomeRoomListFragment @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory, private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val userPreferencesProvider: UserPreferencesProvider private val userPreferencesProvider: UserPreferencesProvider,
private val recentRoomCarouselController: RecentRoomCarouselController
) : VectorBaseFragment<FragmentRoomListBinding>(), ) : VectorBaseFragment<FragmentRoomListBinding>(),
RoomListListener { RoomListListener {
@ -180,6 +183,12 @@ class HomeRoomListFragment @Inject constructor(
} }
}.adapter }.adapter
} }
is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller ->
controller.listener = this
data.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}.adapter
} }
} }
@ -192,6 +201,12 @@ class HomeRoomListFragment @Inject constructor(
) )
} }
override fun onDestroyView() {
views.roomListView.cleanup()
recentRoomCarouselController.listener = null
super.onDestroyView()
}
// region RoomListListener // region RoomListListener
override fun onRoomClicked(room: RoomSummary) { override fun onRoomClicked(room: RoomSummary) {

View file

@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.session.room.state.isPublic
class HomeRoomListViewModel @AssistedInject constructor( class HomeRoomListViewModel @AssistedInject constructor(
@ -78,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
private fun configureSections() { private fun configureSections() {
val newSections = mutableSetOf<HomeRoomSection>() val newSections = mutableSetOf<HomeRoomSection>()
newSections.add(getRecentRoomsSection())
newSections.add(getAllRoomsSection()) newSections.add(getAllRoomsSection())
viewModelScope.launch { viewModelScope.launch {
@ -89,6 +92,18 @@ class HomeRoomListViewModel @AssistedInject constructor(
} }
} }
private fun getRecentRoomsSection(): HomeRoomSection {
val liveList = session.roomService()
.getBreadcrumbsLive(roomSummaryQueryParams {
displayName = QueryStringValue.NoCondition
memberships = listOf(Membership.JOIN)
})
return HomeRoomSection.RecentRoomsData(
list = liveList
)
}
private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData { private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData {
val builder = RoomSummaryQueryParams.Builder().also { val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN) it.memberships = listOf(Membership.JOIN)

View file

@ -24,4 +24,8 @@ sealed class HomeRoomSection {
data class RoomSummaryData( data class RoomSummaryData(
val list: LiveData<PagedList<RoomSummary>> val list: LiveData<PagedList<RoomSummary>>
) : HomeRoomSection() ) : HomeRoomSection()
data class RecentRoomsData(
val list: LiveData<List<RoomSummary>>
) : HomeRoomSection()
} }

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2022 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.app.features.home.room.list.home.recent
import android.content.res.Resources
import android.util.TypedValue
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModelBuilder
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.carousel
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RecentRoomCarouselController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val resources: Resources,
) : EpoxyController() {
private var data: List<RoomSummary>? = null
var listener: RoomListListener? = null
private val hPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
16f,
resources.displayMetrics
).toInt()
private val itemSpacing = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
24f,
resources.displayMetrics
).toInt()
fun submitList(recentList: List<RoomSummary>) {
this.data = recentList
requestModelBuild()
}
override fun buildModels() {
val host = this
data?.let { data ->
carousel {
id("recents_carousel")
padding(Carousel.Padding(host.hPadding, host.itemSpacing))
withModelsFrom(data) { roomSummary ->
val onClick = host.listener?.let { it::onRoomClicked }
val onLongClick = host.listener?.let { it::onRoomLongClicked }
RecentRoomItem_()
.id(roomSummary.roomId)
.avatarRenderer(host.avatarRenderer)
.matrixItem(roomSummary.toMatrixItem())
.unreadNotificationCount(roomSummary.notificationCount)
.showHighlighted(roomSummary.highlightCount > 0)
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
.itemClickListener { onClick?.invoke(roomSummary) }
}
}
}
}
}
private inline fun <T> CarouselModelBuilder.withModelsFrom(
items: List<T>,
modelBuilder: (T) -> EpoxyModel<*>
) {
models(items.map { modelBuilder(it) })
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 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.app.features.home.room.list.home.recent
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass
abstract class RecentRoomItem : VectorEpoxyModel<RecentRoomItem.Holder>(R.layout.item_recent_room) {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var itemLongClickListener: View.OnLongClickListener? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var itemClickListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.onClick(itemClickListener)
holder.rootView.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
itemLongClickListener?.onLongClick(it) ?: false
}
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarImageView.contentDescription = matrixItem.getBestName()
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.title.text = matrixItem.getBestName()
}
override fun unbind(holder: Holder) {
holder.rootView.setOnClickListener(null)
holder.rootView.setOnLongClickListener(null)
avatarRenderer.clear(holder.avatarImageView)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.recentUnreadCounterBadgeView)
val avatarImageView by bind<ImageView>(R.id.recentImageView)
val title by bind<TextView>(R.id.recentTitle)
val rootView by bind<ViewGroup>(R.id.recentRoot)
}
}

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recentRoot"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
tools:viewBindingIgnore="true">
<ImageView
android:id="@+id/recentImageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@sample/room_round_avatars" />
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/recentUnreadCounterBadgeView"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="18dp"
android:minHeight="18dp"
android:textColor="?colorOnError"
android:visibility="gone"
app:layout_constraintCircle="@id/recentImageView"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="28dp"
tools:background="@drawable/bg_unread_highlight"
tools:ignore="MissingConstraints"
tools:text="24"
tools:visibility="visible" />
<TextView
android:id="@+id/recentTitle"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:lines="1"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recentImageView"
tools:text="Coffee" />
</androidx.constraintlayout.widget.ConstraintLayout>