I had extended recyclerview so i can scale it. It works the way I wanted it but now I wanted to programmatically scroll by x amount of pixels. The user story for this is that, if I tap on the upper half part of the screen, it should scroll by x amount of pixel, same goes to the lower half part of the screen.
Here's what I did:
Activity:
#Override
public boolean dispatchTouchEvent(MotionEvent event)
{
// if(mComicViewerFragment != null)
// mComicViewerFragment.consumeEvent(event);
mScaleDetector.onTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_DOWN)
{
int y = (int) event.getY();
if(y >= mScreenSize.y / 2)
{
mComicViewerFragment.scrollBy((int)(mScreenSize.y * 0.10f));
Log.i(ComicApplication.TAG, "ComicViewerActivity.onTouchEvent : You are tapping the lower part : " + y);
}
else
{
mComicViewerFragment.scrollBy((int)(-mScreenSize.y * 0.10f));
Log.i(ComicApplication.TAG, "ComicViewerActivity.onTouchEvent : You are tapping the upper part : " + y);
}
}
return super.dispatchTouchEvent(event);
}
Fragment
public void scrollBy(int by)
{
mScalingRecyclerView.smoothScrollBy(0, by);
}
Every time I tap, it doesn't scroll by x amount of pixels. Is this because I created a custom view extending from RecyclerView? Where should I properly call smoothScrollBy? This is suppose to be easy but I am stuck.
As recyclerView.smoothScrollBy(0, pixels); is not working for your custom view you can try an alternative way.
What you can do is scroll by position by doing some Math but it wont be exact to the pixel.
Hypothetically speaking if your Items are of equal height of 100dp you can convert dp to pixels for any screen type by code see here
Lets say 100dp comes to 100px per item and you want to scroll 400px down the Recycler. That's 4 positions (400 / 100).
All you need then is the current in View bottom item position number, add 4 and scroll or smooth scroll by position.
Here is a helper class to show you how to get the Top or Bottom item position in View if you wish to go both up or down the Recycler.
For a complete Solution Check here
Related
How can I set any view to the middle of my touch position using onTouchEvent?
Red Circle Is My Click Position
Black Circle Is The View Like Button, ImageView, And So On...
Can someone leave an example?
Edit
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
imgCircle.setX(event.getX() - imgCircle.getWidth() / 2);
imgCircle.setY(event.getY() - imgCircle.getHeight() / 2);
}
return false;
}
Example of a solution:
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
yourView.setX(x);
yourView.setY(y);
return false;
}
You get the coordinates from click and set them to the view. If you have multiple views inside of one you might have to get relative position of the parent view and then subtract the clicked position from parent position.
If you want to set the view position to the centre just subract half of view width from x and half of height from y.
EDIT
How to set position to centre of view:
yourView.setX(x-yourView.getWidth()/2);
yourView.setY(y-yourView.getHeight()/2);
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 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.
Scrolling screens and animations are bad things on e-ink screens. Their fast screen refreshes make the screen wildly flicker (first going negative, then black, then new content, repeat many times for scrolling).
One simple way to tame an e-ink screen is to change scrolling to paging. Another method would be to still do scrolling - but without showing intermediate screen updates:
Screen before scroll -> lifting the finger: update to screen after scroll
Screen before scroll -> leaving the finger + timeout: update to actual screen
Any ideas how to implement this?
Is there a way to teach/overwrite existing scroll code?
Thx
#Override
public boolean dispatchTouchEvent(MotionEvent event)
{
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
hit_scrollx = view.getScrollX();
hit_scrolly = view.getScrollY();
hit_touchx = (int) event.getX();
hit_touchy = (int) event.getY();
break;
case MotionEvent.ACTION_UP:
int x = (int) event.getX();
int y = (int) event.getY();
if (x<hit_touchx-5 || x>hit_touchx+5 || y<hit_touchy-5 || y>hit_touchy+5)
scrollAbsolute(hit_scrollx+hit_touchx-x, hit_scrolly+hit_touchy-y);
break;
}
return(true);
}
This presumes that you have your own scrollAbsolute which does clipping for your application and uses View.scrollTo. This is designed for an activity with a main scrolling view. It captures all touch events. The "5" is to prevent unnecessary refresh on insignificant touches.
Desired effect
I have a bunch of small images that I'd like to show on a "wall" and then let the user fling this wall in any direction and select an image.
Initial Idea
as a possible implementation I was thinking a GridView that is larger than the screen can show - but all examples of using this widget indicate that the Gallery doesn't extend beyond the size of the screen.
Question
What is the best widget to use to implement the desired effect ?
A code sample would be especially beneficial.
EDIT...
if someone has example code that will let me put about 30 images on a "wall" (table would be good) then I will accept that as the answer. Note that the "wall" should look like it extends beyond the edges of the display and allow a user to use the finger to drag the "wall" up down left right. Dragging should be in "free-form" mode. A single tap on an image selects it and a callback shall be detectable. I have created a bounty for this solution.
The solution is actually quite simple, but a pain. I had to implement exactly this within a program I recently did. There are a few tricksy things you have to get around though. It must be noted that different OS versions have different experiences. Certain expertise or cludging around is required to make this work as drawing is sometimes adversely affected by this method. While I have a working copy, the code I provide is not for everyone, and is subject to whatever other changes or customizations have been made in your code.
set android:layout_width to wrap_content
set android:layout_height to wrap_content
In your code:
determine how many rows you will have.
divide number of items by the number of rows.
add gridView.setStretchMode(NO_STRETCH);
add gridView.setNumColumns( number of Columns );
add gridView.setColumnWidth( an explicit width in pixels );
To Scroll:
You may simply use gridView.scrollBy() in your onTouchEvent()
These are the steps. All are required in order to get it to work. The key is the NO_STRETCH with an explicit number of columns at a specific width. Further details can be provided, if you need clarification. You may even use a VelocityTracker or onFling to handle flinging. I have snappingToPage enabled in mine. Using this solution, there is not even a requirement to override onDraw() or onLayout(). Those three lines of code get rid of the need for a WebView or any other embedding or nesting.
Bi-directional scrolling is also quite easy to implement. Here is a simple code solution:
First, in your class begin tracking X and Y positions by making two class members. Also add State tracking;
// Holds the last position of the touch event (including movement)
int myLastX;
int myLastY;
// Tracks the state of the Touch handling
final static private int TOUCH_STATE_REST = 0;
final static private int TOUCH_STATE_SCROLLING = 1;
int myState = TOUCH_STATE_REST;
Second, make sure to check for Scrolling, that way you can still click or longclick the images themselves.
#Override public boolean onInterceptTouchEvent(final MotionEvent ev)
{//User is already scrolling something. If we haven't interrupted this already,
// then something else is handling its own scrolling and we should let this be.
// Once we return TRUE, this event no longer fires and instead all goes to
// onTouch() until the next TouchEvent begins (often beginning with ACTION_DOWN).
if ((ev.getAction() == MotionEvent.ACTION_MOVE)
&& (myState != TOUCH_STATE_REST))
return false;
// Grab the X and Y positions of the MotionEvent
final float _x = ev.getX();
final float _y = ev.getY();
switch (ev.getAction())
{ case MotionEvent.ACTION_MOVE:
final int _diffX = (int) Math.abs(_x - myLastX);
final int _diffY = (int) Math.abs(_y - myLastY);
final boolean xMoved = _diffX > 0;
final boolean yMoved = _diffY > 0;
if (xMoved || yMoved)
myState = TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
myLastX = _x;
myLastY = _y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (myState == TOUCH_STATE_SCROLLING)
// Release the drag
myState = TOUCH_STATE_REST;
}
//If we are not At Rest, start handling in our own onTouch()
return myState != TOUCH_STATE_REST;
}
After the GridView knows that you are Scrolling, it will send all Touch Events to onTouch. Do your Scrolling here.
#Override public boolean onTouchEvent(final MotionEvent ev)
{
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
final View child;
switch (action)
{
case MotionEvent.ACTION_DOWN:
//Supplemental code, if needed
break;
case MotionEvent.ACTION_MOVE:
// This handles Scrolling only.
if (myState == TOUCH_STATE_SCROLLING)
{
// Scroll to follow the motion event
// This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
scrollBy(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_UP:
// If Scrolling, stop the scroll so we can scroll later.
if (myState == TOUCH_STATE_SCROLLING)
myState = TOUCH_STATE_REST;
break
case MotionEvent.ACTION_CANCEL:
// This is just a failsafe. I don't even know how to cancel a touch event
myState = TOUCH_STATE_REST;
}
return true;
}
If course, this solution moves at X and Y at the same time. If you want to move just one direction at a time, you can differentiate easily by checking the greater of the X and Y differences. (i.e. Math.abs(deltaX) > Math.abs(deltaY)) Below is a partial sample for one directional scrolling, but can switch between X or Y direction.
3b. Change this in your OnTouch() if you want to handle one direction at a time:
case MotionEvent.ACTION_MOVE:
if (myState == TOUCH_STATE_SCROLLING)
{
//This will update the vars as long as your finger is down.
final int deltaX = (int) (myLastX - x);
final int deltaY = (int) (myLastY - y);
myLastX = x;
myLastY = y;
// Check which direction is the bigger of the two
if (Math.abs(deltaX) > Math.abs(deltaY))
scrollBy(deltaX, 0);
else if (Math.abs(deltaY) > Math.abs(deltaX))
scrollBy(0, deltaY);
// We do nothing if they are equal.
}
break;
FuzzicalLogic
A GridView can have content that extends beyond the size of the screen, but only in the vertical direction.
Question: What is the best widget to use to implement the desired effect ?
The are no default widgets (at least prior to 3.0) which implement 2D scrolling. All of them implement 1D (scrolling in one direction) only. Sadly the solution to your problem is not that easy.
You could:
Place a TableLayout inside a custom ScrollView class, and handle the touch events yourself. This would allow you to do proper 2D panning, but would not be so good for large data-sets since there would be no view recycling
Modify GridView if possible to enable horizontal scrolling functionality (probably the best solution if you can do it)
Try putting a TableLayout inside a ScrollView (with layout_width="wrap_content) which you put inside a HorizontalScrollView - though this may work it wouldn't be the optimum solution
Do something with OpenGL and draw straight to a canvas - this is how the default Gallery 3D app does it's "wall of pictures"
I think that the only Android native solution for this is to have an embebed WebView in which you can have that bi-dimensional scroll (and by the way is the only thing that has this capability).
Be pressing an image you can control the behavior by a callback from JavaScript to JAva.
This is not very "pretty" but... it's the only viable way to do what you want "out of the box".
I have implemented a GridView that overrides the onInterceptTouchEvent methods. I set the OnTouchListeneras explained by Fuzzical Logic. My issue is that setting a custom OnTouchListener, the objects of my gridView are not clickable anymore. Is that because the default OnTouchListener of a GridView calls onItemClickListener ?
I would like to have a 2 directions scrollable GridView but that has still cliackable objects. Should I implement my own method to check whether a touch matches a item or is there a easier way to achieve this ?
Grodak