How to define behavior from the object instance - android

I have an EditText that I overrode in order to detect clicks on the compound drawables.
class MyEditText
constructor(
context: Context,
attrs: AttributeSet
) : androidx.appcompat.widget.AppCompatEditText( context, attrs) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
val result = super.onTouchEvent(event)
if (event != null) {
// All advice says to use ACTION_UP but that never gets here
if( event.action == MotionEvent.ACTION_DOWN ){
if(event.x <= this.totalPaddingStart){
onStartDrawableClick()
}
if(event.x >= this.width - this.totalPaddingEnd){
onEndDrawableClick()
}
performClick()
return true
}
}
return result
}
// Overriding this avoids an accessibility warning
override fun performClick(): Boolean {
return super.performClick()
}
public fun onEndDrawableClick(){
Log.e(TAG, "onEndDrawableClick: ")
}
public fun onStartDrawableClick(){
Log.e(TAG, "onStartDrawableClick: ")
}
}
This works but I want to be able to define what happens in onEndDrawableClick() from the MyEditText object instance, not in the class. I cant pass a closure to the constructor since its a view with params for xml instantiation. Is there an elegant way to do this?
(Extra bonus points for figuring out why ACTION_UP is never seen)

You can define callback properties that can be set from outside the class. Also, you can make the MotionEvent parameter non-nullable since the Android function will never pass you a null value. Then you don't have to do the null check.
Also, if you don't want other click events to happen (like if you set a click listener on the TextView) when you click on this item, you should not call super when the icon is clicked. And you should call through when the touch misses the icon instead of returning true. Example of rearranging the logic like this below.
class MyEditText(
context: Context,
attrs: AttributeSet
) : androidx.appcompat.widget.AppCompatEditText(context, attrs) {
var onEndDrawableClick: (()->Unit)? = null
var onStartDrawableClick: (()->Unit)? = null
override fun onTouchEvent(event: MotionEvent): Boolean {
if( event.action == MotionEvent.ACTION_DOWN ){
if(event.x <= this.totalPaddingStart){
onStartDrawableClick?.invoke()
performClick()
return true
}
if(event.x >= this.width - this.totalPaddingEnd){
onEndDrawableClick?.invoke()
performClick()
return true
}
}
return super.onTouchEvent(event)
}
// Overriding this avoids an accessibility warning
override fun performClick(): Boolean {
return super.performClick()
}
}
Handling a click that behaves as users are probably accustomed, i.e. has visual/audible feedback on touch down, is cancellable by moving your finger off the target before releasing, and firing only if released over the touch target; is not trivial. You might want to build this UI component out of a RelativeLayout or ConstraintLayout that contains an EditText and two Buttons in it to provide a nicer experience.

Apologies for my rusty Kotlin, but something like this should work:
var endClickHandler: View.OnClickListener?
public fun onEndDrawableClick(){
Log.e(TAG, "onEndDrawableClick: ")
endClickHandler?.onClick(this)
}

Related

Kotlin OnTouchListener called but it does not override performClick

