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
Related
I've implemented Dave Smith's elegant solution to displaying multiple views inside a ViewPager here, but am having trouble dispatching touch events to the fragments that are not the "focused" one.
In his PagerContainer solution, there is functionality to handle the touch events outside of the ViewPager's focused area (see below), but that's only to enable scrolling. I need those touch events to actually interact with the views on the fragments themselves.
Does anyone have any experience with this?
#Override
public boolean onTouchEvent(MotionEvent ev) {
//We capture any touches not already handled by the ViewPager
// to implement scrolling from a touch outside the pager bounds.
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialTouch.x = (int)ev.getX();
mInitialTouch.y = (int)ev.getY();
default:
ev.offsetLocation(mCenter.x - mInitialTouch.x, mCenter.y - mInitialTouch.y);
break;
}
return mPager.dispatchTouchEvent(ev);
}
How do I get the touch events from the PagerContainer propagated to the appropriate fragment?
You can use Event bus for this purpose checkout this link how to use it.I have used it for this kind of communications between activities and fragments. https://github.com/greenrobot/EventBus
So, I've got a partial solution implemented but it's not the final answer. In the OnTouchEvent overridden in the PagerContainer, I'm now checking the HitRect of each fragment and determining what fragment the event's point is inside. The problem now? How to stop scrolling to the fragment that's been selected.
Here's what I've tried, but it's still scrolling to the clicked on fragment. Any ideas on how to stop this?
#Override
public boolean onTouchEvent(MotionEvent evt) {
//We capture any touches not already handled by the ViewPager
// to implement scrolling from a touch outside the pager bounds.
int fragCount = mPager.getAdapter().getCount();
for(int i = 0; i < fragCount; i++)
{
View view = mPager.getChildAt(i);
Rect rect = new Rect();
view.getHitRect(rect);
if(rect.contains((int)evt.getX(),(int)evt.getY()))
{
int currentItem = mPager.getCurrentItem();
if(currentItem != i)
{
mPager.clearOnPageChangeListeners();
mPager.setCurrentItem(currentItem + i, false);
mPager.addOnPageChangeListener(this);
}
break;
}
}
return mPager.dispatchTouchEvent(evt);
}
If anyone stumbles on this question and is looking for a solution, I ended up using HorizontalGridView instead of ViewPager...
Javadoc for HorizontalGridView here
HorizontalGridView Sample
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 have a scrollview with a lot of content. Now when user do a fling or scroll down, I want the scrollview to stop at a particular view location, where I am doing some animation, and then user can again fling or scroll down.
I have tried the disabling of scrollview as mentioned here but It only disables when the scrollview completely stops and cannot stop in the middle of a fling.
Is there any way I can stop a fling of the scrollview when a certain view location or a certain y value is reached?
I had the same problem my solution was.
listView.smoothScrollBy(0,0)
This will stop the scrolling.
To stop a fling at a particular point simply call
fling(0)
If you are only concerned about flings this is the most logical way to do so in my opinion, because velosityYis set to 0 and thereby the fling is stopped immediately.
Here is the javadoc of the fling method:
/**
* Fling the scroll view
*
* #param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/cursor is moving down the screen,
* which means we want to scroll towards the top.
*/
One approach may be to use smoothScrollToPosition, which stops any existing scrolling motion. Note this method requires API level >= 8 (Android 2.2, Froyo).
Note that if the current position is far away from the desired position, then the smooth scrolling will take quite a long time and look a bit jerky (at least in my testing on Android 4.4 KitKat). I also found that a combination of calling setSelection and smoothScrollToPosition could sometimes causes the position to "miss" slightly, this seems to happen only when the current position was very close to the desired position.
In my case, I wanted my list to jump to the top (position=0) when the user pressed a button (this is slightly different from your use case, so you will need to adapt this to your needs).
I used the following method to
private void smartScrollToPosition(ListView listView, int desiredPosition) {
// If we are far away from the desired position, jump closer and then smooth scroll
// Note: we implement this ourselves because smoothScrollToPositionFromTop
// requires API 11, and it is slow and janky if the scroll distance is large,
// and smoothScrollToPosition takes too long if the scroll distance is large.
// Jumping close and scrolling the remaining distance gives a good compromise.
int currentPosition = listView.getFirstVisiblePosition();
int maxScrollDistance = 10;
if (currentPosition - desiredPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition + maxScrollDistance);
} else if (desiredPosition - currentPosition >= maxScrollDistance) {
listView.setSelection(desiredPosition - maxScrollDistance);
}
listView.smoothScrollToPosition(desiredPosition); // requires API 8
}
In my action handler for the button I then called this as follows
case R.id.action_go_to_today:
ListView listView = (ListView) findViewById(R.id.lessonsListView);
smartScrollToPosition(listView, 0); // scroll to top
return true;
The above does not directly answer your question, but if you can detect when the current position is at or near your desired position, then maybe you could use smoothScrollToPosition to stop the scrolling.
You need to disable ScrollView handling of the fling operation. To do this simply override the fling method in ScrollView and comment super.fling(). Let me know if this works !
public class CustomScrollView extends ScrollView {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
return false;
}
#Override
public void fling (int velocityY)
{
/*Scroll view is no longer gonna handle scroll velocity.
* super.fling(velocityY);
*/
}
}
I'm trying to achieve a similar situation. I have a partial solution:
I've managed to stop the ScrollView at a particular y coordinate by overriding onScrollChanged and then calling smoothScrollTo. I allow the user to continue scrolling down past that point by keeping a boolean that indicates if this was the first fling/drag or not. And the same goes for scrolling from bottom to top - I stop the scroll at the same point again. And then allow the user to continue scrolling upward again.
The code looks something like this:
boolean beenThereFromTop = false;
boolean beenThereFromBottom = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
TextView whereToStop = (TextView) findViewById(R.id.whereToStop);
final int y = whereToStop.getBottom();
int scrollY = scrollView.getScrollY();
// manage scrolling from top to bottom
if (scrollY > y) {
if (!beenThereFromTop) {
beenThereFromTop = true;
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, y);
}
});
}
}
if (scrollY < y && beenThereFromTop) {
beenThereFromTop = false;
}
// manage scrolling from bottom to top
if (scrollY < y) {
if (!beenThereFromBottom) {
beenThereFromBottom = true;
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, y);
}
});
}
}
if (scrollY > y && beenThereFromBottom) {
beenThereFromBottom = false;
}
}
});
}
Well im actualy using this method:
list.post(new Runnable()
{
#Override
public void run()
{
list.smoothScrollToPositionFromTop(arg2, 150);
}
});
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
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();
}