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.piasy',
'com.github.shyiko.klob',
'com.github.rubensousa',
'com.google',
'com.google.android',
'com.google.api.grpc',

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
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.VectorBaseFragment
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.RoomListQuickActionsSharedAction
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.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -53,7 +55,8 @@ import javax.inject.Inject
class HomeRoomListFragment @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val userPreferencesProvider: UserPreferencesProvider
private val userPreferencesProvider: UserPreferencesProvider,
private val recentRoomCarouselController: RecentRoomCarouselController
) : VectorBaseFragment<FragmentRoomListBinding>(),
RoomListListener {
@ -180,6 +183,12 @@ class HomeRoomListFragment @Inject constructor(
}
}.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
override fun onRoomClicked(room: RoomSummary) {

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
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.toActiveSpaceOrNoFilter
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.model.Membership
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
class HomeRoomListViewModel @AssistedInject constructor(
@ -78,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
private fun configureSections() {
val newSections = mutableSetOf<HomeRoomSection>()
newSections.add(getRecentRoomsSection())
newSections.add(getAllRoomsSection())
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 {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)

View File

@ -24,4 +24,8 @@ sealed class HomeRoomSection {
data class RoomSummaryData(
val list: LiveData<PagedList<RoomSummary>>
) : 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>