Autocomplete : handle click and better detection for / commands

This commit is contained in:
Benoit Marty 2019-04-08 18:31:24 +02:00
parent 6d3028c2d7
commit c64d6b6b28
11 changed files with 149 additions and 18 deletions

View file

@ -27,7 +27,7 @@ import kotlin.reflect.KProperty
* See [SampleKotlinModelWithHolder] for a usage example.
*/
abstract class VectorEpoxyHolder : EpoxyHolder() {
private lateinit var view: View
lateinit var view: View
override fun bindView(itemView: View) {
view = itemView

View file

@ -0,0 +1,25 @@
/*
* 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.riotredesign.core.listener
/**
* Simple generic listener interface
*/
interface Listener<T> {
fun onEvent(t: T)
}

View file

@ -24,8 +24,9 @@ import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.AutocompletePresenter
import im.vector.riotredesign.core.listener.Listener
abstract class EpoxyViewPresenter<T>(context: Context) : AutocompletePresenter<T>(context) {
abstract class EpoxyAutocompletePresenter<T>(context: Context) : AutocompletePresenter<T>(context), Listener<T> {
private var recyclerView: EpoxyRecyclerView? = null
private var clicks: AutocompletePresenter.ClickProvider<T>? = null
@ -73,6 +74,10 @@ abstract class EpoxyViewPresenter<T>(context: Context) : AutocompletePresenter<T
observer?.onChanged()
}
override fun onEvent(t: T) {
dispatchClick(t)
}
private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {

View file

@ -17,21 +17,27 @@
package im.vector.riotredesign.features.autocomplete.command
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotredesign.core.listener.Listener
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.command.Command
class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() {
var listener: Listener<Command>? = null
override fun buildModels(data: List<Command>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach {
data.forEach { command ->
autocompleteCommandItem {
id(it.command)
name(it.command)
parameters(it.parameters)
description(stringProvider.getString(it.description))
id(command.command)
name(command.command)
parameters(command.parameters)
description(stringProvider.getString(command.description))
clickListener { _ ->
listener?.onEvent(command)
}
}
}
}

View file

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.autocomplete.command
import android.view.View
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -32,8 +33,12 @@ abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandIte
var parameters: CharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
holder.nameView.text = name
holder.parametersView.text = parameters
holder.descriptionView.text = description

View file

@ -18,12 +18,16 @@ package im.vector.riotredesign.features.autocomplete.command
import android.content.Context
import com.airbnb.epoxy.EpoxyController
import im.vector.riotredesign.features.autocomplete.EpoxyViewPresenter
import im.vector.riotredesign.features.autocomplete.EpoxyAutocompletePresenter
import im.vector.riotredesign.features.command.Command
class AutocompleteCommandPresenter(context: Context,
private val controller: AutocompleteCommandController
) : EpoxyViewPresenter<Command>(context) {
private val controller: AutocompleteCommandController) :
EpoxyAutocompletePresenter<Command>(context) {
init {
controller.listener = this
}
override fun providesController(): EpoxyController {
return controller

View file

@ -0,0 +1,44 @@
/*
* 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.riotredesign.features.autocomplete.command
import android.text.Spannable
import com.otaliastudios.autocomplete.AutocompletePolicy
class CommandPolicy : AutocompletePolicy {
override fun getQuery(text: Spannable): CharSequence {
if (text.length > 0) {
return text.substring(1, text.length)
}
// Should not happen
return ""
}
override fun onDismiss(text: Spannable?) {
}
// Only if text which starts with '/' and without space
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
return text?.startsWith("/") == true
&& !text.contains(" ")
}
override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean {
return !shouldShowPopup(text, cursorPos)
}
}

View file

@ -18,18 +18,24 @@ package im.vector.riotredesign.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.core.listener.Listener
class AutocompleteUserController() : TypedEpoxyController<List<User>>() {
class AutocompleteUserController : TypedEpoxyController<List<User>>() {
var listener: Listener<User>? = null
override fun buildModels(data: List<User>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach {
data.forEach { user ->
autocompleteUserItem {
id(it.userId)
name(it.displayName)
avatarUrl(it.avatarUrl)
id(user.userId)
name(user.displayName)
avatarUrl(user.avatarUrl)
clickListener { _ ->
listener?.onEvent(user)
}
}
}
}

View file

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.autocomplete.user
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@ -32,8 +33,12 @@ abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Hold
var name: String? = null
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
holder.nameView.text = name
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView)
}

View file

@ -21,14 +21,19 @@ import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.features.autocomplete.EpoxyViewPresenter
import im.vector.riotredesign.core.listener.Listener
import im.vector.riotredesign.features.autocomplete.EpoxyAutocompletePresenter
class AutocompleteUserPresenter(context: Context,
private val controller: AutocompleteUserController
) : EpoxyViewPresenter<User>(context) {
) : EpoxyAutocompletePresenter<User>(context), Listener<User> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun providesController(): EpoxyController {
return controller
}

View file

@ -20,12 +20,14 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.fragmentViewModel
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
@ -34,6 +36,7 @@ import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.command.CommandPolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer
@ -133,10 +136,22 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText)
.with(CharPolicy('/', false))
.with(CommandPolicy())
.with(autocompleteCommandPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable?, item: Command?): Boolean {
editable?.clear()
editable
?.append(item?.command)
?.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteUserPresenter.callback = this
@ -145,6 +160,17 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
.with(autocompleteUserPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<User> {
override fun onPopupItemClicked(editable: Editable?, item: User?): Boolean {
// TODO
editable?.append(item?.displayName)
?.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
sendButton.setOnClickListener {