We have pointerInput for detecting tap, drag and pan events, and it also provides a handy awaitPointerEventScope, the pointer being the finger, for mobile devices here. Now, we do have a awaitFirstDown() for detecting when the finger first makes contact with the screen, but I can't seem to find an upper equivalent to this method.
I have a little widget that I wish to detect taps on, but the thing is that the app is made for such a use-case that the user might be in weird positions during its use, and so I wished to have it trigger the required action on just a touch and lift of the finger. The paranoia is that the user might accidentally 'drag' their finger (even by a millimeter, android still picks it up), and I do not want that to be the case. I could implement a tap as well as a drag listener, but none of them offer a finger-lift detection, as far as I know.
What solution, if there is one as of now, is suitable for the use-case while adhering to and leveraging the declarative nature of Compose while keeping the codebase to a minimum?
Better way, and what is suggested by Android code if you are not using interoperability with existing View code is Modifier.pointerInput()
A special PointerInputModifier that provides access to the underlying
MotionEvents originally dispatched to Compose. Prefer pointerInput and
use this only for interoperation with existing code that consumes
MotionEvents. While the main intent of this Modifier is to allow
arbitrary code to access the original MotionEvent dispatched to
Compose, for completeness, analogs are provided to allow arbitrary
code to interact with the system as if it were an Android View.
val pointerModifier = Modifier
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
// ACTION_DOWN here
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
// ACTION_MOVE loop
// Consuming event prevents other gestures or scroll to intercept
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consumePositionChange()
}
} while (event.changes.any { it.pressed })
// ACTION_UP is here
}
}
}
This answer explains in detail how it works, internals and key points to consider when creating your own gestures.
Also this is a gesture library you can check out for onTouchEvent counterpart and 2 for detectTransformGestures with onGestureEnd callback and returns number of pointers down or list of PointerInputChange in onGesture event. Which can be used as
Modifier.pointerMotionEvents(
onDown = {
// When down is consumed
it.consumeDownChange()
},
onMove = {
// Consuming move prevents scroll other events to not get this move event
it.consumePositionChange()
},
onUp= {}
delayAfterDownInMillis = 20
)
Edit
As of 1.2.0-beta01, partial consumes like
PointerInputChange.consemePositionChange(),
PointerInputChange.consumeDownChange(), and one for consuming all changes PointerInputChange.consumeAllChanges() are deprecated
PointerInputChange.consume()
is the only one to be used preventing other gestures/event.
pointerInteropFilter is the way to go
Item(
Modifier.pointerInteropFilter {
if (it.action == MotionEvent.ACTION_UP) {
triggerAction()
}
true // Consume touch, return false if consumption is not required here
}
)
Related
When I use the CameraPositionState members isMoving and cameraMoveStartedReason, Google Maps freezes and stops responding. My goal is to do some work after the map is moved by gesture.
val singapore = LatLng(1.35, 103.87)
val cameraPositionState: CameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(singapore, 11f)
}
if(cameraPositionState.isMoving &&
cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
/* TODO */
}
How can I do it better?
Thank you.
Your map freezes because it recomposes while the map moves – which is a LOT btw, you can simply print it in your composable just to check that out. So you're doing some treatment at each recomposition.
You should rather treat it as a side effect instead. The simplest way would be with a LaunchedEffect:
LaunchedEffect(cameraPositionState.isMoving) {
if (cameraPositionState.isMoving && cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
// Do your work here, it will be done only when the map starts moving from a drag gesture.
}
}
Alternatively, you may want to have a look into derivedStateOf (also a side effect) that could better address that than a LaunchedEffect.
Your map freezes because compose always recreate it on camera position change. I have written an article on this topic, you can check out it and also know how it works:
https://towardsdev.com/jetpack-compose-google-map-camera-movement-listener-erselan-khan-5a9d1b223548
output result:
https://youtu.be/S5LV9bbXtzo
In an Android photo viewing app, I want to allow users to:
Zoom into a photo, drag to see its details, unzoom.
Swipe to go to the next photo.
Implementation attempt using ZoomableDraweeView in Facebook's Fresco library:
private fun init(imageUri: Uri?) {
val hierarchy = GenericDraweeHierarchyBuilder.newInstance(resources)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.setProgressBarImage(ProgressBarDrawable())
.setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.build()
zoomableDraweeView!!.hierarchy = hierarchy
zoomableDraweeView!!.setAllowTouchInterceptionWhileZoomed(false)
zoomableDraweeView!!.setIsLongpressEnabled(false)
zoomableDraweeView!!.setTapListener(DoubleTapGestureListener(zoomableDraweeView))
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
.setUri(imageUri)
.setControllerListener(loadingListener)
.build()
zoomableDraweeView!!.controller = controller
Problem: When I zoom in, lift the fingers, then try to unzoom, this gets misinterpreted as a swipe and I am randomly sent to the next picture.
What am I doing wrong? How to disable swipes when zoomed in (or any better UX solution)?
I specifically call setAllowTouchInterceptionWhileZoomed(false), whose javadoc says: "If this is set to true, parent views can intercept touch events while the view is zoomed. For example, this can be used to swipe between images in a view pager while zoomed."
I tried to execute the swipe action only when zoomableDraweeView.getZoomableController().isIdentity() is false, but that does not always prevent unintended swipe. In particular, when I fully zoom out, often swipe accidentally happens, maybe because isIdentity() has been updated by the time I release all fingers. getScaleFactor() has the same issue. Another issue is that this solution only allows me to drag the zoomed picture with two fingers, dragging with one finger has no effect.
I thought about writing my own DraweeController and surface the zoom level (and ignore swipes when zoomed) but the base classes do not seem to contain any zoom level information.
By the way, here is how I detect swipes:
open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
private inner class GestureListener :
GestureDetector.SimpleOnGestureListener() {
override fun onFling(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
try {
val diffX: Float = event2.x - event1.x
if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) >
SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
goToTheNextPhoto() // Swipe detected.
(full code on GitHub if needed)
The issue is not in the Fresco library or in the ZoomableDraweeView. The problem is in the orchestrating of the overall UX in your activity. It is not hard though, and you were on the right track with the zoomableDraweeView.getZoomableController().isIdentity() and setAllowTouchInterceptionWhileZoomed.
There are several ways to overcome it, starting from the custom ZoomableController and ending by exchanging your swipes detector with a proper ViewPager2(RecyclerView based)(or even two as you have vertical and horizontal swipes) with snapping and its own callbacks and animations. But the solution will be similar in all the cases - using everything where(and when) it is supposed to be used. It may require a bit of refactoring of your overall approach.
The first way to fix it is via setAllowTouchInterceptionWhileZoomed, which is not working for you because you apply your swipe detector directly to the ZoomableDraweeView rather than to its parent. Thus your view parent cannot handle swipes because you told it not to via setAllowTouchInterceptionWhileZoomed, but your ZoomableDraweeView can, and it does. Thus making you ZoomableDraweeView parent handle swipes rather than the view itself should do the trick.
Secondly, the ZoomableController is AnimatedZoomableController thus, it performs animation, and the animation has a duration(in this case, it depends on the pinch gesture velocity) - in the case when you were zooming out, and your swipe detector was changing the image - the animation was still ongoing, but the transformation matrix was already back to identity - thus the issue.
To fix this, you have to consider animation duration. I think even a simple 200ms delay should fix it in most cases. Also, I would suggest having an onTransformChanged method of the ZoomableDraweeView view(making a custom view) overridden, checking for the identity matrix in it, and exposing it to the activity in some way - this way, you will be sure that the transformation has already happened(the animation delay is still relevant in this case), and you can easily enable your own swipe detection.
The first method is preferable and more "clean," and the second one is "hacky," but its minimal implementation should be very fast.
I haven't tried to refactor it so I assume some additional work may be required.
Hope it helps.
I have an app which detects swipe on edge of the screen using an overlay view. Now I wanted to add an action which will be performed when the overlayed view is touched and held without moving. However, I observed some weird behaviours, which I suspect, after hours of trying and searching, is caused by the accidental palm touch rejection feature of Android. My observations:
If I touch and hold for some duration and then release without moving, no event is detected. (this is the problem)
If I quickly touch and release, both action down and up events are generated consecutively.
The above behave the same way with both MotionEvent and onClickListener.
If I touch and hold as long as I want, and then move without releasing, both down and move events are generated as soon as move starts.
If I remove FLAG_NOT_FOCUSABLE and FLAG_NOT_TOUCH_MODAL flags and write 0 there, whole screen becomes touchable and detects the touches correctly, except the left/right edges.
If I place the overlay away from the edge towards middle of the screen, everything works correctly.
On android emulator, everything works well with mouse clicks on default overlay position.
So, I attached a mouse to my physical phone with OTG, it also works correctly with mouse clicks.
In the main settings activity of the app, same behaviour is observed on preference buttons. No button animation when pressing and holding on edge of the screen, but animation and click works if I quickly touch and release.
I tried all flags of WindowManager.LayoutParams, tried to capture dispatch events of the view, none worked.
Is the problem palm rejection feature? Is there a way to bypass this and detect touch and hold (preferably with custom hold duration) on edges of the screen on overlay?
Note: Tried on Pixel 2 XL, Android 11.
val overlayedButton = View(this)
val overlayedWidth = 30
val overlayedHeight = resources.displayMetrics.heightPixels / 2
val LAYOUT_FLAG: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
#Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
}
val params = WindowManager.LayoutParams(
overlayedWidth,
overlayedHeight,
LAYOUT_FLAG,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT
)
params.gravity = Gravity.RIGHT
params.x = 0
params.y = 0
if (overlayedButton!!.parent == null)
{
wm!!.addView(overlayedButton, params)
}
else
{
wm!!.updateViewLayout(overlayedButton, params)
}
overlayedButton!!.setOnTouchListener(this)
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//...
}
MotionEvent.ACTION_MOVE -> {
//...
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
//...
}
//...
}
Maybe take a look at setSystemGestureExclusionRects. Also this article explains what is going on with the OS intercepting touches for high-level gestures.
I add UI object Button and add the C# script with public function.
To button I add component Event Trigger, do events(Pointer Click and Pointer Down) and redirect to my function public void onClick()
On PC code works, but when I upload game to android and touch the object, code not works.
How to do onTouch event?
I think OnMouseDown will check every frame if there is a mouse input it's like update so you have to cheek touch in Update & with touch you will have more control like Touch Phase to detect if the touch begins or lifted or moved etc...
you need to check
if(input.touchCount > 0)
void Update() {
if (Input.touchCount > 0){
print("exist a touch");
if(Input.GetTouch(0).phase == TouchPhase.Began){
print("Touch begans");
}
if(Input.GetTouch(0).phase == TouchPhase.Ended){
print("Touch Ended");
}
}
}
chCount > 0)& inside this you can cheek for touch Phase
Touches aren't Clicks
In order to handle touch input you need to check Input.touchCount and then query each touch with Input.GetTouch. Note that each Touch has an an ID that will be unique per finger and consistent across frames.
There are no easy OnClick like methods for touches, as touches can be a lot more complex (tap, long tap, drag, etc), so you will have to check inside Update() and handle the conversion from touch data to mouse analogs yourself.
I develop a game that need user to have a single touch to draw over to link multiple objects. Currently using mouse button as touch input with this code:
if(Input.GetButton("Fire1"))
{
mouseIsDown = true;
...//other codes
}
It works fine with mouse but it will give me problem when I compile it for multi touch device. In multitouch device, if you have 2 fingers down, the middle point will be taken as the input. But if I already touch an object then with second finger down to screen, it will mess up my game input and give me havoc design.
What I want is to limit it to 1 touch only. If the second finger come touching, ignore it. How can I achieve that?
Below is all you need:
if ((Input.touchCount == 1) && (Input.GetTouch (0).phase == TouchPhase.Began)) {
mouseIsDown = true;
...//other codes
}
It will only fire when one finger is on the screen because of Input.touchCount == 1
You are using Input.GetButton. A button is not a touch. You probably want to look at Input.GetTouch
If you need to support multiple input-type devices, you may want to consider scripting your own manager to abstract this out somewhat.
I would roll with a bit of polling!
so in your Update() I typically do something like this:
foreach (Touch touch in Input.touches)
{
// Get the touch id of the current touch if you're doing multi-touch.
int pointerID = touch.fingerId;
if (touch.phase == TouchPhase.Began)
{
// Do something off of a touch.
}
}
If you're looking for more info check this out:
TouchPhases in Unity