mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
recents carousel for new home screen layout (#6707)
This commit is contained in:
parent
6e1e31bac1
commit
6045eac87a
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) })
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
61
vector/src/main/res/layout/item_recent_room.xml
Normal file
61
vector/src/main/res/layout/item_recent_room.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user