Using a widget.Gallery to display a horizontally scrolling list of items. I've implemented paging in the gallery with what seems to be the standard technique: subclass Gallery and implement:
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityX>0) {
onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT, null);
} else {
onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null);
}
}
When tapping the gallery, I fade up next and previous image buttons. When clicking these, I want the gallery to animated to the next/previous page, respectively. I've tried calling onKeyDown from my next-button handler, but strangely this has no effect.
AbsSpinner has setSelection(int position, boolean animate) but animate is ignored in Gallery.
Exactly what toucan said, but to elaborate further (comments limits are too short):
The problem seems to be the fact that the Gallery doesn't let the user scroll if no children exist in that position yet. scrollToChild() is the culprit when trying to inject the event:
private boolean scrollToChild(int childPosition) {
View child = getChildAt(childPosition);
if (child != null) {
int distance = getCenterOfGallery() - getCenterOfView(child);
mFlingRunnable.startUsingDistance(distance);
return true;
}
return false;
}
Interestingly, if you fling the gallery with your fingers, it will cause the child to be created. Then, if you let go of your finger (going back to the original position), and then press the button that activates the onKeyDown injection, it will work flawlessly - because the child is there.
Unfortunately there's no real solution since everything is private in that class. The only solution is to use setSpacing(-1) or something in the gallery, so left and right children are always created and visible, but just behind the currently selected view.
As a footnote, I'm really baffled as to why everything is private in that class (or in any other of the Android widget classes for that matter). This is one of those things that could easily be fixed with some small code change.
Edit (Aug 2012): For future reference, rather than trying to use a Gallery for this kind of pattern (when you want the user to swipe between different items while only one of them is visible), it's much better to use the Android's compatibility package's ViewPager class. In my opinion, the compatibility package is not as celebrated as it should be (it took me a while to get notice of it). For developers targeting Android 2.x+, it's a godsend.
this is working
create 2 image buttons and gallery
eg:
nextbtn,backbtn and gall
button code is avialable here
nextbtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
gall.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null);
}
});
backbtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
gall.onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT, null);
}
});
String strMethod = velocityX > 0 ? "movePrevious" : "moveNext";
try {
Method method = Gallery.class.getDeclaredMethod(strMethod, null);
method.setAccessible(true);
method.invoke(this, null);
} catch (Exception e) {
e.printStackTrace();
}
Related
I want to disable right to left swipe in ViewPager2.
I basically have a viewpager2 element with 2 pages in my navigation drawer. I want my second page to show up only when I click some element in my first page (right to left swipe from the first page should not open the second page), while when I'm in the second page, the viewpager2 swipe (left to right swipe) should swipe as it should do in viewpager.
I've tried extending the ViewPager2 class and override the touch events, but unfortunately it ViewPager2 is a final class, so I cannot extend it.
Secondly, I tried to use setUserInputEnabled method to false, but this disabled all swipes altogether (I just want to disable right to left swipe). If I could find some listener which checks for the current page before swiping and disable swipe otherwise, it would probably work.
implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha05'
Code for setting up of ViewPager2
ViewPager2 pager = view.findViewById(R.id.pager);
ArrayList<Fragment> abc = new ArrayList<>();
abc.add(first);
abc.add(second);
navigationDrawerPager.setAdapter(new DrawerPagerAdapter(
this, drawerFragmentList));
pager.setAdapter(new FragmentStateAdapter(this), abc);
I found a listener which can listen when the user tries to swipe, it'll then check the current page, if it's the first page, disable the user input else enable it as it was by default.
Here's the code snippet for that
In Java:
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
if (state == SCROLL_STATE_DRAGGING && pager.getCurrentItem() == 0) {
pager.setUserInputEnabled(false);
} else {
pager.setUserInputEnabled(true);
}
}
});
In Kotlin:
viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
viewPager.isUserInputEnabled = !(state == SCROLL_STATE_DRAGGING && viewPager.currentItem == 0)
}
})
Since my scenario was of 2 pages only, checking the page number would be good for me, but in case we have more than 2 pages and we need to disable the swipe in one particular direction, we may use onPageScrolled(int position, float positionOffset, int positionOffsetPixels) listener of viewpager2 and handle the desired scenario according to the positive or negative values of position and positionOffset.
Solution for more than 2 Fragments.
If you know enough about Android go straight to the CODE... if don't:
In reality this solution aims to mimic the absence of a page in a
given direction
So instead of using the code bellow I would rather recommend:
Disabling the swiping function for the entire ViewPager2 and allow
navigation only via tabs. One can then add or remove tabs to make it
seem as if Fragments are being added or removed.
To make use of a 100% functional swiping function there is at least 2 behaviors that still require fixing which are discussed after the code.
Code
It's been awhile(8/21/2022), and I finally took the time to test some issues with the code and came up with a better solution:
public enum Direction {
allow_all(null),
right_to_left(Resolve.r2L()),
left_to_right(Resolve.l2R()),
left_and_right(Resolve.lR()); // NOT TESTED SHOULD IGNORE
Direction(Resolve resolve) {
this.resolve = resolve;
}
#FunctionalInterface
private interface Resolve {
boolean resolve(float prev, float next);
static Resolve r2L() {
return (prev, next) -> prev > next;
}
static Resolve l2R() {
return (prev, next) -> prev < next;
}
static Resolve lR() {
return (prev, next) -> prev != next; //THIS REQUIRES TESTING
}
}
private final Resolve resolve;
public static class Resolver {
float prev;
long prevTime;
Direction toBlock = allow_all;
public boolean shouldIntercept(MotionEvent event) {
if (toBlock == allow_all) return false;
long nextTime = event.getDownTime();
float next = event.getX();
boolean intercept = false;
if (prevTime == nextTime) {
intercept = toBlock.resolve.resolve(prev, next);
} else {
prevTime = nextTime;
}
prev = next;
return intercept;
}
public void setToBlock(Direction toBlock) {
if (this.toBlock != toBlock) {
this.toBlock = toBlock;
prev = 0;
}
}
}
}
Inside the Adapter...
public class MyAdapter extends FragmentStateAdapter {
private final Direction.Resolver resolver = new Direction.Resolver();
#Override
public void onAttachedToRecyclerView(#NonNull RecyclerView recyclerView) {
recyclerView.addOnItemTouchListener(
new RecyclerView.SimpleOnItemTouchListener(){
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
return resolver.shouldIntercept(e);
}
}
);
super.onAttachedToRecyclerView(recyclerView);
}
public void disableDrag(Direction direction) {
resolver.setToBlock(direction);
}
public void enableDrag() {
resolver.setToBlock(Direction.allow_all);
}
}
The drawbacks:
**
A) onPageSelected:
**
the setToBlock(Direction) method should be executed upon page change.
The question is then: What should call it / When should I call it?
And I don't have the answer for that...
My best guess is that placing the method inside the ViewPager's onPageSelected callback listener would be a good place.... but there is an issue with this listener.
viewPager2.registerOnPageChangeCallback(
new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
if (position == 2) mAdapter.enableDrag(); return;
if (position == 1) mAdapter.disableDrag(left_to_right)
}
}
);
The listener sometimes registers a page change BEFORE the swapping animation ends, when a certain finger fling is used.
This means that for a fraction of a second the fling is subjected to the Direction rule of the incoming page, but in reality the swapping is still on the previous page.
In the example above, the mAdapter.enableDrag(); occurs at position 2.
Let's say that position 0 should be disallowed, so position 1's rule is mAdapter.disableDrag(left_to_right) so that postiion 0 cannot be reached.
If I fling the finger in such a way so that the ViewPager register's a position change to 2 (eanbleDrag()) and then fling in the opossite direction without lifting the finger, the page turns back from a half rendered position 2 to position 0 which should be inaccessible.
This is not hard to reproduce 1 out of 5 attempts, but you need to actively want to reproduce it.
I don't know how to fix this.
Maybe the disableDrag() call should be done at a much later stage in the swapping, but that implies accessing the Page Fragment's lifecycle.
which means using the setTargetFragment() method (The "clean" way) and I would rather die than use that.
An alternative is using a shared ViewModel bound to the backStackEntry for ViewPagerFrag to PageFrag (Fragment to Fragment) communication.
Off course... let's ignore for a moment that both of this methods use static fields behind curtains for reference storage... Which means you can absolutely go the public static way, that is of course you keep the code clean...
**
B) Restricting swipes in one direction.
**
In reality this solution aims to mimic the absence of a page in a given direction, the issue is that If we carefully decompose the behavior of page absence, we notice that the animation restricts motion only once a given axis has been reached, to be more precise, the restriction becomes a reality once the LAST page(index 0) is fully displayed, if the page of index 0 slides off screen towards index 1, even in the slightest, you can still swipe the page towards 0 again.
By restricting the movement in one direction and let this rule govern THE ENTIRE PAGE, an unwanted behavior occurs:
Example: [position 0(disallowed)] - [1(allowed)] - [2(allowed)]
To mimic the absence of position 0, position 1 must disableDrag(left_to_rigth);
If we drag our finger from 1, towards 2, and then GENTLY drag it back (maybe because the user changed their minds and decided to stay on page 1)..., Then, because the entire page is ruled by toBlock direction == left_to_rigth, the page will refuse to go back, and the animation will get stuck in between both fragments.
My guess is that the disabling should be performed once a given Fragment Y axis has reached a given trigger Y axis in the screen (AKA: Using a screen coordinates listener), this implies more knowledge on all the different available listeners that the component gives us access to.
...
So...
This is the best I can do for now, I would really appreciate any advice on how to solve this or maybe tackle the main issue which is the addition and removal of fragments without losing states. Even though I hardly see this being a possibility (at least with the StateAdapter) since the DiffUtil would be required to infer reordering changes and I believe the Mayer's algo is not meant to deal with that level of inference (reordering inference ("index jump")).Instead the Mayers only works by inferring whole segment reordering.
Also the Fragment collection would be required to behave both as LIFO AND FIFO (I believe the term is carousel???), in order to support additions and removals from both ends, Top (prohibiting left-to-right access) AND Bottom (prohibiting right-to-left access).
I think this is both too much and too specific to be a necessary enhancement to the ViewPager2 tool.
Because I lack knowledge on all these details...IMHO the best solution would be:
To disable the swiping function for the entire ViewPager2 and allow navigation only via tabs. One can then add or remove tabs to make it seem as if Fragments are being added or removed.
Simplest Solution for more than 2 Fragments:
int previousPage = 0;
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
if(position < previousPage){
pager.setCurrentItem(previousPage, false);
} else {
previousPage = position;
}
}
});
Extend the viewpager class and override the functions onInterceptTouchEvent and onTouchEvent. Then identify the direction of the swipe and return false if you don't want to swipe.
You can use this helper method for swipe detection:
float downX; // define class level variable in viewpager
private boolean wasSwipeToLeftEvent(MotionEvent event){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
return false;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
return event.getX() - downX > 0;
default:
return false;
}
}
Then in your method for touch events:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return !this.wasSwipeToLeftEvent(event);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
return return !this.wasSwipeToLeftEvent(event);
}
I modified the code from this answer, if you need more explanation please see this: https://stackoverflow.com/a/34111034/4428159
I have a RecyclerView (with LinearLayoutManager) and a custom RecyclerView.ItemDecoration for it.
Let's say, I want to have buttons in the decoration view (for some reason..).
I inflate the layout with button, it draws properly. But I can't make the button clickable. If I press on it, nothing happening(it stays the same, no pressing effect) and onClick event is not firing.
The structure of ItemDecoration layout is
<LinearLayout>
<TextView/>
<Button/>
</LinearLayout>
And I'm trying to set listener in ViewHolder of the decoration
class ItemDecorationHolder extends RecyclerView.ViewHolder {
public TextView header;
public Button button;
public HeaderHolder(View itemView) {
super(itemView);
header = (TextView)itemView.findViewById(R.id.header);
button = (Button)itemView.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//.. Show toast, etc.
}
});
}
}
And i'm drawing the decoration in onDrawOver method. (actually, I'm modifying this codebase: https://github.com/edubarr/header-decor )
Any ideas? Is it doable?
Thanks!
While the real header is scroll off the screen, the visible one is drawing on canvas directly ,not like a normal interactive widget.
You have these options
Override RecyclerView.onInterceptTouchEvent(), though with some invasiveness so I prefer the next one.
Make use of RecyclerView.addOnItemTouchListener(), remember the motion event argument has been translated into RecyclerView's coordinate system.
Use a real header view, but that will go a little far I think.
If you take option 1/2, Button.setPressed(true) and redraw the header will have a visual press effect.
In addition to what Neil said,
the answer here might help.
Passing MotionEvents from RecyclerView.OnItemTouchListener to GestureDetectorCompat
And then you just need to calculate the height of the header and see if the click falls onto that header view and handle the event yourself.
private class RecyclerViewOnGestureListener extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
float touchY = e.getY();
ALLog.i(this, "Recyclerview single tap confirmed y: " + touchY);
//mGroupHeaderHeight is the height of the header which is used to determine whether the click is inside of the view, hopefully it's a fixed size it would make things easier here
if(touchY < mGroupHeaderHeight) {
int itemAdapterPosition = mRecyclerView.getLayoutManager().getPosition(mRecyclerView.findChildViewUnder(0, mGroupHeaderHeight));
//Do stuff here no you have the position of the item that's been clicked
return true;
}
return super.onSingleTapConfirmed(e);
}
#Override
public boolean onDown(MotionEvent e) {
float touchY = e.getY();
if(touchY < mGroupHeaderHeight) {
return true;
}
return super.onDown(e);
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
float touchY = e.getY();
if(touchY < mGroupHeaderHeight) {
return true;
}
return super.onSingleTapUp(e);
}
}
As Neil is pointing out, things are getting more complicated than that. However by definition you can't.
So, why not including good libraries that do that and more?
I propose my hard work for clickable sticky header in my FlexibleAdapter project, which uses a real view (not decorators) to handle click events on headers when sticky.
There's also a working demo and a Wiki page on that part (and not only).
I have an Activity which uses the Android NavigationDrawer.
When using only fragments (as usual), everything works perfect.
But now I want to use this drawer on other activities of my app, and for some of them,
I don't want the main view to be a fragment.
Question
The problem is, the onTouchEvent() of the activity itself (and the onItemClickedListener() of a child ListView for that matter) isn't called, because the drawer consumes it.
Of course, I want it to be called:)
Needless to say, I would hope the answer will be simple (even a XML one), and hopefully not by extending the Drawer class (unless that's what it takes of course).
More Info
The Activity's main layout is very simple, basically a ListView and the DrawerLayout on top of it (below in XML).
The Drawer has one fragment as it's childView (for fragment navigation) and of course, the ListView for the Drawer Items.
I've seen many questions regarding (not exactly) similar issues, and the frequent answer was to use onInterceptTouch(), requestDisallowInterceptTouchEvent() on the DrawerLayout, and on the Parent view (Activity's main content) and even onTouchEvent() (with False returned) on the ListView of the Drawer.
Nothing seems to do the trick.
I read this link
and it does seem like using Intercept methods somewhere could be the answer. But how?
Please let me know if you need any code. But it's a very basic code/layout for this matter.
Thanks!
Apparently the answer is somewhat easy, although it does make you extend the DrawerLayout and do some thinking, and maybe will result in some strange results (using the LAST
example, I haven't seen any, yet).
Anyway, related questions which looking backwards can help understanding the issue (will explain about the first one later on):
1. DrawerLayout prevents call of MainActivity.onTouchEvent()
2. How can I requestDisallowTouchEvents on Android DrawerLayout
3. Set drag margin for Android Navigation Drawer
Answer
First, please note that I put lots of examples here. If you just want the best one (for me), jump to the last one.
Secondly, if someone has enough reputation, please comment on the first link's question and put a link to this answer (it can help that guy).
Example 1
Well, basically, just extend Android's DrawerLayout and replace onTouchEvent() to this:
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
return false;
}
This solution will do anything except that it won't open the Drawer on slides, only menu clicks and the like. Besides, it forwards clicks so when the Drawer is open
for instance, touching outside of it will NOT close it, but click on whatever is behind (e.g. a ListView). Le'ts try harder...
Example 2
Now, let's catch the open OR visible cases, to return true (and consume the action at the Drawer).
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
if(isDrawerOpen(findViewById(R.id.list_slidermenu)) ||
isDrawerVisible(findViewById(R.id.list_slidermenu))){
return true;
}
return false;
}
This solution is better, as it prevents clicks on behind the Drawer when the drawer is open or even visible (slide starts...). But touch-sliding it still doesn't work.
Example 3
Ok, so let's just split cases. Touches (MotionEvent.ACTION_DOWN) inside the Drawer's margin (area that Google desided to slide Drawer when touched at)
will result in returning True to consume the action, and others will forward the event (return False).
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
float edge = 30;//that's for a left drawer obviously. Use <parentWidth - 30> for the right one.
View mDrawerListView = findViewById(R.id.drawer_listview);
if(isDrawerOpen(mDrawerListView) ||
isDrawerVisible(mDrawerListView)){
return true;
} else if(arg0.getAction() == MotionEvent.ACTION_DOWN && arg0.getX() > edge){
return false;
}
return true;
}
Note that I used 30dp. That's what I found to be the margin (although in one of the links it is said to be 20....).
Well, the next example would of course be deciding what is, exactly, that edge (see in code above) value is, according to Android. We don't want to
use a number that could change or whatever.
New Question
So now that first link should come handy. It "hacks" the Drawer code to get that Drawer edge/megin number. BUT, it didn't work for me, as those exact Field names could not be found.
I run mDrawerLayout.getClass().getField() which returns all the fields, but without any luck finding what we want. Anyone?
Last Example - Full Code
Ok, looking on example number 3, after understanding what exactly I did, we can make it faster by extending the onFinishInflate() method and save it as a global variable
for this CustomDrawerLayout for later use. We can also put that first 'if' inside the second one to save some more work. OK here goes:
View mDrawerListView;
...
#Override
protected void onFinishInflate() {
super.onFinishInflate();
mDrawerListView = findViewById(R.id.drawer_listview);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if(event.getX() > 30 && event.getAction() == MotionEvent.ACTION_DOWN){
if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
return true;
} else{
return false;
}
}
return true;
}
That's it for now! Hope it'll helps someone in the future beside myself, hehe....
While working on the same problem I was inspired by guy_m's answer and boiled down his proposals to the following solution.
Again it amounts to extending DrawerLayout and overriding onInterceptTouchEvent(). The logic is simple:
Whenever the touch event occurs off the drawer view (the slideable part), we return false. Then our DrawerLayout is out of the game when it comes to handling the event -- the event is handled by whatever view we put into the DrawerLayout at the respective position.
On the other hand, when the event occurs inside the drawer view, we delegate to super.onInterceptTouchEvent() to decide what to do with the event. That way the drawer will slide in and out as before on touch gestures happening on itself.
The following code sample is for a DrawerLayout whose drawer view is located on the right (android:gravity="right"). It should be obvious how to modify it to cover also the case of a left-placed drawer.
public class CustomDrawerLayout extends DrawerLayout
{
#Override
public boolean onInterceptTouchEvent( MotionEvent event )
{
final View drawerView = getChildAt( 1 );
final ViewConfiguration config = ViewConfiguration.get( getContext() );
// Calculate the area on the right border of the screen on which
// the DrawerLayout should *always* intercept touch events.
// In case the drawer is closed, we still want the DrawerLayout
// to respond to touch/drag gestures there and reopen the drawer!
final int rightBoundary = getWidth() - 2 * config.getScaledTouchSlop();
// If the drawer is opened and the event happened
// on its surface, or if the event happened on the
// right border of the layout, then we let DrawerLayout
// decide if it wants to intercept (and properly handle)
// the event.
// Otherwise we disallow DrawerLayout to intercept (return false),
// thereby letting its child views handle the event.
return ( isDrawerOpen( drawerView ) && drawerView.getLeft() <= event.getX()
|| rightBoundary <= event.getX() )
&& super.onInterceptTouchEvent( event );
}
}
With these answers, i still had some trouble. I could get the motionEvent back to the activity but I lost the onClick listener answer by fragment or everything on the screen. So I found another way to have everything work ( get answer when override OntouchEvent from activity, and answer to onClick Listener )
Extend DrawerLayout and Override this methode :
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(super.onInterceptTouchEvent(ev)) return true;
else {
Activity activity = AppContext.getCurrentActivity();
return activity.onTouchEvent(ev);
}
}
if the drawer want the motion event, let it handle it. And if not, pass the event to activity yourself. (AppContext.getCurrentActivity is something from you with current activity, you can for instance attach activity as weakreference to the drawerLayout OnCreate)
The good thing with this way, you don't care about the edge and don't care if start or end. And you don't care also if it is open or close. Everything work fine.
I have a solution:
Set OnTouchListener on the screen layout (the first childview of DrawerLayout, normally) and transmit the TouchEvent to a custom GestureDetector.
So, you can do your own things in it. One more important thing: if you want to override onSingleTapUp() or something else, you should return true in onDown() to make sure that you can get the rest MotionEvent to make onSingleTapUp() work.
private class MyGestureListener implements GestureDetector.OnGestureListener{
#Override
public boolean onDown(MotionEvent e) {
return true;
}
#Override
public void onShowPress(MotionEvent e) {
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
// do your own things
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
#Override
public void onLongPress(MotionEvent e) {
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
and set it :
mGestureDetector=new GestureDetector(this, new MyGestureListener());
layout_content.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
});
To add on to guy_m 's answer, here is my implementation for a drawer that opens from the right, includes constructors so that it is viewable in the layout editor and also takes into account when a user swipes from past the edge point:
public class CustomDrawerLayout extends DrawerLayout {
View mDrawerListView;
float edge;
int holddown = 0;
static final String TAG = CustomDrawerLayout.class.getSimpleName();
public CustomDrawerLayout(#NonNull Context context) {
super(context);
setscreendimensionvals(context);
}
public CustomDrawerLayout(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
setscreendimensionvals(context);
}
public CustomDrawerLayout(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setscreendimensionvals(context);
}
private void setscreendimensionvals(Context context){
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
/*((Activity) context).getWindowManager()
.getDefaultDisplay()
.getMetrics(displayMetrics); */
int width = displayMetrics.widthPixels;
float density = displayMetrics.density;
edge = width - (30 * density); // 30 is the edge of the screen where the navigation drawer comes out
Log.d(TAG,"edge: " + edge);
Log.d(TAG,"width: " + width);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
mDrawerListView = findViewById(R.id.drawerconstraint_overworld);
}
#Override
public boolean onTouchEvent(MotionEvent event){
super.onTouchEvent(event); // need to add action up and a local variable to detect when lifted finger
//Log.d(TAG,"point: " + event.getX());
if(event.getX() >= edge && (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE)){
holddown = 1;
//Log.d(TAG,"hold down");
}
if(event.getAction() == MotionEvent.ACTION_UP){
holddown = 0;
//Log.d(TAG,"hold up");
}
if(holddown == 1){
return true;
}else{
if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
return true;
} else{
return false;
}
}
}
}
For anyone who might have the unfortunate luck of encountering as persistent an issue as this one, I will add onto the others' answers with my own problem case and solution in the hopes that fewer souls will face this nightmare of a headscratcher.
Due notice is that my explanation will most likely work for any swipeable view whose parent is DrawerLayout (e.g. this solution only works for views that are children of DrawerLayout), but I will regale my experience and my toils for the purpose of clarity.
In my case, I needed to have a MaterialCalendarView (3rd-party CalendarView on steroids) in a DrawerLayout with a NavigationView to the right (i.e. with "android:gravity"="end"). It wasn't long after implementing the view hierarchy that I realised there had existed a conflict between the swipe events of my NavigationView and MaterialCalendarView.
In essence, what occured was that whenever I began swiping the MaterialCalendarView to the right in order to swipe back to the next month, I wound up triggering the DrawerLayout's touch event interceptor and closing said DrawerLayout instead of swiping to the previous month.
So, the solution should be easy, shouldn't it? Set a onTouchListeneron the MaterialCalendarView, call requestDisallowInterceptTouchEvent(), and call it a day—akin to this in the view-hosting Activity:
calendar.setOnTouchListener { _, motionEvent ->
when(motionEvent.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
drawerLayout.requestDisallowInterceptTouchEvent(true)
}
}
true
}
...and you should be all set?
Well, the fact that I'm answering here is enough to infer that wasn't the case and that my onTouchListener wasn't, like the rest, being triggered.
After extensively scouring this thread and trying to follow everyone's advice, I came at a point wherein none of the solutions offered were helpful for someone who'd simply wanted to "exclude" a view from being detected by the DrawerLayout's touch event interceptor. Some ideas entirely paralysed my touch event infrastructure, while others simply gave me more of the same behaviour. I had hit a roadblock and I didn't know what to do.
Then, an epiphany.
I realised that due to my inexperience with writing custom views I'd missed the glaringly obvious: what I needed to do was simply find out where the MaterialCalendarView was, get its coordinates, and see if any touch events are inside in order to call the proper implementation (be it the Activity or default DrawerLayout one)! And, of course, since in the former, the onTouchListener disables interception of touch events by the DrawerLayout, that meant only the MaterialCalendarView could handle the swipes! It was so simple!
And fast-forward to learning about MotionEvents, reading up on what the heck a Rect was, and a muddy in-between of crashes, I finally wrote the custom DrawerLayout which responded to my swipes on the MaterialCalendarView only with the Activity implementation and ignored the ones outside, opting for the DrawerLayout touch interceptor:
class EventCalendarDrawerLayout : DrawerLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
lateinit var calendar: MaterialCalendarView
lateinit var drawer: View
override fun onFinishInflate() {
super.onFinishInflate()
drawer = getChildAt(1)
calendar = findViewById(R.id.event_calendar)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
val rect = Rect()
calendar.getGlobalVisibleRect(rect) // get the calendar rect positions
// respond to proper motions and forward events contained inside the calendar's rect only
if((event.action == MotionEvent.ACTION_MOVE ||
event.action == MotionEvent.ACTION_DOWN) && rect.contains(event.x.roundToInt(), event.y.roundToInt())) {
return (context as Activity).onTouchEvent(event)
}
// otherwise return the default intercept touch event response
return super.onInterceptTouchEvent(event)
}
}
It's not rocket science, truly, but it is something I deemed worth showing, as it was new and unexpected for me (and undoubtedly many others yet to venture here). Nevertheless, I believe that this implementation may work to ignore as many views embedded inside DrawerLayouts as one could possibly wish for.
I call
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
when my app starts to make my app able to display the full screen.
I want my app's UI to pop up when screen is touched, but Activity.onTouchEvent() is not triggered until the screen is touched a second time. At first touch, only the Virtual Keys are shown.
So, I have to trigger my app's UI to pop up on
public void onSystemUiVisibilityChange(int visibility) {
if (visibility == View.SYSTEM_UI_FLAG_VISIBLE) {
// show my APP UI
}
}
but onSystemUiVisibilityChange with View.SYSTEM_UI_FLAG_VISIBLE will be invoked NOT once per touch (3 times on my Galaxy Nexus) by system, especially if the user touches the screen very fast/often.
project lib 4.0 or 4.03.
Samsung galaxy(9250) with 4.03.
Android 4.4 (API Level 19) introduces a new SYSTEM_UI_FLAG_IMMERSIVE flag for setSystemUiVisibility() that lets your app go truly "full screen." This flag, when combined with the SYSTEM_UI_FLAG_HIDE_NAVIGATION and SYSTEM_UI_FLAG_FULLSCREEN flags, hides the navigation and status bars and lets your app capture all touch events on the screen.
This did work for me:
setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
#Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
// show my app UI
}
}
});
What I've done is first imported android.view.GestureDetector so I can use it to detect gestures. Android has a number of default gestures that are automatically detected in the GestureDector class. Most of this info is found here, but below is code in a form that I've used in an actual project that works.
First I've made an anonymous class in my Activity (this can be nested wherever, but I tend to make my anonymous classes at the bottom, right before the closing bracket). NOTE: You can also implement OnGestureListener as part of your class, also.
The code below is for using gesture detection to give a simple hide/show.
I've declared and defined my action bar (my UI, which is initially hidden) as an instance variable, so I can access it here, and wherever else, but you can substitute it for a getActionBar().show() and getActionBar().hide() in the case you don't want to declare it as an instance variable. Substitute your UI in the place of the actionBar here:
public class Example extends ActionBarActivity {
// declared in onCreate() method
private android.support.v7.app.ActionBar actionBar;
private GestureDetectorCompat mDetector;
private YourView view1;
private YourView view2;
private YourView view3;
private YourView view4;
// some other code
class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final String DEBUG_TAG = "Gestures in Example Class";
#Override
public boolean onDoubleTap(MotionEvent event) {
Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString());
// if there is a double tap, show the action bar
actionBar.show();
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
Log.d(DEBUG_TAG, "onSingleTapConfirmed: " + event.toString());
// if the tap is below the action bar, hide the action bar
if (event.getRawY() > getResources().getDimension(R.dimen.abc_action_bar_default_height)) {
actionBar.hide();
return true;
}
return false;
}
#Override
public boolean onDown(MotionEvent event) {
return true;
}
} // end-of-Example Class
Then in my onCreate() I've declared my GestureDetector and also (optionally) set my GestureListeners:
private GestureDetectorCompat mDetector;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// some code here
mDetector = new GestureDetectorCompat(this, new GestureListener());
// this code is for more advanced view logic not needed for a basic set-up
//setGestureListeners();
} // end-of-method onCreate()
Then in order to actually send gestures to be processed we provide the instructions for doing that, there are two ways I know about, first the simplest:
/**
* This method recognizes a touchEvent and passes it to your custom GestureListener
* class.
*/
#Override
public boolean onTouchEvent(MotionEvent event){
this.mDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
The second way is more complex, but if you want to only recognize touch events on certain Views in your layout as in the case where you have overlapping views and can only access the top View, you can create a custom class to pass the event around or up:
class MyOnTouchListener implements View.OnTouchListener {
public boolean onTouch(View v, MotionEvent event) {
if (v.equals(view4)) {
return mDetector.onTouchEvent(event);
} else return false;
}
} // end-of-class MyOnTouchListener
and then use it here:
public void setGestureListeners() {
/* when we return false for any of these onTouch methods
* it means that the the touchEvent is passed onto the next View.
* The order in which touchEvents are sent to are in the order they
* are declared.
*/
view1.setOnTouchListener(new MyOnTouchListener());
view2.setOnTouchListener(new MyOnTouchListener());
view3.setOnTouchListener(new MyOnTouchListener());
view4.setOnTouchListener(new MyOnTouchListener());
} // end-of-method setGestureListeners()
In my setGestureListeners method, I gave them all the same set of commands, that essentially only recognizes touchEvents on view4. Otherwise, it just passes the touchEvent to the next view.
This is code using AppCompat, but if you are not building for older versions, you can use the regular GestureDetector and ActionBar.
Have you tried adding code to only show your UI when the state has changed? You have to maintain the last known visibility and only show your UI when you first come into being visible:
int mLastSystemUiVis;
#Override
public void onSystemUiVisibilityChange(int visibility) {
int diff = mLastSystemUiVis ^ visibility;
mLastSystemUiVis = visibility;
if ((diff&SYSTEM_UI_FLAG_VISIBLE) != 0
&& (visibility&SYSTEM_UI_FLAG_VISIBLE) == 0) {
// DISPLAY YOUR UI
}
}
Code sample adopted from the Android docs
The method Activity.onTouchEvent() gets called at the end of the responder chain (meaning after all other views have had a chance to consume the event). If you tap on a view that is interested in touch (i.e. a Button or EditText) there's a good chance your Activity will never see that event.
If you want to have access to touches before they every get dispatched to your view(s), override Activity.dispatchTouchEvent() instead, which is the method called at the beginning of the event chain:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
//Check the event and do magic here, such as...
if (event.getAction() == MotionEvent.ACTION_DOWN) {
}
//Be careful not to override the return unless necessary
return super.dispatchTouchEvent(event);
}
Beware not to override the return value of this method unless you purposefully want to steal touches from the rest of the views, an unnecessary return true; in this spot will break other touch handling.
I got this problem too, and I found this http://developer.android.com/reference/android/view/View.html#SYSTEM_UI_FLAG_HIDE_NAVIGATION
So, no way to help. Even the android system packaged Gallery app used SYSTEM_UI_FLAG_LOW_PROFILE instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION in photo page view. This is at least what we can do.
I had a very similar issue with trying to update the UI from an onTouchEvent() requiring two touches to work, and I tried a bunch of elaborate stuff before finally getting it to work on the first click.
In my case, I was showing a previously hidden item, then getting its height, then moving a button down by that height. The problem I ran into is that the height was showing as 0 until after the first touch event finished. I was able to solve this by calling show() during ACTION_UP for the onTouchEvent() instead of its ACTION_DOWN. Maybe it'd work if you did something similar?
Try to use:
getWindow().getDecorView().setSystemUiVisibility(View.GONE);
instead:
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
After that you can use normal activity in fullscreen and if you want nav keys you need to swipe from bottom to up. Working for me at Galaxy Tab 2 with android 4.1.2
I am working on a eBook reading app for my company; we use a library that does dynamic reflowing layout of screens to a custom View I provide.
I want a display that lets the user move from one screen to the next by finger swipes. I'm using my own subclass of android.widget.Gallery backed by a custom adapter; the adapter's getView() is responsible for talking to the library and generating a View for each requested page.
My problem is that the Gallery expects to know the total count of Views, and to have an index for its current position in the View array, but the library we use makes it impossible to know that. Because it does dynamic reflow, the total number of 'screens' that comprise the book depends on the screen size of the device, the current font size, screen orientation, etc.--there's no way to know it in advance. We can also jump to ay location in the book; when it does so, there is no way to know how many 'screens' from the start we are (short of returning to the start and advancing a page at a time to the same place), and thus no way to get a position index into the Gallery view.
My current solution is to handle the 'ends' of the Gallery as special conditions in my adapter's getView() call: if it hits the start of the Gallery but I know more pages are available, I force the Gallery to change its current position. Here's an example of PageAdapter.getView():
public View getView(int position, View convertView, ViewGroup parent)
{
...
if( 0 == position ) {
// The adapter thinks we're at screen 0; verify that we really are
int i = 0;
// previousScreen() returns true as long as it could move
// to another screen; after this loop, i will equal the
// number of additional screens before our current position
while( m_book.previousScreen() ) {
i++;
}
PageFlipper pf = (PageFlipper) parent;
// Remember the last REAL position we dealt with.
// The +1 to mActualPosition is a hack--for some reason,
// PageFlipper.leftResync() needs it to work correctly.
m_lastRequestedPosition = i;
pf.mActualPosition = i + 1;
pf.mNeedsLeftResync = true;
// Do a fixup so we're on the right screen
while( i-- > 0 ) {
m_book.nextScreen();
}
}
...
m_view = new PageView(m_book);
return m_view;
}
And here's how it's used in my Gallery subclass:
public class PageFlipper extends Gallery {
...
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Triggers a call to PageAdapter.getView()
super.onScroll(e1, e2, distanceX, distanceY);
// Adapter getView() may have marked us out of sync
this.checkLeftResync();
return true;
}
...
private void checkLeftResync() {
if( mNeedsLeftResync ) {
setSelection(mActualPosition, false);
mActualPosition = 0;
mNeedsLeftResync = false;
}
}
}
However, my solution is unreliable, and feels intuitively wrong. What I really want is something that looks and feels like a Gallery widget, but never tracks any position; instead, it would always ask the Adapter if a new view is available and behave appropriately. Has anyone seen a solution to a problem like this?
BTW, the closest thing I've seen is this project on Google apps, but it appears to expect a static, preallocated set of views.
Thanks in advance for any suggestions!