How to override performClick in Kotlin to avoid warning?
next.setOnTouchListener(View.OnTouchListener { view, motionEvent ->
when (motionEvent.action){
MotionEvent.ACTION_DOWN -> {
val icon: Drawable = ContextCompat.getDrawable(activity.applicationContext, R.drawable.layer_bt_next)
icon.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY)
next.setImageDrawable(icon)
}
MotionEvent.ACTION_UP -> {
//view.performClick()
next.setImageResource(R.drawable.layer_bt_next)
}
}
return#OnTouchListener true
})
view.performClick does not work.
Try this way :
next.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> //Do Something
}
return v?.onTouchEvent(event) ?: true
}
})
Okay, I solved my own problem by overriding the OnTouch listener.
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (view) {
next -> {
Log.d("next", "yeyy")
when (motionEvent.action){
MotionEvent.ACTION_DOWN -> {
val icon: Drawable = ContextCompat.getDrawable(activity.applicationContext, R.drawable.layer_bt_next)
icon.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY)
next.setImageDrawable(icon)
}
MotionEvent.ACTION_UP -> {
view.performClick()
next.setImageResource(R.drawable.layer_bt_next)
}
}
}
previous -> {
//ingredients here XD
}
}
return true
}
And in that way, I can call single onTouch and implement it to many button and also can use the onClick by :
view.performClick()
Don't forget to implement :
View.OnTouchListener
And set the listener :
next.setOnTouchListener(this)
previous.setOnTouchListener(this)
I don't think your solution will actually solve them problem presented by the warning. The warning states that certain accessibility functions use performClick() to activate buttons. If you look in the View class, the performClick() funtions calls the onClickListener directly, meaning the code in the onTouchListener will not be executed (next.setImageResource(R.drawable.layer_bt_next)) for these accessibility functions, since the view will never be physically touched, and thus your onTouch code won't run. You have to do one of either:
Subclass the view you are setting the onTouchListener on, and override performClick to execute the code, or
Set an onClickListener on the view that executes the code.
You could just implement onClickListener in your onTouchListener class and manually call onClick() from your onTouchListener (where you have view.performClick() now), and then move your executable code to the onClick override. You would also have to set BOTH onTouchListener and onClickListener on your views.
I'm not sure this is the same issue you saw, but since I found this page searching for my issue, I thought I'd add my experience to help others :)
In my case the warning was being generated because the nullable view could have been of type Void. Calling the following:
nullableView?.setOnTouchListener(this)
produced the error:
Custom view Void has setOnTouchListener called on it but does not override performClick
Performing a null check and casting to a View before setting the listener solved for me in this case, since View will override performClick:
if (nullableView != null) (nullableView as View).setOnTouchListener(this)
After a ton of digging, and not being able to fix my variation of this issue with anything in this thread, I finally found a fix. Maybe it will work for some of you. I had this widget listener setter in my MainActivity onCreate function:
findViewById<TextView>(R.id.tvAnimalList).setOnTouchListener { v, event ->
mGestureDetector.onTouchEvent(event)
}
Which results in the warnings:
'onTouch' lambda should call 'View#performClick' when a click is detected
Custom view "TextView" has 'setOnTouchListener' called on it but does not override 'performClick'
First, I added a call to v.performClick(), which got rid of the first warning. Like this:
findViewById<TextView>(R.id.tvAnimalList).setOnTouchListener { v, event ->
v.performClick()
mGestureDetector.onTouchEvent(event)
}
I got rid of the second warning by changing the findViewById cast from <TextView> to <View>. Here's my warning-free result:
findViewById<View>(R.id.tvAnimalList).setOnTouchListener { v, event ->
v.performClick()
mGestureDetector.onTouchEvent(event)
}
private fun closeKeyboard(binding: ContollerMeterBinding) {
binding.scrollView.apply {
setOnTouchListener(OnTouchListener { v, event ->
if (event != null && event.action == MotionEvent.ACTION_MOVE) {
val imm =
activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val isKeyboardUp = imm.isAcceptingText
if (isKeyboardUp) {
imm.hideSoftInputFromWindow(v.windowToken, 0)
}
}
performClick()
false
})
}
}
This works for me: (not directly related to onTouch event but yields the same warning, might be helpful to someone)
takePhotoButton.setOnTouchListener { _, motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
//when user touch down
}
MotionEvent.ACTION_UP -> {
//when user touch release
}
}
true
}

Android kotlin onTouchListener wants me to override performClick()

I'm trying to get rid of a warning where Android Studio wants my onTouchListener to override performClick which I do, but the warning remains.
draggableBar!!.setOnTouchListener(View.OnTouchListener { view, motionEvent ->
when (motionEvent.getAction()) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_UP -> {
view.performClick()
}
}
return#OnTouchListener true
})
Could this be an Android Studio bug or am I doing something wrong?
Okay, I have the same problem but i fixed it by overriding the onTouch listener.
The default onTouch wants us to override performClick(), but this does not work even calling the method by view.performClick().
So therefore override your onTouch like this:
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (view) {
draggableBar -> {
when (motionEvent.getAction()) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_UP -> {
view.performClick()
}
}
}
otherButtonHere -> {
//your welcome
}
}
return true
}
And in that way, you can use single onTouch() in all clickable views you have.
Don't forget to implement to your Class:
View.OnTouchListener
And set the listener:
draggableBar!!.setOnTouchListener(this)
HOPE IT HELPS! :)

How do I make a draggable LimitLine in MPAndroidChart

I have managed to create a line chart using MP Android Chart, And want to create a draggable line to set a limit. So if the value crosses the line value, then the user gets an alert. My use case is similar to the Android system Data usage limit.
I came across LimitLines - https://github.com/PhilJay/MPAndroidChart/wiki/The-Axis and also dragging using touch event callbacks https://github.com/PhilJay/MPAndroidChart/wiki/Interaction-with-the-Chart
My question is whether I can add the limit line dynamically on the response to a translate (onChartTranslate) event so I can simulate the limit setting? Is this a better approach than trying to overload the MarkerView ?
I managed to create a draggable (horizontal) LimitLine by using OnChartGestureListener to listen for begin and end of drag events:
chart.onChartGestureListener = object: OnChartGestureListener {
override fun onChartGestureEnd(me: MotionEvent?, lastPerformedGesture: ChartTouchListener.ChartGesture?) {
if (lastPerformedGesture == ChartTouchListener.ChartGesture.DRAG && limitLineDragEnabled) {
me?.let {
displayLimitLine(me)
}
limitLineDragEnabled = false
}
}
override fun onChartLongPressed(me: MotionEvent?) {
if (binding.chart.isFullyZoomedOut) {
limitLineDragEnabled = true
me?.let {
displayLimitLine(me)
}
}
}
}
and a ChartTouchListener to catch MotionEvents between drag begin and end (sadly, OnChartGestureListener wasn't able to help me there):
chart.onTouchListener = object: BarLineChartTouchListener(chart, chart.viewPortHandler.matrixTouch, 3f) {
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, me: MotionEvent?): Boolean {
if (limitLineDragEnabled) {
me?.let {
displayLimitLine(me)
}
}
return super.onTouch(v, me)
}
}

