I have a ListView mRecipeListView that contains a number of items.
Inside the listViewAdapter, when it generates views for the items to display, I add an onLongClickListener that starts a drag operation:
view.setOnLongClickListener(new View.OnLongClickListener() {
ClipData data = ClipData.newPlainText("", "");
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(data, shadowBuilder, view, 0);
// Highlight dragged view.
view.setBackgroundColor(getResources().getColor(R.color.app_color_accent));
view.invalidate();
}
The same view also gets an onDragListener so that each view is a valid drop target. The goal is of course to enable re-ordering of the views by drag and drop:
view.setOnDragListener(new View.OnDragListener() {
#Override
public boolean onDrag(View view, DragEvent dragEvent) {
switch (dragEvent.getAction()) {
// ...
// re-ordering works just fine
// ...
case DragEvent.ACTION_DRAG_LOCATION:
// y-coord of touch event inside the touched view (so between 0 and view.getBottom())
float y = dragEvent.getY();
// y-coord of top border of the view inside the current viewport.
int viewTop = view.getTop();
// y-coord that is still visible of the list view.
int listBottom = mRecipeListView.getBottom(),
listTop = mRecipeListView.getTop();
// actual touch position on screen > bottom with 200px margin
if (y + viewTop > listBottom - 200) {
mRecipeListView.smoothScrollBy(100, 25);
View child = mRecipeListView.getChildAt(mRecipeListView.getChildCount() - 1);
child.setVisibility(View.GONE);
child.setVisibility(View.VISIBLE);
} else if (y + viewTop < listTop + 200) {
mRecipeListView.smoothScrollBy(-100, 25);
View child = mRecipeListView.getChildAt(0);
child.setVisibility(View.GONE);
child.setVisibility(View.VISIBLE);
}
return true;
}
}
So, in the case of ACTION_DROP_LOCATION, which fires if the drag is continued inside the bounding box of a valid drop target view, I check whether the drag is so close to the bottom or top that we need to scroll.
This works, but it has flaws:
Views are recycled when you scroll, or equivalently, not all views are always present. When the drag is started, only the currently visible views are asked whether they want to accept drops. As the list is scrolled down, new views are added which were not asked, and they are not valid drop targets. The workaround for this are these lines:
View child = mRecipeListView.getChildAt(mRecipeListView.getChildCount() - 1);
child.setVisibility(View.GONE);
child.setVisibility(View.VISIBLE);
for scrolling down. This means that the last child of the listView is acquired, its visibility changed to GONE and back, which forces it to register as a valid drop target. Analog for scrolling up, but get the child at index 0 instead of the last one.
Another problem arises for this for which I could not find a solution yet:
1) I highlight the item that the user is dragging by changing its background color. When the user now scrolls down, that item's view is discarded. When the user scrolls back up, the view is recycled, but it is now a different view than before. The background is changed back to the default, and keeping a reference to the view does not work either, as an equality check will fail because the recycled view is not the same as the original view. So I don't know how to keep the background changed for this item's view.
2) Scrolling is bound to events that fire when the user is dragging inside the bounding box of a valid drop target. This forced me to keep the view visible that the user is dragging, because I need that view's bounding box to start the scrolling event when it was, for example, the top visible view. I would rather turn it invisible, but for that I would have to do scrolling outside of the drag event constraint.
3) For the same reason: When the user keeps the finger at the bottom of the list and there is room to scroll it will only keep scrolling if the finger keeps moving, as otherwise no new event is fired.
I tried to react to onTouchEvents but it seemed they are not handled during a drag operation.
Related
I have some views which I like to drag around. The views are within
a LinearLayout, which itself is within a scrollview.
I want to get the position of the current finger (touch), to
make a smooth scroll on my scrollview, depending of the
height of the current drag.
I start my draggin after a long click with the
View build-in listener startDrag
view.startDrag(null, shadowBuilder, view, 0);
I am also able to get the relativ position of the drag
on the view that is currently hovered by
view.setOnDragListener(new OnDragListener() {
#Override
public boolean onDrag(View v, DragEvent event) {
//get event positions
}
});
But this only works for the view where the drag shadow is currently on
and DragEvent only provides relative positions, not raw positions.
What I need is the position of the finger, while the drag happens. Unfortunately all onTouchEvents get consumed while dragging.
Does anybody know how I get this to work?
Ps: One way that works (kind of) which I currently use, is to calculate the
position of the touch via the dragEvent.relativePosition in combination
with the view position. But isn't there a better way to go?
OK, since there seems not to be an easier answer, I will provide my own relativly easy solution for further readers, with no need of gesture detection.
At first you need the view that receives the drag event and at second the
drag event ifself (or at least the x and y coordinates).
With fetching of the view position and some simple addition you get the
raw touch position.
This method is tested with the show pointer position developer options
to provide the correct data.
The method for the calculation looks like this:
/**
* #param item the view that received the drag event
* #param event the event from {#link android.view.View.OnDragListener#onDrag(View, DragEvent)}
* #return the coordinates of the touch on x and y axis relative to the screen
*/
public static Point getTouchPositionFromDragEvent(View item, DragEvent event) {
Rect rItem = new Rect();
item.getGlobalVisibleRect(rItem);
return new Point(rItem.left + Math.round(event.getX()), rItem.top + Math.round(event.getY()));
}
Call this method inside of your onDragListener implementation:
#Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
//etc etc. do some stuff with the drag event
break;
case DragEvent.ACTION_DRAG_LOCATION:
Point touchPosition = getTouchPositionFromDragEvent(v, event);
//do something with the position (a scroll i.e);
break;
default:
}
return true;
}
Addtional:
If you want to determinate if the touch is inside of a specific view, you can
do something like this:
public static boolean isTouchInsideOfView(View view, Point touchPosition) {
Rect rScroll = new Rect();
view.getGlobalVisibleRect(rScroll);
return isTouchInsideOfRect(touchPosition, rScroll);
}
public static boolean isTouchInsideOfRect(Point touchPosition, Rect rScroll) {
return touchPosition.x > rScroll.left && touchPosition.x < rScroll.right //within x axis / width
&& touchPosition.y > rScroll.top && touchPosition.y < rScroll.bottom; //withing y axis / height
}
It is also possible to implement a smooth scroll on a ListView based on this solution.
So that the user can drag an item out of a list, and scroll the list via dragging the item either to the top or bottom of the list view.
Cheers.
I have set up a GridView that has TextViews, and I would like to change the background of the text view when it is touch, then upon release reset the background. The grid view already has OnItemClick and OnItemLongClick listeners. I have implemented the background swap for the touched text view by setting a OnTouch listener for the GridView, then I get the position of the touch and use getChildAt to get the text view that was touched. This works really well, until I scroll the grid view. Because the text views are reused, getChildAt is returning the wrong text view, so I'm changing the background of the wrong text view in that situation.
How can I always get the correct text view, or is there a way to set an OnTouch listener for each text view instead of trying to get a text view after touching the grid view?
gridView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
float currentXPosition = event.getX();
float currentYPosition = event.getY();
int position = gridView.pointToPosition((int) currentXPosition, (int) currentYPosition);
if (gridView.getChildAt(position) != null) {
TextView txtView = (TextView)gridView.getChildAt(position);
//might not be the right text view!
...
You can use getFirstVisiblePosition to retrieve the offset for the first child
int firstPosition = gridView.getFirstVisiblePosition();
int childPosition = position - firstPosition;
TextView txtView = (TextView)gridView.getChildAt(childPosition);
This should give you the correct position. Of course, you should check whether childPosition is actually a valid position (i.e. not <0 or >=gridView.getChildCount())
I suggest you should take a look at Android's list selectors. You don't even need listeners for this task. List selectors make customizing touch feedback a piece of cake ;)
There are lots of good answers and questions about list selectors on Stack Overflow, for example: Custom selector for list background
But if you really are going to ignore my advice...
Then I suggest you to check the MotionEvent for being of the type ACTION_DOWN or ACTION_UP instead of processing every little bit of touch input.
Example code:
int action = event.getActionMasked();
if(action == MotionEvent.ACTION_DOWN){
//get child and change background to pressed state
} else if(action == MotionEvent.ACTION_UP){
//get child and change background to normal state
}
Is it possible to create a list view in android that is not fixed to a particular position and user can move the list view on a gesture some what floating listview that can be moved anywhere on the screen?I tried finding it but could not find some link.Does any one have some idea about it?
You can achieve this by setting an OnTouchListener on the list view and overriding it's onTouch() method. In the onTouch() method you will have to handle the touch events like(ACTION_DOWN, ACTION_MOVE). Here is how you can handle touch event:
#Override
public boolean onTouch (View v, MotionEvent event)
{
switch(event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
start.set(event.getX(), event.getY()); //saving the initial x and y position
mode = DRAG;
break;
case MotionEvent.ACTION_MOVE:
if(mode == DRAG) {
float scrollByX = event.getX() - start.x; //computing the scroll in X
float scrollByY = event.getY() - start.y; //computing the scroll in Y
v.scrollBy((int)scrollByX/20, 0);
}
break;
}
return false;
}
When the user places the finger down on the screen the ACTION_DOWN event is triggered and when the user drags his finger on the screen ACTION_MOVE event is triggered. Hence a drag event is a combination of ACTION_DOWN and ACTION_MOVE.
Once you have the scrollX and scrollY value the you can move the view on the screen by passing these values to the scrollBy() method of the View class.
Here in the code there are two things to notice:
first is that I have passed 0 as the value of scrollY as it will prevent the list view to be dragged and scrolled simultaneously.
second is that the return value is false as you want the android to handle the scrolling of the list items
Here you can find the complete code for this
I would move the listview using the margins from the layout.
Outline:
Override OnTouch, and based on the event.getX() and event.getY(), update the margins of the listview as follows:
if the parent is a relative layout:
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)mListView.getLayoutParams();
lp.leftMargin = event.getX();
lp.topMargin = event.getY();
mListView.setLayoutParams(lp);
Add the dispatchTouchEvent to your activity so that events from the listview get passed up to onTouch:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
Hope this helps.
Although it's hard to maintain, AbsoluteLayout lets you specify a position in (x,y) coordinates. You would just declare your ListView inside a AbsoluteLayout, get a handle to it in java code where you intercept the clicks and then set new coordinates.
In the Android framework, the word "floating" is usually synonymous with new Window (such as a Dialog). It depends somewhat on what your end goal is, but if you want to create a ListView that floats over the rest of the content in your hierarchy, that you can easily hide and show, you should probably consider wrapping that view inside a PopupWindow, which has methods to allow you to easily show the window with showAsDropdown() and shotAtLocation() and also move it around while visible with update().
This is how the newer versions of the SDK create pop-up lists for Spinner and Menu
I am trying to make horizontal scroll view for grid view. I am succeeded in with horizontal scroll view. But now i need to implement onItemClickListener too for the same grid view. I used GestureDetector.SimpleOnGestureListener and onTouchEvent for horizontal scroll.If i use this onItemClickListener is not working. Con somebody help me to get both working. Thanks in advance.
I recently had to deal with the same issue and this is how I solved it.
Firstly, I override the default onListItemClick listener inside the ListFragment/ListActivity. Then in the onTouchEvent method, I set up a condition that when true, makes a call to the ListView's onListItemClick. This call is performed when the action is equal to MotionEvent.ACTION_UP and the condition was met. You must first decide what group of events constitute a click for your view. I declared mine as an ACTION_DOWN immediately followed by an ACTION_UP. When you are ready to perform the onListItemClick, your must pass the actual item's view, position and id to the method.
See my example below.
Class....{
private boolean clickDetected = true;
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final int x = (int) ev.getX();
final int y = (int) ev.getY();
//if an action other than ACTION_UP and ACTION_DOWN was performed
//then we are no longer in a simple item click event group
//(i.e DOWN followed immediately by UP)
if (action != MotionEvent.ACTION_UP
&& action != MotionEvent.ACTION_DOWN)
clickDetected = false;
if (action == MotionEvent.ACTION_UP){
//check if the onItemClick requirement was met
if (clickDetected){
//get the item and necessary data to perform onItemClick
//subtract the first visible item's position from the current item's position
//to compensate for the list been scrolled since pointToPosition does not consider it
int position = pointToPosition(x,y) - getFirstVisiblePosition();
View view = getChildAt(position);
if (view != null){//only continue if a valid view exists
long id = view.getId();
performItemClick(view, position, id);//pass control back to fragment/activity
}//end if
}//end if
clickDetected= true;//set this to true to refresh for next event
}//end if
return super.onTouchEvent(ev);
....
}//end onTouchEvent
}//end class
This setup allow for much flexibility, for example if you want to set up a long click you can do the same as above and simple check for the difference in time between the ACTION_DOWN and the ACTION_UP.
Another approach is to get the position of the item that initiated the action down and check it against the one for the action up and even throw in a time differential criteria for the down and up action (say less than 1 second).
I have a view in a linearlayout. When the view is longpressed the view will be removed from this linearlayout and placed, on the same position of the screen, to a relative layout. On this way a can move the view over the screen with my finger.
it almost works:
i got the longpress event working (remove the view and place the view to the relativelayout). after that i add an ontoucheventlistener so my view stays with my finger, but only for a second. the last time the touchevent is fired i got "MotionEvent.ACTION_CANCEL". When i remove my finger and place my finger again to the view i can go feature with my movement, then it will keep until i remove my finger.
I think that my problem is that the view it removed for a short moment, at that time i get a "MotionEvent.ACTION_CANCEL", however, there are still some unhandled events, they will be fired first. Thats why i got for about 1 second still ontouchevents. (this is just a thought).
someone a idee how i can keep the ontouchevent, or let the ontouchevent fired without replacing my finger?
Edited
My thought is not correct. When i do a longpress the view stays with my finger, however i lost the view as soon as i move about 50 to 100 pixels to any direction.
Edited 2
the longpress code of the view inside the linearlayout
view.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
_linearLayout.removeView(v);
moveView(v);
return true;
}
});
moveView will be called by the longpress
private void moveView(View v) {
_relativeLayout.addView(v);
v.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getRawX();
int y = (int) event.getRawY();
v.layout(x, y, x + v.getWidth(), y + v.getHeight());
break;
case MotionEvent.ACTION_UP:
_relativeLayout.removeView(v);
v = null;
break;
case MotionEvent.ACTION_CANCEL:
//it comes here when i move my finger more then 100 pixels
break;
}
return true;
}
});
}
of corse, this is the relevant part of the code and not the original code
Have a look at the Experience - Android Drag and Drop List post. There is also a source code that does something very similar to what are you trying to get. Eric, the author of that post, uses a separate temporary ImageView to hold a dragged view image, taken with getDrawingCache(). He also hides the original list item by setVisibility(View.INVISIBLE).
In the DragNDrop project (link above) you can replace drag(0,y) calls by drag(x,y) in DragNDrop.java to see a dragging in all directions.
Just throwing some possibilities in because I can't see the code but MotionEvent.ACTION_CANCEL could be being called as the parent layout is switched and the view is getting redrawn.
You could try overriding the onTouchEvent and at the point the ACTION_CANCEL event is sent try get hold of an equivalent view in the new layout and operate on that but I still think the better idea is to redesign so it all takes place in the same layout.