Breadcrumbs simple UI

This commit is contained in:
Benoit Marty 2019-12-05 14:51:12 +01:00
parent cec08a20e5
commit 7c561ae622
15 changed files with 491 additions and 11 deletions

View file

@ -33,6 +33,7 @@ import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.*
@ -249,4 +250,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(PublicRoomsFragment::class)
fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
}

View file

@ -29,6 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedVie
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.HomeSharedActionViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.login.LoginSharedActionViewModel
@ -118,4 +119,9 @@ interface ViewModelModule {
@IntoMap
@ViewModelKey(LoginSharedActionViewModel::class)
fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(RoomDetailSharedActionViewModel::class)
fun bindRoomDetailSharedActionViewModel(viewModel: RoomDetailSharedActionViewModel): ViewModel
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import androidx.recyclerview.widget.DefaultItemAnimator
private const val ANIM_DURATION_IN_MILLIS = 200L
class BreadcrumbsAnimator : DefaultItemAnimator() {
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = ANIM_DURATION_IN_MILLIS
changeDuration = ANIM_DURATION_IN_MILLIS
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import android.view.View
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class BreadcrumbsController @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : EpoxyController() {
var listener: Listener? = null
private var viewState: BreadcrumbsViewState? = null
init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the the whole list of rooms on the main thread.
requestModelBuild()
}
fun update(viewState: BreadcrumbsViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
// TODO Display a loading, or an empty state
nonNullViewState.asyncRooms.invoke()
?.forEach {
breadcrumbsItem {
id(it.roomId)
avatarRenderer(avatarRenderer)
roomId(it.roomId)
roomName(it.displayName)
avatarUrl(it.avatarUrl)
itemClickListener(
DebouncedClickListener(View.OnClickListener { _ ->
listener?.onRoomClicked(it)
})
)
}
}
}
interface Listener {
fun onRoomClicked(room: RoomSummary)
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.detail.RoomDetailSharedAction
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import kotlinx.android.synthetic.main.fragment_breadcrumbs.*
import javax.inject.Inject
class BreadcrumbsFragment @Inject constructor(
private val breadcrumbsController: BreadcrumbsController,
val breadcrumbsViewModelFactory: BreadcrumbsViewModel.Factory
) : VectorBaseFragment(), BreadcrumbsController.Listener {
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
private val breadcrumbsViewModel: BreadcrumbsViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_breadcrumbs
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
breadcrumbsViewModel.subscribe { renderState(it) }
}
override fun onDestroyView() {
super.onDestroyView()
breadcrumbsRecyclerView.adapter = null
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
breadcrumbsRecyclerView.layoutManager = layoutManager
breadcrumbsRecyclerView.itemAnimator = BreadcrumbsAnimator()
breadcrumbsController.listener = this
breadcrumbsRecyclerView.setController(breadcrumbsController)
}
private fun renderState(state: BreadcrumbsViewState) {
breadcrumbsController.update(state)
}
// BreadcrumbsController.Listener **************************************************************
override fun onRoomClicked(room: RoomSummary) {
sharedActionViewModel.post(RoomDetailSharedAction.OpenRoom(room.roomId))
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_breadcrumbs)
abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute var avatarUrl: String? = null
// TODO @EpoxyAttribute var unreadNotificationCount: Int = 0
// TODO @EpoxyAttribute var hasUnreadMessage: Boolean = false
// TODO @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener(itemClickListener)
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
// TODO val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
// TODO val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.schedulers.Schedulers
class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState,
private val session: Session)
: VectorViewModel<BreadcrumbsViewState, EmptyAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel
}
companion object : MvRxViewModelFactory<BreadcrumbsViewModel, BreadcrumbsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: BreadcrumbsViewState): BreadcrumbsViewModel? {
val fragment: BreadcrumbsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.breadcrumbsViewModelFactory.create(state)
}
}
init {
observeBreadcrumbs()
}
override fun handle(action: EmptyAction) {
// No op
}
// PRIVATE METHODS *****************************************************************************
private fun observeBreadcrumbs() {
session.rx()
.liveBreadcrumbs()
.observeOn(Schedulers.computation())
.execute { asyncRooms ->
copy(asyncRooms = asyncRooms)
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 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.features.home.room.breadcrumbs
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class BreadcrumbsViewState(
val asyncRooms: Async<List<RoomSummary>> = Uninitialized
) : MvRxState

View file

@ -20,17 +20,25 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
import kotlinx.android.synthetic.main.activity_room_detail.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun getLayoutRes(): Int {
return R.layout.activity_room_detail
}
override fun getLayoutRes() = R.layout.activity_room_detail
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
// Simple filter
private var currentRoomId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -38,14 +46,55 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return
currentRoomId = roomDetailArgs.roomId
replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs)
replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java)
}
sharedActionViewModel = viewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
is RoomDetailSharedAction.OpenRoom -> {
drawerLayout.closeDrawer(GravityCompat.START)
// Do not replace the Fragment if it's the same roomId
if (currentRoomId != sharedAction.roomId) {
currentRoomId = sharedAction.roomId
replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(sharedAction.roomId))
}
}
}
}
.disposeOnDestroy()
drawerLayout.addDrawerListener(drawerListener)
}
override fun onDestroy() {
drawerLayout.removeDrawerListener(drawerListener)
super.onDestroy()
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
}
}
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
companion object {
private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 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.features.home.room.detail
import im.vector.riotx.core.platform.VectorSharedAction
/**
* Supported navigation actions for [RoomDetailActivity]
*/
sealed class RoomDetailSharedAction : VectorSharedAction {
data class OpenRoom(val roomId: String) : RoomDetailSharedAction()
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2019 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.features.home.room.detail
import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
/**
* Activity shared view model
*/
class RoomDetailSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<RoomDetailSharedAction>()

View file

@ -17,6 +17,7 @@
android:layout_height="match_parent" />
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,14 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:openDrawer="start">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/vector_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/roomDetailContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/merge_overlay_waiting_view" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/roomDetailContainer"
android:id="@+id/roomDetailDrawerContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:layout_gravity="start" />
<include layout="@layout/merge_overlay_waiting_view" />
</FrameLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/breadcrumbsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_breadcrumbs" />

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/breadcrumbsRoot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/breadcrumbsImageView"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="4dp"
tools:src="@tools:sample/avatars" />
</FrameLayout>