I have and android app that has 4 tabs, the first 3 take input from the user and selecting the 4th tab performs some calculations and displays results.
This works fine with the tabs implemented so that I just switch views within the same activity as I can easily access all of the inputed data on the 4th tab.
What I would like to do is switch activities when the tabs are changed. My tab layouts were getting out of control and it is easier having them in separate fies, same with the code.
I would like to save the inputed data from each tab to singleton so I can access it from other activities but onTabChangedListener does not seem to be the way go as the tab has changed, new activity started and view gone already.
How can I perform an action like calling a method that saves user data from the current view when a tab is changed but BEFORE it does it.
what about using onPause() or onStop() methods of the inner activities? you could save your data there.
You could set an OnTouchListener to catch the event and retrieve the index like this:
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
float x = event.getX();
int width = v.getWidth();
int index = (int) (x / (width / tabsCount)); // The amount of tabs.
...
}
}
Related
Please Could You give me a bit of help, I have two activities the main activity (A) and the second activity (B) I made the layout background of activity(B) transparently, so now I can see the components of Activity(A) through Activity(B) until now everything is great, but now I want to access to activity(A) components through Activity(B) that have a transparent background, I used the "setOnTouchListener" to get the touch position but I have no idea how to set this position that I get to the Activity(A) and make it act as I touch it directly
my code
relativeLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN){
float x = (int) motionEvent.getX();
float y = (int) motionEvent.getY();
MainActivity m = new MainActivity();
m.simulateTouch(motionEvent);
}
return true;
}
});
Thank you in advance
I don't think that is possible, because when you are in second activity the first activity is paused and even though you can see the elements of first activity you wont be able to access them.
I suggest you to find another way for doing this.
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
background
I have a viewPager and a tabHost which are used to navigate between pages.
On some cases , the fragment within the viewPager would want to disallow navigating , and even show a dialog to ask the user to verify that the changes that were done in the fragment will be canceled .
If the user confirmed , I would allow navigating , and if not , I would disallow it.
The problem
I need to get notified when the user attempts to navigate to another tab , and (under some conditions i've created) disallow/allow it .
What I've tried
The only functionality i've found is how to disable the switching .
for viewPager , i use :
mViewPager.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(final View v, final MotionEvent event) {
return !mIsTabsSwitchingEnabled;
}
});
and for the tabHost , i use:
mTabHost.getTabWidget().setEnabled(mIsTabsSwitchingEnabled);
However , what i need is quite different - i need to capture the event of starting to navigate , and be able to tell it if i allow it or not .
The question
How should i solve this problem ?
How can i get notified when starting to navigate , and cancel it if needed ?
You should use TabTabHost.OnTabChangeListener
ok , i've solved it for now by using the next code , which i hope that it covers everything .
if anyone can think of another solution that might be better , please write it down .
final OnTouchListener onTouchListener = new OnTouchListener() {
#Override
public boolean onTouch(final View v, final MotionEvent event) {
return isCurrentFragmentNeedTabSwitchingBlocking();
}
};
mViewPager.setOnTouchListener(onTouchListener);
for (int i = 0; i < mTabHost.getTabWidget().getChildCount(); ++i)
mTabHost.getTabWidget().getChildAt(i).setOnTouchListener(tabOnTouchListener);
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!
Okey, so I've implemented a button on a Sliding drawer in a android application I'm building. The only problem is that when I press the button the whole sliding drawer is pressed and it slides up.
I know I can disable 'press to slide up' in the XML, but that does not seem to work as the sliding drawer still is pressed just without the slide up.
If I call the slidingDrawer.lock(); function the button actually works but then the sliding drawer can't slide up or even be pressed up.
Any one have a simple solution to this problem?
If I understand well you have added buttons on your SlidingDrawer handle and you want them to work like buttons when the user press them with keeping a standard SlidingDrawer behaviour when the handle is pressed/dragged?
I just solved a similar problem.
My Handle was looking something like that:
It's composed of two buttons and a center TextView which will be the real handle (reacting as a standard SlidingDrawer handle).
To make the buttons work independently of the SlidingDrawer I changed a bit of source code in the onInterceptTouchEvent method of the standard SlidingDrawer.java class (copy paste the source file from the android code source):
public boolean onInterceptTouchEvent(MotionEvent event) {
//...
final Rect frame = mFrame;
final View handle = mHandle;
// original behaviour
//mHandle.getDrawingRect(frame);
// New code
View trackHandle = mTrackHandle;
// set the rect frame to the mTrackHandle view borders instead of the hole handle view
// getParent() => The right and left are valid, but we need to get the parent top and bottom to have absolute values (in screen)
frame.set(trackHandle.getLeft(), ((ViewGroup) trackHandle.getParent()).getTop(), trackHandle.getRight(), ((ViewGroup) trackHandle.getParent()).getBottom());
if (!mTracking && !frame.contains((int) x, (int) y)) {
return false;
}
//...
}
I also added a setter for the mTrackHandle attribute to set, during the activity creation, the real hanlde to use:
protected void onCreate(Bundle savedInstanceState) {
//...
mSlidingDrawer.setTrackHandle((View) findViewById(R.id.menu_handle_TextView_Title));
//...
}
After that you can set standard listener on your two buttons. They will work like a charm.
in response to Joakim Engstrom:
Yes that's possible!
to do that you have to override onInterceptTouchEvent as follow.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
rect = new Rect(handle.getLeft(), ((View) handle.getParent()).getTop(),
handle.getRight(), ((View) handle.getParent()).getBottom());
if (!rect.contains((int) event.getX(), (int) event.getY())) {
if (event.getAction() == MotionEvent.ACTION_UP)
this.lock();
else
this.unlock();
return false;
} else {
this.unlock();
return super.onInterceptTouchEvent(event);
}
}
you have also to add a setter to set handle to actual handle view during activity creation.
may be this code can help you
https://github.com/xPutnikx/SlidingDrawerWithButtons