I have a text view to which I need to create a listener for onLongClick.
Right now for the respective viewmodel it has a function sendLogs() which deals with the logic for onClick. If I change onClick to onLongClick function never get call. Is there any way to make it work for onLongClick?
onClick is directly linked to my model class function but not the onLongClick. So I think model class binding is correct but I may need some extra work here.
<data>
<import type="android.view.View" />
<variable
type="com.aaa.bbb.viewmodel.SystemSettingsViewModel"
name="systemSettings"
</variable>
</data>
<TextView
android:gravity="end"
android:id="#+id/tv_logging"
android:layout_centerVertical="true"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_width="wrap_content"
android:onClick="#{() -> systemSettings.sendLogs()}"
android:text="#string/enable_logs"
android:textAlignment="viewEnd" />
I managed to work it correctly. I doubt this is properly documented.
In xml
android:onLongClick="#{(view) -> presenter.onLongClickOnHeading(view)}"
In presenter viewmodel class
public boolean onLongClickOnHeading(View v) {
//logic goes here
return false;
}
Note: this method signature should be exactly in this format. Otherwise biding errors will be thrown at runtime.
Here is the complete the code.
There is no such attribute for long click. So we have to create a binding adapter.
BindingUtils.kt
object BindingUtils {
private const val ON_LONG_CLICK = "android:onLongClick"
#JvmStatic
#BindingAdapter(ON_LONG_CLICK)
fun setOnLongClickListener(
view: View,
func : () -> Unit
) {
view.setOnLongClickListener {
func()
return#setOnLongClickListener true
}
}
}
Layout
<androidx.constraintlayout.widget.ConstraintLayout
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:onLongClick="#{() -> vm.onLongClick()}"/>
For it to work, the part in parenthesis has to match the method signature from the interface View.OnLongClickListener which looks like this :
boolean onLongClick(View view);
So this is how I got it to work :
<View
...
android:onLongClick="#{(view) -> listener.onLongClick(view, viewmodel)}"/>
...
In the xml section, you must refer to the Boolean return function, such as the following code, so as not to get into trouble.in build project android studio
in xml
android:onLongClick="#{(view) -> presenter.onLongClick(view)}"
in java
public boolean onLongClick(View v) {
return false;
}
You should look into this document
OnLongClick is as easy as onClick
Within your SystemSettingsViewModel you can have
public boolean onLongClick(){}
and in xml
android:onLongClick="#{() -> presenter.onLongClick()}"
As mentioned in the Google documentation Link there is no problem with what you wrote.
This is a sample of OnLongClick in XML:
android:onLongClick="#{(theView) -> presenter.onLongClick(theView, task)}"
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
Related
I'm working on an android project in kotlin and while trying to add an OnTouchListener to several buttons, I have run into a problem: it cannot be done from XML and I want to keep my backing code clean. After a bit of research, I found out that I could add the XML support by using a method with the #BindingAdapter annotation:
#BindingAdapter("onTouch")
fun Button.setXMLTouchListener(listener : View.OnTouchListener)
{
this.setOnTouchListener(listener);
}
to this method:
class MainActivity : AppCompatActivity()
{
...
...
fun goLeft(v : View, event : MotionEvent) : Boolean
{
// my code
}
}
and in the XML:
<layout
...>
<data>
<variable name="main_activity" type="my.path.to.MainActivity" />
</data>
<androidx.constraintLayout.widget.ConstraintLayout
...>
<Button
...
app:onTouch="#{main_activity.goLeft}" />
...
</androidx.constraintLayout.widget.ConstraintLayout>
</layout>
and enabled data binding in the build.gradle:
apply plugin: 'kotlin.kapt'
and
android {
...
dataBinding {
enabled = true
}
}
This obviously didn't work, these are the solutions I have tried:
move the #Bindingadapter function from companion object to top level, so it's compiled static
try the app:onTouch contents as "main_activity.goLeft" (seen in a tutorial), "main_activity.goLeft()" (original try), and "main_activity::goLeft" (suggested by the compiler as the first is deprecated)
add logging to the click event to ensure the button receives events at all
change the value of the annotation to "app:onTouch" to be absolutely sure it's in the right xml namespace
move the touch listener function to a class that is non-activity and implements View.OnTouchListener (and renamed function accordingly)
After a bit of debugging, I also found out that the binding function doesn't run at all.
What could be the problem, and how can I solve it?
first write your data binding adapter like this.
#BindingAdapter("app:onTouch")
fun setXMLTouchListener(btn : Button , listener : View.OnTouchListener)
{
btn.setOnTouchListener(listener)
}
then chenge the goLeft() fun to it
val goLeftListener = View.OnTouchListener { v, event ->
Log.d("goLeftListener " , "it Worked !")
return#goLeftListener true
}
and don't forget to set activity on your binding object in onCreate fun
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: mainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.main_activity = this
}
and for the last step write onTouch attribute of Button in your xml layout like this
<Button
.
.
app:onTouch="#{main_activity.goLeftListener}"
.
/>
I have a Custom component in android with this layout.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
when using in another layout I find editText by this code.(Espresso)
val editText = onView(
allOf(withId(R.id.editText)
, isDescendantOfA(withId(R.id.mainLayout))
, isDescendantOfA(withId(R.id.mobileEdt))
)
)
I use this custom Component in all app and many layouts.
can I minify or convert to function in my app for doesn't write again and again?
Maybe I change the component layout, so I have to edit all withId in all test.
Your component has probably a class name. Let's say CustomEditText.
In that case you can implement a BoundedMatcher based custom matcher, which makes sure that it will only match view instances of your CustomEditText.
Simple implementation could look like this:
fun customEditWithId(idMatcher: Matcher<Int>): Matcher<View> {
return object : BoundedMatcher<View, CustomEditText>(CustomEditText::class.java!!) {
override fun describeTo(description: Description) {
description.appendText("with id: ")
idMatcher.describeTo(description)
}
override fun matchesSafely(textView: CustomEditText): Boolean {
return idMatcher.matches(textView.id)
}
}
}
then your assertion looks like this:
onView(customEditWithId(0)).perform(click());
it's obviously not a descendant of R.id.mobileEdt ...
val editText = onView(allOf(
withId(R.id.editText),
isDescendantOfA(withId(R.id.mainLayout))
))
I am trying to use last features from android - Kotlin, mvvm, architecture components, jetpack, databinding, one activity - many fragments approach with new navigation graph, but I am struggling with handling UI events in Fragments
In activity it is simple with kotlin-android-extensions
In XML I create a Button like this:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="clicked"/>
and in Activity I just write
fun clicked(view : View){
}
That's perfect, but unfortunately does not work in Fragment. Yes it is possible to still handle event in Activity and send it to fragment but that's ugly.
Next option is to use an interface,
public interface MyClickCallback{
void onLoginButtonClick();
}
implement it in fragment.
In xml it looks like this:
<variable
name="clickCallback"
type="com.test.MyClickCallback" />
then in fragment's onCreateView I have to set clickCallback to the fragment and finally I can use it
#Override fun onLoginButtonClick() {
}
Problem I have with this is to declare interface and on each new UI event enhance this interface and update fragment which implements it
Next option is RxView.clicks what looks really great with all its features. For example:
RxView.clicks(mSearchBtn)
.throttleFirst(2, TimeUnit.SECONDS)
.map(aVoid -> mSearchEdit.getText().toString().trim())
.filter(s -> !TextUtils.isEmpty(s))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
KeyBoardUtil.closeKeybord(mSearchEdit,
SearchActivity.this);
showSearchAnim();
clearData();
content = s;
getSearchData();
});
Problem here is that I have to bind it to the UI component - mSearchBtn. I do not want this :-). I do not want to have any UI component in fragment unless I really have to. I am always communicating with layout file via variables declared in layout like this
<data>
<variable
name="items"
type="java.util.List" />
</data>
I would love to bind it to variable declared in the XML which is set in Button
android:onClick="myclick"
But I did not find the way how to do it.
Anybody can help me maybe with other simple and nice options ?
In your databinding layout create a variable that is of type View.OnClickListener:
<variable
name="onClickListener"
type="android.view.View.OnClickListener" />
Set it to your View like this:
<View
...
android:onClickListener="#{onClickListener}"
... />
In your Fragment create the onClickListener and set it to the variable:
binding.onClickListener = View.OnClickListener {
/* do things */
/* like getting the id of the clicked view: */
val idOfTheClickedView = it.id
/* or get variables from your databinding layout: */
val bankAccount = binding.bankAccount
}
Or in Java:
binding.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
/* do things */
/* like getting the id of the clicked view: */
Int idOfTheClickedView = view.getId();
/* or get variables from your databinding layout: */
Object bankAccount = binding.getBankAccount()
}
});
it is simple with kotlin-android-extensions
It is indeed simple, but you are currently not using it to its fullest potential.
Setting click listeners in Kotlin is very easy, look:
fun View.onClick(clickListener: (View) -> Unit) {
setOnClickListener(clickListener)
}
And now thanks to synthetic imports in Kotlin-Android-Extensions:
<Button
android:id="#+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="#string/click_me"/>
and
import kotlinx.synthetic.blah.* // something like that
// Activity:
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
setContentView(R.layout.blah)
myButton.onClick {
// handle click event
}
}
// Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?) = inflater.inflate(R.layout.blah, container, false)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
myButton.onClick {
// handle click event
}
}
But if you really want to use databinding and layouts for this, then set the callback lambda and inside the databinding layout file.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="activity" type="com.acme.MainActivity"/>
</data>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/btnOpenSecondView"
android:text="Click me for second view!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:onClick="#{(v) -> activity.startNextActivity(v)}" />
</RelativeLayout>
</layout>
So I have a viewHolder with a checkbox
here is my viewModel
#Bindable
var itemIsSelected: Boolean = isSelected
set(value) {
if (field != value) {
field = value
notifyPropertyChanged(BR.itemIsSelected) // this doesn't work
notifyChange() // this one works
}
}
here is my viewHolder class
inner class SpecialityItemViewHolder(val binding: ItemSpecialityFilterBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(specialityItemViewModel: SpecialityItemViewModel) {
binding.viewModel = specialityItemViewModel
binding.executePendingBindings()
this.itemView.setOnClickListener {
binding.viewModel?.let {
it.itemIsSelected = !it.itemIsSelected // this doesn't trigger ui changes
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="packagename.ItemViewModel" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/vertical_margin_small"
android:paddingBottom="#dimen/vertical_margin_small"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<Checkbox
android:id="#+id/checkbox"
android:layout_width="25dp"
android:layout_height="25dp"
android:checked="#={viewModel.itemIsSelected}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
so what happens is that the setting is working properly as in that when i press the checkbox it sets the backing field to the corresponding value
but when i set the backing field (notice code in bind function) it doesn't trigger ui change I know that calling binding.executePendingBindings() would solve the problem but my understanding is that notifyPropertyChanged(BR.itemIsSelected) should not need executePendingBindings call, Actually if i call notifyChange instead everything works properly ( but I presume there is performance issue here as it notifies change for all properties instead )
Your ViewModel class have to extend BaseObservable class and using kotlin you have to use #get:Bindable annotation. If you don't want to use BaseObservable as a parent class then use ObservableField<Boolean>(). You find more information in https://developer.android.com/topic/libraries/data-binding/observability#kotlin
the view-model needs Kotlin annotations, else the annotation processor will ignore it:
class ViewModel : BaseObservable() {
#get:Bindable
var isSelected: Boolean
set(value) {
if (isSelected != value) {
isSelected = value
notifyPropertyChanged(BR.isSelected)
}
}
}
it.isSelected is easier to read than it.itemIsSelected.
I'm trying to use Kotlin extension methods inside Android's databinding. For example; calling an onclick handler. So I've made this code:
posttest_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
<import type="android.view.View"/>
<import type="com.example.test.post.posttest.PostTestItemViewModelExtensionKt" />
<variable
name="viewModel"
type="com.example.test.post.posttest.PostTestItemViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:clickable="true"
android:onClick="#{(view) -> viewModel.clicked(view)}"
>
[...]
PostTestItemViewModel.kt
open class PostTestItemViewModel : ViewModel() {
val postTitle = MutableLiveData<String>()
val postBody = MutableLiveData<String>()
/**
* Binds the required properties/entities to this ViewModel
*/
fun bind(post: Post) {
postTitle.value = post.title
postBody.value = post.body
}
}
PostTestItemViewModelExtension.kt
fun PostTestItemViewModel.clicked(v: View) {
this.postTitle.value = "clicked"
}
So when I place the clicked method inside the viewmodel, it works perfectly the way it should be. However, when I create it as an extension method, I get the following error on compilation:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
cannot find method clicked(android.view.View) in class ...PostItemViewModel
I've tried different things already, such as changing the android:onclick tag to PostTestItemViewModelExtensionKt instead of viewModel. Unfortunately all the things don't seem to work. So it looks like the extension method is getting generated after the databinding takes place. Is there a way around this or am I still doing something wrong? Or is it just not possible to bind extension methods?
I'm using Kotlin version 1.2.71, gradle 3.2.0 and have the databinding { enabled = true } and kapt { generateStubs = true } added to my .gradle, and have the plugings kotlin-android, kotlin-android-extensions and kotlin-kapt defined.
Unfortunately you can't use extension methods as onClick callbacks.
Extension methods in Kotlin are created as Java static methods while the Android framework is expecting an instance method.
Note that in Android Studio you can decompile the Kotlin classes as Java to see the generated Java code.
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78