I am using the Drag & Drop API's introduced in 3.0 - however, I only want the drag shadow to follow the user's finger around while they are inside one particular area of the screen. If they drift outside that, I would like the shadow to go away and the drag event to end. I have already done some searching and seen that the API does not support updating the drag shadow after creation, which was my first plan, but is there any way to stop the DragEvent without the user actually lifting their finger?
There are two API calls for API 24+ that should be useful:
View#cancelDragAndDrop
Cancels an ongoing drag and drop operation.
A DragEvent object with DragEvent.getAction() value of DragEvent.ACTION_DRAG_ENDED and DragEvent.getResult() value of false will be sent to every View that received DragEvent.ACTION_DRAG_STARTED even if they are not currently visible.
This method can be called on any View in the same window as the View on which startDragAndDrop was called.
and View#updateDragShadow
Updates the drag shadow for the ongoing drag and drop operation.
Which you use and how you use them will depend upon your intentions. If you simply need the drag shadow to disappear when it is moved outside a defined area without ending the drag and drop operation so that the drag shadow will appear again if the user moves it back into the area of interest, updateDragShadow should suffice. If you want to cancel the drag and drop operation completely, use cancelDragAndDrop.
The question doesn't define what "inside one particular area of the screen" means. I assume that it is part of a view. In the drag listener, you can use the drag view coordinates to determine if the drag view is inside or outside the area. The drag view coordinates can be found in DragEvent. Another option would be to define an empty view that covers the area of the screen and take action when the drag view exits that view. (You will need to set a drag and drop listener on that empty view as well.)
The above does not apply to API 23 and below and the solution is not immediately obvious for these lower APIs. The drag shadow has an internal representation that is not accessible through the API as far as I can tell, although you might be able to do something with Reflection. View.java has internal information about the drag and drop operation.
The question is old but since there is no answer; I want to bring attention that this is now the default behaviour according to the official documentation and API here
This behavior can be achieved using the following quoted code:
val imageView = ImageView(this)
// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->
// Handles each of the expected events.
when (e.action) {
DragEvent.ACTION_DRAG_STARTED -> {
// Determines if this View can accept the dragged data.
if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
// As an example of what your application might do, applies a blue color tint
// to the View to indicate that it can accept data.
(v as? ImageView)?.setColorFilter(Color.BLUE)
// Invalidate the view to force a redraw in the new tint.
v.invalidate()
// Returns true to indicate that the View can accept the dragged data.
true
} else {
// Returns false to indicate that, during the current drag and drop operation,
// this View will not receive events again until ACTION_DRAG_ENDED is sent.
false
}
}
DragEvent.ACTION_DRAG_ENTERED -> {
// Applies a green tint to the View.
(v as? ImageView)?.setColorFilter(Color.GREEN)
// Invalidates the view to force a redraw in the new tint.
v.invalidate()
// Returns true; the value is ignored.
true
}
DragEvent.ACTION_DRAG_LOCATION ->
// Ignore the event.
true
DragEvent.ACTION_DRAG_EXITED -> {
// Resets the color tint to blue.
(v as? ImageView)?.setColorFilter(Color.BLUE)
// Invalidates the view to force a redraw in the new tint.
v.invalidate()
// Returns true; the value is ignored.
true
}
DragEvent.ACTION_DROP -> {
// Gets the item containing the dragged data.
val item: ClipData.Item = e.clipData.getItemAt(0)
// Gets the text data from the item.
val dragData = item.text
// Displays a message containing the dragged data.
Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()
// Turns off any color tints.
(v as? ImageView)?.clearColorFilter()
// Invalidates the view to force a redraw.
v.invalidate()
// Returns true. DragEvent.getResult() will return true.
true
}
DragEvent.ACTION_DRAG_ENDED -> {
// Turns off any color tinting.
(v as? ImageView)?.clearColorFilter()
// Invalidates the view to force a redraw.
v.invalidate()
// Does a getResult(), and displays what happened.
when(e.result) {
true ->
Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
else ->
Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
}.show()
// Returns true; the value is ignored.
true
}
else -> {
// An unknown action type was received.
Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
false
}
}
}
Related
I have a button to trigger some action in MainActivity and I want to draw animation as follows: when user holds the button (ACTION_DOWN) it scales down, when the button is released (ACTION_UP or ACTION_CANCEL) it scales up and go back to its normal state.
This works great when user holds the button for some time, but for a normal click, ACTION_UP never gets called even though ACTION_DOWN does.
I searched in the documentation and checked almost every answer on stackoverflow but didn't find anything useful.
Here's what the documentation says: "While the framework tries to deliver consistent streams of motion events to views, it cannot guarantee it. Some events may be dropped or modified by containing views in the application before they are delivered thereby making the stream of events inconsistent. Views should always be prepared to handle ACTION_CANCEL and should tolerate anomalous situations such as receiving a new ACTION_DOWN without first having received an ACTION_UP for the prior gesture."
And here's my code:
button.setOnTouchListener { view, event ->
when(event?.action){
//behaves as expected
MotionEvent.ACTION_DOWN -> {
scaleDown(view)
}
//the problem here: ACTION_UP gets called only when user holds the button for some time
//and not for a quick/ normal click.
MotionEvent.ACTION_UP -> {
scaleUp(view)
doSomething()
}
//behaves as expected
MotionEvent.ACTION_CANCEL -> {
scaleUp(view)
}
}
true
}
I have two questions, First: why ACTION_UP never gets called for a quick/normal click?
Second: how to differentiate between normal click and when user holds the button for some time if i want to react in two different ways?
Question#1
I tried the below snippet and it was working as expected with long, normal, and quick clicks
button.setOnTouchListener { view, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
Log.d("TESTOOO", "DOWN")
} else if (event.action == MotionEvent.ACTION_UP) {
Log.d("TESTOOO", "UP")
}
true
}
Question#2
Can you try setOnLongClickListener(), I tried it and worked fine
Answering my own question:
For the first question, the problem was with the animation not the OnTouchListener.
For the second question, to differentiate between a normal click and a long click, i had to calculate how many milliseconds have passed since ACTION_DOWN was triggered when ACTION_UP or ACTION_CANCEL is triggered.
If the difference between the two events is less than 300 milliseconds it's considered a normal click, else it's a long click.
button.setOnTouchListener { view, event ->
val now = System.currentTimeMillis()
when(event?.action){
MotionEvent.ACTION_DOWN -> {
scaleDown(view)
}
MotionEvent.ACTION_UP -> {
scaleUp(view)
if(now - event.downTime < 300){
//normal click
}else{
//long click
}
}
MotionEvent.ACTION_CANCEL ->
scaleUp(view)
}
}
true
}
Samsung phones have this feature where you can hold your finger on an item then drag it down, i can multi select items quickly like in the gif
Is there any way i can implement this feature in my apps even if it's just for Samsung devices?
Example image
even if it's just for samsung devices
You can implement this feature on any device. I first found this feature in Mi devices.
It is somewhat complicated. But you can use libraries for this purpose like DragSelectRecyclerView.
Add following code to your build.gradle:
dependencies {
implementation 'com.github.MFlisar:DragSelectRecyclerView:0.3'
}
In your java file, add following code:
Create a touch listener.
mDragSelectTouchListener = new DragSelectTouchListener()
.withSelectListener(onDragSelectionListener)
// following is all optional
.withMaxScrollDistance(distance) // default: 16; defines the speed of the auto scrolling
.withTopOffset(toolbarHeight) // default: 0; set an offset for the touch region on top of the RecyclerView
.withBottomOffset(toolbarHeight) // default: 0; set an offset for the touch region on bottom of the RecyclerView
.withScrollAboveTopRegion(enabled) // default: true; enable auto scrolling, even if the finger is moved above the top region
.withScrollBelowTopRegion(enabled) // default: true; enable auto scrolling, even if the finger is moved below the top region
.withDebug(enabled);
Attach this touch listener to the RecyclerView.
recyclerView.addOnItemTouchListener(mDragSelectTouchListener);
On item long press, inform the listener to start the drag selection.
mDragSelectTouchListener.startDragSelection(position);
Use the DragSelectionProcessor, it implements the above mentioned interface and can be set up with 4 modes:
Simple: simply selects each item you go by and unselects on move back
ToggleAndUndo: toggles each items original state, reverts to the original state on move back
FirstItemDependent: toggles the first item and applies the same state to each item you go by and applies inverted state on move back
FirstItemDependentToggleAndUndo: toggles the item and applies the same state to each item you go by and reverts to the original state on move back
Provide a ISelectionHandler in it's constructor and implement these functions:
onDragSelectionListener = new DragSelectionProcessor(new DragSelectionProcessor.ISelectionHandler() {
#Override
public Set<Integer> getSelection() {
// return a set of all currently selected indizes
return selection;
}
#Override
public boolean isSelected(int index) {
// return the current selection state of the index
return selected;
}
#Override
public void updateSelection(int start, int end, boolean isSelected, boolean calledFromOnStart) {
// update your selection
// range is inclusive start/end positions
// and the processor has already converted all events according to it'smode
}
})
// pass in one of the 4 modes, simple mode is selected by default otherwise
.withMode(DragSelectionProcessor.Mode.FirstItemDependentToggleAndUndo);
You can see a demo activity here.
Is there any other library for the same purpose?
Yes, you can also use Drag Select Recycler View by afollestad. And you can see practical implementation for afollestad's Drag select Recycler View here.
I want to play animation when user clicks or swipes. But can we handle both behaviours in MotionLayout? They work separately perfectly, but if I add OnClick and OnSwipe in the same scene, only OnClick works. Is there any workarounds?
Yes, you can work around this by removing the onClick behavior from your MotionScene and implement the click yourself by extending the MotionLayout and overriding dispatchTouchEvent.
In this function, you decide if the touch event is a click inside the 'target area' where you want the onClick behavior to occur.
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (touchEventInsideTargetView(clickableArea, ev)) {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
startX = ev.x
startY = ev.y
}
MotionEvent.ACTION_UP -> {
val endX = ev.x
val endY = ev.y
if (isAClick(startX!!, endX, startY!!, endY)) {
if (doClickTransition()) {
return true
}
}
}
}
}
return super.dispatchTouchEvent(ev)
}
A post on this and a full code example can be found here:
https://medium.com/vrt-digital-studio/picture-in-picture-video-overlay-with-motionlayout-a9404663b9e7
Sure, just handle the onTouch event in your parent class.
Then decide what you are doing with it and what to send it to.
onTouch(touchEvent){
motionLayout.doClickIfYouWant(touchEvent)
motionLayout.doSwipeIfYouWant(touchEvent)
}
Pseudo code, but you can send it to both if you catch it first and decide who gets it. Also don't forget to return handled boolean flag from onTouch callback to ensure it doesn't get handled twice. Lastly, if other items are touchable on the screen you may have to check the x,y of your touch to determine if it should go to the motionLayout or just return "not handled" and let the native behavior pass the touch on through.
Hope that helps. Also if the motionLayout class doesn't expose the methods you need to touch to send the correct touch event to more then one place, you can still get it using reflection. Of course reflection can be a little slower, so use with caution, but I've had to do it before for a layout class that had children images that I needed to move around, and controlled the touch at a parent level to decide if the image gets it or not, but it was not publicly available, so I looked at the code and touched it via reflection with no issues. Treat that as a last resort though, and maybe everything you need is exposed.
Also be aware some engineer despise reflection lol, so code with caution.
I am implementing google maps in my application. My requirement is to display custom infoWindow when click on marker.
My custom infoWindow contains a listview and i need to perform some operation based on clicked item.
i am getting click event of whole infoWindow (using OnInfoWindowClickListener) but the problem is, i am not getting click event for list view inside infoWindow.
As per description here
Note: The info window that is drawn is not a live view. The view is rendered as an image (using View.draw(Canvas)) at the time it is returned. This means that any subsequent changes to the view will not be reflected by the info window on the map. To update the info window later (for example, after an image has loaded), call showInfoWindow(). Furthermore, the info window will not respect any of the interactivity typical for a normal view such as touch or gesture events. However you can listen to a generic click event on the whole info window as described in the section below.
So is there any way by which i can get click event of each clicked item inside custom infoWindow ?
Screenshot :
The best approach is probably by wrapping the map in layout. Where you will set listener to touch evetns and then propagate the events to saved instance of infowindows View.
See answer: https://stackoverflow.com/a/15040761/838424
This works for buttons, but from unknown reason it does not works for ListView (but clicks into ListView are propagated).
Update:
The clicks are not propagated to items because of function isAttachedToWindow returns false (in AbsListView.onTouchEvent). Which blocks processing the click event.
Workaround is to use code in wrapper on dispatchTouchEvent
int pos = myListView.pointToPosition((int)copyEv.getX(), (int)copyEv.getY());
my complete workaround:
if (copyEv.getX() >= 0 && copyEv.getY() >= 0
&& copyEv.getX() < infoWindow.getWidth()
&& copyEv.getY() < infoWindow.getHeight())
{
// event is inside infoWindow
final int actionMasked = ev.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
int pos = vItems.pointToPosition((int)copyEv.getX(), (int)copyEv.getY());
if (pos >= 0) {
vItems.performItemClick(infoWindow, pos, 0);
ret = true;
}
break;
case MotionEvent.ACTION_UP: {
break;
}
}
}
I have one activity, which contains header (title bar). This header also contains the image of Sync feature of my app.
As per requirement of client, I want that when the user tap on this button, the animation of this image (button) starts, which is rotating animation (I've achieved this animation). But while this animation is being performed, I want to freeze the whole screen so the user can not tap on other views while sync is in progress.
Till now I've used the following method but with no success :
private boolean stopUserInteractions = false;
public boolean dispatchTouchEvent(int i) {
View v = imgSync;
if (v.getId() == R.list.imgSync) {
System.out.println("UI Blocked...");
return false;
}
else {
return super.dispatchTouchEvent(ev);
}
return false;
}
Can anyone guide me for this? Any trick/snippet or any other hint.
EDIT : I dont want to use ProgressDialog while this sync process is being happening.
Have an OnTouchListener class within your activity, or let your Activity implement OnTouchListener. All children of the activity's layout should set a single object of this if they want to listen touch events. Within listener, check whether you should process the click or not.