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. * See [SampleKotlinModelWithHolder] for a usage example.
*/ */
abstract class VectorEpoxyHolder : EpoxyHolder() { abstract class VectorEpoxyHolder : EpoxyHolder() {
private lateinit var view: View lateinit var view: View


override fun bindView(itemView: View) { override fun bindView(itemView: View) {
view = itemView 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.airbnb.epoxy.EpoxyRecyclerView
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.AutocompletePresenter 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 recyclerView: EpoxyRecyclerView? = null
private var clicks: AutocompletePresenter.ClickProvider<T>? = null private var clicks: AutocompletePresenter.ClickProvider<T>? = null
@ -73,6 +74,10 @@ abstract class EpoxyViewPresenter<T>(context: Context) : AutocompletePresenter<T
observer?.onChanged() observer?.onChanged()
} }


override fun onEvent(t: T) {
dispatchClick(t)
}



private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() {


View File

@ -17,21 +17,27 @@
package im.vector.riotredesign.features.autocomplete.command package im.vector.riotredesign.features.autocomplete.command


import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotredesign.core.listener.Listener
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.command.Command


class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() { class AutocompleteCommandController(private val stringProvider: StringProvider) : TypedEpoxyController<List<Command>>() {


var listener: Listener<Command>? = null

override fun buildModels(data: List<Command>?) { override fun buildModels(data: List<Command>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }
data.forEach { data.forEach { command ->
autocompleteCommandItem { autocompleteCommandItem {
id(it.command) id(command.command)
name(it.command) name(command.command)
parameters(it.parameters) parameters(command.parameters)
description(stringProvider.getString(it.description)) description(stringProvider.getString(command.description))
clickListener { _ ->
listener?.onEvent(command)
}
} }
} }
} }

View File

@ -16,6 +16,7 @@


package im.vector.riotredesign.features.autocomplete.command package im.vector.riotredesign.features.autocomplete.command


import android.view.View
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -32,8 +33,12 @@ abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandIte
var parameters: CharSequence? = null var parameters: CharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var description: CharSequence? = null var description: CharSequence? = null
@EpoxyAttribute
var clickListener: View.OnClickListener? = null


override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)

holder.nameView.text = name holder.nameView.text = name
holder.parametersView.text = parameters holder.parametersView.text = parameters
holder.descriptionView.text = description holder.descriptionView.text = description

View File

@ -18,12 +18,16 @@ package im.vector.riotredesign.features.autocomplete.command


import android.content.Context import android.content.Context
import com.airbnb.epoxy.EpoxyController 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 import im.vector.riotredesign.features.command.Command


class AutocompleteCommandPresenter(context: Context, class AutocompleteCommandPresenter(context: Context,
private val controller: AutocompleteCommandController private val controller: AutocompleteCommandController) :
) : EpoxyViewPresenter<Command>(context) { EpoxyAutocompletePresenter<Command>(context) {

init {
controller.listener = this
}


override fun providesController(): EpoxyController { override fun providesController(): EpoxyController {
return controller 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 com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User 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>?) { override fun buildModels(data: List<User>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }
data.forEach { data.forEach { user ->
autocompleteUserItem { autocompleteUserItem {
id(it.userId) id(user.userId)
name(it.displayName) name(user.displayName)
avatarUrl(it.avatarUrl) avatarUrl(user.avatarUrl)
clickListener { _ ->
listener?.onEvent(user)
}
} }
} }
} }

View File

@ -16,6 +16,7 @@


package im.vector.riotredesign.features.autocomplete.user package im.vector.riotredesign.features.autocomplete.user


import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -32,8 +33,12 @@ abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Hold
var name: String? = null var name: String? = null
@EpoxyAttribute @EpoxyAttribute
var avatarUrl: String? = null var avatarUrl: String? = null
@EpoxyAttribute
var clickListener: View.OnClickListener? = null


override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)

holder.nameView.text = name holder.nameView.text = name
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView) 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.Async
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.user.model.User 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, class AutocompleteUserPresenter(context: Context,
private val controller: AutocompleteUserController private val controller: AutocompleteUserController
) : EpoxyViewPresenter<User>(context) { ) : EpoxyAutocompletePresenter<User>(context), Listener<User> {


var callback: Callback? = null var callback: Callback? = null


init {
controller.listener = this
}

override fun providesController(): EpoxyController { override fun providesController(): EpoxyController {
return controller return controller
} }

View File

@ -20,12 +20,14 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User 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.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter 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.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
@ -133,10 +136,22 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
val elevation = 6f val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE) val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText) Autocomplete.on<Command>(composerEditText)
.with(CharPolicy('/', false)) .with(CommandPolicy())
.with(autocompleteCommandPresenter) .with(autocompleteCommandPresenter)
.with(elevation) .with(elevation)
.with(backgroundDrawable) .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() .build()


autocompleteUserPresenter.callback = this autocompleteUserPresenter.callback = this
@ -145,6 +160,17 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
.with(autocompleteUserPresenter) .with(autocompleteUserPresenter)
.with(elevation) .with(elevation)
.with(backgroundDrawable) .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() .build()


sendButton.setOnClickListener { sendButton.setOnClickListener {