I have an ImageView inside of a view pager with an ActionBar at the top. I would like to be able to single tap to hide the action bar, and I would also like to be able to pinch zoom and pan on each ImageView.
To implement the single tap to hide the action bar I have a simple OnClickListener that hides it.
To implement the pinch zoom and pan on each ImageView I am using the PhotoView Library Project.
I am having issues because only one touch event listener can be associated with an ImageView, and the implementing the PhotoView Library project overwrites my OnClickListener to hide the ActionBar with,
parent.requestDisallowInterceptTouchEvent(true);
I am not sure how to go about getting both implemented at the same time. It seems like the only solution is to create my own Pinch Zoom ImageView in order to control touch events myself.
Found out that the PhotoView library actually allows me to set onViewTap for the PhotoViewAttacher object which is exactly what I wanted.
To create the PhotoViewAttacher in the current Fragment/Activity have it implement PhotoViewAttacher.OnViewTapListener, create the attacher,
PhotoViewAttacher mAttacher = new PhotoViewAttacher(imageView);
mAttacher.setOnViewTapListener(this);
and add the following function,
public void onViewTap(View view, float x, float y) {
// your code here
}
Source
You'll have to override the PhotoView library itself. If you look at the source code, the PhotoViewAttacher class is the one that handles the onTouch events.
You'll have to add the special funcionality you're looking for at this part of the code (specially, the ACTION_DOWN) event:
#Override
public final boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
// First, disable the Parent from intercepting the touch
// event
if (null != parent)
parent.requestDisallowInterceptTouchEvent(true);
else
Log.i(LOG_TAG, "onTouch getParent() returned null");
// If we're flinging, and the user presses down, cancel
// fling
cancelFling();
break;
case ACTION_CANCEL:
case ACTION_UP:
// If the user has zoomed less than min scale, zoom back
// to min scale
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (null != rect) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
// Check to see if the user double tapped
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
if (!handled && null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
// Finally, try the Scale/Drag detector
if (null != mScaleDragDetector
&& mScaleDragDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
Related
I am making a single screen application of a simple logic circuit. I am using onTouchEvent to handle user interactions. I am using ACTION_UP, ACTION_MOVE, & ACTION_DOWN right now, but this only allows me to use one gesture. I want to be able to selection an option in my UI, such as "AND" gate. I want to use one touch to select what I want to do, then the next touch to place the gate on the screen. Instead, my onTouchEvent only allows me to touch the component I want and I have to keep my finger on the screen to drag it to the location I want to place it. This is not want I want.
I've tried researching how to implement some sort of state variable to allow onTouchEvent to wait for the next touch, but I don't think I correctly understand how to implement it.
#Override
public boolean onTouchEvent(MotionEvent motionEvent) {
Log.d("Debugging", "In onTouchEvent");
if((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
Touch.horizontalTouched = (int)motionEvent.getX()/ grid.getBlockSize();
Touch.verticalTouched = (int)motionEvent.getY()/ grid.getBlockSize();
whatWasTouched = whatWasTouched(Touch.horizontalTouched, Touch.verticalTouched);
}else if((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE){
//do nothing, finger is moving on screen
}
else if((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP){
Touch.secondHorizontalTouch = (int)motionEvent.getX()/ grid.getBlockSize();
Touch.secondVerticalTouch = (int)motionEvent.getY()/ grid.getBlockSize();
placeComponent();
draw();
}
return true;
}
I expect my first touch on the screen to be able to select an option, ex: "AND", "OR", "NOT", "SWITCH", "EDIT", etc. and then my second touch completes the desired action. I also want to be able to touch a component I placed on the screen and then touch another component so I can wire them together.
By default, event listeners in Android are for waiting - you don't have to provide any delay.
Simply set the onTouchEvent(...) listener on the ImageView and show the first bitmap. When the ImageView is touched, show the next bitmap and so on. All you have to do is keep a count of how many touches there have been in order to know which image to show (image 1, 2, 3, 4 etc).
Example...
public class LoadImage extends Activity {
int imageNumber = 1;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_load_image);
//get an image and create a bitmap from it
ImageView imageView = (ImageView) findViewById(R.id.imageView);
imageView.setImageBitmap(bitmap);
}
#Override
public boolean onTouchEvent(MotionEvent evt) {
if (evt.getAction() == MotionEvent.ACTION_DOWN) {
imageNumber++;
switch (imageNumber) {
case 2:
// show image 2
break;
case 3:
// show image 3
break;
...
}
return true;
}
return false;
}
}
I was able to figure this out by using a static class that will handle the state of my touches.
#Override
public boolean onTouchEvent(MotionEvent motionEvent) {
Log.d("Debugging", "In onTouchEvent");
if (placeState.getState() == false) {
if ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
Touch.horizontalTouched = (int) motionEvent.getX() / grid.getBlockSize();
Touch.verticalTouched = (int) motionEvent.getY() / grid.getBlockSize();
whatWasTouched = whatWasTouched(Touch.horizontalTouched, Touch.verticalTouched);
if(whatWasTouched.equals("DELETE")){
visualComponents.clear();
logicalComponents.clear();
}
placeState.toggleState();//sets to true
draw();
}
}else if (placeState.getState() == true) {
Touch.secondHorizontalTouch = (int) motionEvent.getX() / grid.getBlockSize();
Touch.secondVerticalTouch = (int) motionEvent.getY() / grid.getBlockSize();
placeComponent();
placeState.toggleState();//sets to false
draw();
}
return true;
}
Solved it by using emandt's suggestion. My personal Solution added below.
I'm using Android Studio for this.
I searched for solutions but couldn't find anything resembling this.
I want to know on which ImageView an UP action occurs while starting the DOWN action on a different ImageView (to eventually be able to drag one image over the other and make it snap to the same position by getting the position of the image I dragged over).
My example has two ImageViews with the id imageView (left) and imageView2(right).
In my example I'm not dragging anything yet, I just want to touch the left image, see "Action was down" in the log and lift the finger over the right image showing "Action was up2".
I don't know if this is easily possible.
As far as I can tell from testing, the MotionEvent.ACTION_UP only fires for an ImageView when you also pressed down on it beforehand. So when I release on top of imageView2 it only shows "Action was up" from the left image.
I wondered if it was possible by playing with return false, since the return value tells if an ActionEvent is consumed so I thought if the UP event of imageView returns false, maybe it does trigger the UP event of imageView2 but no. (Either complete misunderstanding on my part or it doesn't recognise UP on the second because it didn't start with a DOWN and MotionEvents probably always have to start with a DOWN).
public class MainActivity extends Activity {
ImageView imageView;
ImageView imageView2;
String DEBUG_TAG = "action";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
imageView2 = findViewById(R.id.imageView2);
imageView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
return false;
default :
//return true;
}
return true;
}
});
imageView2.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN2"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP2"+v.toString());
return true;
default :
//return true;
}
return true;
}
});
}
}
If there is no simple way to do this, I'm thinking about solving this mathematically, but maybe some of you can help.
So my question is, is there a way to recognise an UP action on a second ImageView while currently being in a MotionEvent of another ImageView?
SOLUTION (see emandt's answer)
I ditched the second OnClickListener because I realised that the 2nd image doesn't need any, I just need its position.
Added this method:
#Nullable
private View getDroppedView(View droppedView, int x, int y, List<View> arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
//if currently iterated view doesn't have values for getGlobalVisibleRect, skip the .contains part
//ignore the item which is your current active item (which would potentially be dropped)
//getGlobalVisibleRect sets cVisibleBoundsRect immediately to the Rect given as parameter
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)||(cView.equals(droppedView))) continue;
if (cVisibleBoundsRect.contains(x, y)) {
Log.d(DEBUG_TAG,"Found something");
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
Log.d(DEBUG_TAG,"Found nothing");
return null;
}
And added this in onUP:
case (MotionEvent.ACTION_UP) :
View dropTarget;
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
dropTarget = getDroppedView(v, (int)event.getRawX(), (int)event.getRawY(), listOfViews);
if (dropTarget != null){
v.setX(dropTarget.getX());
v.setY(dropTarget.getY());
}
I think you want to know which is the View where you release the finger from the screen, am I right?
To do this you can use the same "View.OnTouchListener()" for all of your Views and in the ACTION_UP you have to call a new method similar to this (pseudo-code):
....
case (MotionEvent.ACTION_UP) :
View[] cArrayOfPossibileViews = new View[]{ findViewById(IMAGE_1), findViewById(IMAGE2) }
getDroppedView(v, event.getRawX(), event.getRawY(), cArrayOfPossibileViews);
break;
}
....
#Nullable
private View getDroppedView(View view, int x, int y, View[] arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)) continue;
if (cVisibleBoundsRect.contains(x, y)) {
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
return null;
}
This method get View bounds and compare them avains X and Y of your Touch Event. If X and Y are contained inside a View bounds it means that View is the one you need.
Can you create different actions for clicking on the same ImageView but different parts? Say I have an ImageView and I want for it to act differently if I click on the top of the ImageView and differently if I click on the middle part of the ImageView . Can I achieve this in android
The best way is to add an OnTouchListener to the view and handle click event by yourself related to the touch coordinate.
But if you need an easy way which I don't suggest because it can lead to performance issue you can add transparent views on top of your ImageView and add different OnClickListener for them.
if you have fixed sections in your imageview you can use a frame layout and add 3(for example) transparent views on your imageview and then set click listeners on those views.
but if you have dynamic sections you can handle it by combining onTouchListener and onClickListener, for example :
// just store the touch X,Y coordinates
View.OnTouchListener touchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// save the X,Y coordinates
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
lastTouchDownXY[0] = event.getX();
lastTouchDownXY[1] = event.getY();
}
// let the touch event pass on to whoever needs it
return false;
}
};
View.OnClickListener clickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
// retrieve the stored coordinates
float x = lastTouchDownXY[0];
float y = lastTouchDownXY[1];
// use the coordinates for whatever or check the clickable areas
Log.i("TAG", "onLongClick: x = " + x + ", y = " + y);
}
};
I think the best way for this is that you declare a frameLayout as parent view then you can declare views on your imageView then clicking each view will perform what you want.
So this is not a tecnical question with code but more on some thoughts. I am new to android developing and because of that I am not completely familiar with a lot of what it has to offer.
I have a list of items on the screen and you can scroll up and down to view the different items.
My issue came up when I decided to have the items be able to slide left and right to reveal a settings like panal with a few optons below them(each item has its own settings below it, not a setting screen for all items kind of deal). I looked around for different ways to do this but cant seem to find one that works. What I am thinking so far is to have a HorizonalScrollView with my item in it with the setting menu to the left or right off the screen, but when I put my text area in the HorizonalScrollView, it only takes up half the screen, so I can fit two side by side, not what I wanted and the end result wont be what I imagined. I really would like a solution that allows for the setting to be under the item, so when you push it out of the way it reveals the settings.
Is there a better way or should I just continue trying to make my HorizonalScrollView work, any guides or thought on how I could go about this would be greatly appreciated.
Went though my apps to see if I could find one that has something similar, gmail app has it. Here is a link to an image that gives you an idea, after you slide the item to the side it brings up an undo or archive buttons in the place of the item as if they were hiding under the list item you swiped away
it's quite an interesting topic this question. And be prepared to do a certain amount of custo stuff.
First of all, drop the HorizonalScrollView, I don't think it will help you. The layout of each item should be something like this (pseudo-code, you can build the XML yourself =] ):
FrameLayout
RelativeLayout id:topContent background:someSolidColor
// inside this RelativeLayout, the stuff that is visible on the ListView
RelativeLayout id:bottomContent
// inside this RelativeLayout, the stuff that is behind the content
/FrameLayout
that way you will be literally putting one thing on top of the other. Also note that the topContent have a background with a solid color. If you do not specify any background, both RelativeLayouts will be visible. Also note that I used RelativeLayout, just because I like them, and I like their flexibility, but this will depend on the content of your list view and your settings.
And now is when things get fun, you'll need a GestureDetector to detect the finger sliding and you'll use that value to generate a margin offset on the id:topContent.
You can create a TouchListener like this:
public class MySlideListener extends View.OnTouchListener{
private View v;
private GestureDetector gestureDetector;
public MySlideListener (View v){
this.v = v;
gestureDetector = new GestureDetector(v.getContext(), myGestureListener);
}
public boolean onTouch (View v, MotionEvent event){
return gestureDetector.onTouchEvent(event);
}
private SimpleOnGestureListener myGestureListener = new SimpleOnGestureListener(){
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
// now here we make the view scroll
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
lp.leftMargin = distanceX;
lp.rightMargin = -distanceX;
// You might need to call view.requestLayout();
// but first give it a try without it
// This part of the method for processing the horizontal
// offset can (and should) be further developed to add some
// 'snap-in' or transparency functionality to make the whole
// concept work better.
// But this code should give you a proof of concept on how to deal with stuff.
// The important part is that now you have a call back that have access
// to the view during onScroll.
// Also might be necessary to enable/disable the bottomContent view
// in order for it to be not clickable whilst not visible.
return true;
}
}
}
and then set a new of those listeners for each topContent of your ListView (probably inside the getView from the adapter) with topContentView.setOnTouchListener(new MySlideListener(topContentView));
Please keep in mind that I typed all this code by heart and is 100% untested!
edit:
The above code is the correct direction, but it's a 100% untested thing.
Now the code below, I've just compiled, tested, and this code works!
This class is also a bit more efficient as you can create only one and apply the same instance to all the items created on your adapter. You can see it's getting the view to scroll on the touch event.
public class MySlideListener implements View.OnTouchListener {
private View view;
private ListView listView;
private GestureDetector gestureDetector;
public MySlideListener(ListView lv) {
listView = lv;
gestureDetector = new GestureDetector(lv.getContext(), myGestureListener);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
view = v;
gestureDetector.onTouchEvent(event);
return true;
}
private SimpleOnGestureListener myGestureListener = new SimpleOnGestureListener() {
private int origLeft, origRight;
public boolean onDown(MotionEvent e) {
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
origLeft = lp.leftMargin;
origRight = lp.rightMargin;
return true;
};
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
listView.requestDisallowInterceptTouchEvent(true);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
lp.leftMargin = (int) (origLeft + (e2.getRawX() - e1.getRawX()));
lp.rightMargin = (int) (origRight - (e2.getRawX() - e1.getRawX()));
view.requestLayout();
return true;
};
};
}
I have a ListView inside of a ViewFlipper which I am flipping when the user swipes across the screen. Clicking on a ListView will open the browser. Sometimes when I am swiping, it gets detected as a touch on the ListView and will open the browser. This can be annoying. How can I prevent this from happening?
class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
try {
if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)
return false;
// right to left swipe
if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
viewFlipper.setInAnimation(slideLeftIn);
viewFlipper.setOutAnimation(slideLeftOut);
viewFlipper.showNext();
} else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
viewFlipper.setInAnimation(slideRightIn);
viewFlipper.setOutAnimation(slideRightOut);
viewFlipper.showPrevious();
}
if (viewFlipper.getDisplayedChild() == 0) {
// TODO: light up left
flipperPosition = 0;
} else if (viewFlipper.getDisplayedChild() == 1) {
// TODO: light up middle
flipperPosition = 1;
} else if (viewFlipper.getDisplayedChild() == 2) {
// TODO: light up right
flipperPosition = 2;
}
} catch (Exception e) {
System.out.println(e);
}
return false;
}
}
protected MotionEvent downStart = null;
public boolean onInterceptTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// keep track of the starting down-event
downStart = MotionEvent.obtain(event);
break;
case MotionEvent.ACTION_MOVE:
// if moved horizontally more than slop*2, capture the event for ourselves
float deltaX = event.getX() - downStart.getX();
if(Math.abs(deltaX) > ViewConfiguration.getTouchSlop() * 2)
return true;
break;
}
// otherwise let the event slip through to children
return false;
}
The way this is normally done is through the parent view's onInterceptTouchEvent method. onInterceptTouchEvent has a chance to see any touch event before a view's children do. If onInterceptTouchEvent returns true the child view that was previously handling touch events receives an ACTION_CANCEL and the events from that point forward are sent to the parent's onTouchEvent method for the usual handling. It can also return false and simply spy on events as they travel down the view hierarchy to their usual targets.
You want to do essentially this in onInterceptTouchEvent on the parent view where you're detecting the flings:
On ACTION_DOWN, record the location of the touch. Return false.
On ACTION_MOVE, check the delta between initial touch down position and current position. If it's past a threshold value, (the framework uses ViewConfiguration#getScaledTouchSlop() or other appropriate values from ViewConfiguration for things like this,) return true.
Detect and handle the fling as usual based on onTouchEvent.
Once you intercept, the ListView will cancel its touch handling and you won't get unwanted tap events on your list items. ListView is also set up to disallow its parent from intercepting events once the user has started vertically scrolling the list, which means you won't get mistaken horizontal flings if the user sloppily flings the list vertically.
This is how things like the stock Android Launcher or News and Weather do side to side paging of scrolling/tappable content.
Have you tried using SimpleOnGestureListener.onSingleTapConfirmed(MotionEvent) for the on touch event ("click")? This will only be called after the detector is confident that the user's first tap is really a tap and not a double tap (or hopefully a fling).
class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Code...
}
}