Change Switch state without animation

In my Android project, I have a ListView with rows containing SwitchCompat items (AppCompat for Switch widget).
My problem occurs when I scroll into the list and getView(...) method of MyAdapter is invoked with a recycled view. I redefine the correct Switch state but the animation is visible.
There is a solution to prevent the animation in this case?
Call jumpDrawablesToCurrentState() to skip the animation
switchCompat.setChecked(true);
switchCompat.jumpDrawablesToCurrentState();
I finally found a solution but seems not really clean:
ViewGroup viewGroup = (ViewGroup) view; // the recycled view
viewGroup.removeView(switch);
switch.setChecked(states[index]);
viewGroup.addView(switch);
If a better solution exists, please share it.
The issue in with animation playing in the list can be present if you use Android Databinding.
To resolve it, run binding.executePendingBindings() method after you set data – it will refresh binding state for the component in current frame and will not wait for the next one to come.
As you have probably guessed already – next frame is the animation
I had the same problem and I managed to solved it using some minimal reflection.
Usage:
To change the switch state without animation call the setChecked(boolean checked, boolean animate) method with false for the animate parameter. If the switch already is animating at the moment this method is being called the animation will be stopped and the switch jumps to the desired position.
SwitchCompatFix.java
import android.content.Context;
import android.support.v7.widget.SwitchCompat;
import android.util.AttributeSet;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Work around for: http://stackoverflow.com/questions/27139262/change-switch-state-without-animation
* Possible fix for bug 101107: https://code.google.com/p/android/issues/detail?id=101107
*
* Version 0.2
* #author Rolf Smit
*/
public class SwitchCompatFix extends SwitchCompat {
public SwitchCompatFix(Context context) {
super(context);
initHack();
}
public SwitchCompatFix(Context context, AttributeSet attrs) {
super(context, attrs);
initHack();
}
public SwitchCompatFix(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHack();
}
private Method methodCancelPositionAnimator = null;
private Method methodSetThumbPosition = null;
private void initHack(){
try {
methodCancelPositionAnimator = SwitchCompat.class.getDeclaredMethod("cancelPositionAnimator");
methodSetThumbPosition = SwitchCompat.class.getDeclaredMethod("setThumbPosition", float.class);
methodCancelPositionAnimator.setAccessible(true);
methodSetThumbPosition.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public void setChecked(boolean checked, boolean animate){
// Java does not support super.super.xxx calls, a call to the SwitchCompat default setChecked method is needed.
super.setChecked(checked);
if(!animate) {
// See original SwitchCompat source:
// Calling the super method may result in setChecked() getting called
// recursively with a different value, so load the REAL value...
checked = isChecked();
// Cancel any running animations (started by super.setChecked()) and immediately move the thumb to the new position
try {
if(methodCancelPositionAnimator != null && methodSetThumbPosition != null) {
methodCancelPositionAnimator.invoke(this);
methodSetThumbPosition.invoke(this, checked ? 1 : 0);
}
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
Note for proguard users:
Because this method uses reflection an additional proguard rule might be needed (if not yet present).
-keep class android.support.v7.widget.SwitchCompat {
private void cancelPositionAnimator();
private void setThumbPosition(float);
}
This additional rule is not needed when you're using one of the following proguard rules (or similar ones):
-keep class android.support.v7.widget.** { *; }
-keep class android.support.v7.** { *; }
Using SwitchCompat and DataBinding
#BindingAdapter({"bind:checkedState"})
public static void setCheckedState(SwitchCompat switchView, boolean checked) {
int visibility = switchView.getVisibility();
switchView.setVisibility(View.INVISIBLE);
switchView.setChecked(checked);
switchView.setVisibility(visibility);
}
Then in xml:
<android.support.v7.widget.SwitchCompat
android:id="#+id/my_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:checkedState="#{my_data.checked}"/>
And don't forget to call executePendingBindings() (thanks AAverin)
For Kotlin developer:
fun SwitchCompat.setCheckedWithoutAnimation(checked: Boolean) {
val beforeVisibility = visibility
visibility = View.INVISIBLE
isChecked = checked
visibility = beforeVisibility
}
And the usage:
mySwitch.setCheckedWithoutAnimation(true)
Kotlin sample of Rolf ツ answer.
class SwitchImproved(context: Context, attributeSet: AttributeSet) : SwitchCompat(context, attributeSet) {
private lateinit var methodCancelPositionAnimator: Method
private lateinit var methodSetThumbPosition: Method
init {
initHack()
}
fun setChecked(checked: Boolean, animate: Boolean = true) {
super.setChecked(checked)
if (!animate) {
methodCancelPositionAnimator.invoke(this)
methodSetThumbPosition.invoke(this, if (isChecked) 1 else 0)
}
}
private fun initHack() {
methodCancelPositionAnimator = SwitchCompat::class.java.getDeclaredMethod("cancelPositionAnimator")
methodSetThumbPosition = SwitchCompat::class.java.getDeclaredMethod("setThumbPosition", Float::class.javaPrimitiveType)
methodCancelPositionAnimator.isAccessible = true
methodSetThumbPosition.isAccessible = true
}
}
In my case, I am using the new material library:
implementation 'com.google.android.material:material:1.1.0-alpha07'
and in the setChecked method of this class there is this condition:
if (getWindowToken() != null && ViewCompat.isLaidOut(this))
So what I did was to create a class that extends from this SwitchMaterial, and deal with "isLaidOut". The code is the next one (omitting constructors):
class SwitchCustomView : SwitchMaterial {
private var laidOutForAnimation = false
fun setChecked(checked: Boolean, animate: Boolean) {
if (!animate) {
laidOutForAnimation = true
}
super.setChecked(checked)
laidOutForAnimation = false
}
override fun isLaidOut(): Boolean {
return if (laidOutForAnimation) {
return false
} else {
super.isLaidOut()
}
}
}
Then just use this class in your xml and call programatically
setChecked(checked: Boolean, animate: Boolean)

How to disable all click events of a layout?

I have a layout that contains many views. Is there an easy way to disable all its views click events?
You can pass View for disable all child click event.
public static void enableDisableView(View view, boolean enabled) {
view.setEnabled(enabled);
if ( view instanceof ViewGroup ) {
ViewGroup group = (ViewGroup)view;
for ( int idx = 0 ; idx < group.getChildCount() ; idx++ ) {
enableDisableView(group.getChildAt(idx), enabled);
}
}
}
Rather than iterating through all the children view, you can add this function to the parent Layout view
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
This will be called before the onTouchEvent for any child views, and if it returns true, the onTouchEvent for child views wont be called at all. You can create a boolean field member to toggle this state on and off if you want.
I would create a ViewGroup with all the views that you want to enable/disable at the same time and call setClickable(true/false) to enable/disable clicking.
Here is a Kotlin extension function implementation of Parag Chauhan's answer
fun View.setAllEnabled(enabled: Boolean) {
isEnabled = enabled
if (this is ViewGroup) children.forEach { child -> child.setAllEnabled(enabled) }
}
You need to call setEnabled(boolean value) method on the view.
view.setClickable(false);
view.setEnabled(false);
If you dont want to set all child views to disable state (because they may look different to enable state) you can use this approach:
private fun toogleTouchable(canTouch: Boolean) {
container.descendantFocusability =
if (value) {
container.requestFocus()
ViewGroup.FOCUS_BLOCK_DESCENDANTS
} else {
ViewGroup.FOCUS_AFTER_DESCENDANTS
}
}
my layout has two modes. One preview, when children should not be clickable and the other when children should be clickable.
Of all the solutions mentioned here, I found overriding onInterceptTouchEvent most appropriate. Very little code, with no iteration on children.
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(isPreviewMode())
{
//No child is clickable in preview mode.
return true;
}
//All children are clickable otherwise
return false;
}
Though disabling children is most popular solution, I am not confortable with that.
You may need to reEnable them and the logic should match exactly. The more is the code, the higher the chances of bugs in the future.
Extension function in Kotlin, with the support of Recycler view.
fun ViewGroup.setEnabledStateForAllChildren(enable: Boolean) {
children.forEach {
when (it) {
is RecyclerView -> {
it.addOnItemTouchListener(object : SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
return enable.not()
}
})
}
is ViewGroup -> {
it.setEnabledStateForAllChildren(enable)
}
else -> {
it.isEnabled = enable
}
}
}
}
I would implement onClickListener interface in your activity class and return false in onClick method. I feel it's the easiest way to solve your problem.

Categories

Resources