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!
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 attached to a CursorAdapter and my own custom RecyclerView.OnScrollListener. As the user scrolls to the top and pulls down, all the items are removed, and replaced with newer ones fetched from the network.
The problem is when the number of new items retrieved is small enough to fit on the screen(When it doesn't overflow), my custom onScrollListener, which I use to load the next page of items when scrolling down, is never activated. More specifically it's onScroll method is never called, since well, there aren't enough videos in the list to require needing scrolling down.
To get around this I've tried,
Calling smoothScrollBy(0,0) on the RecyclerView to have my onScroll method called
Calling requestLayout() on the RecyclerView
Both calls I made in onLoadFinished of my Loader after swapping the cursor having the new data, with the old.
Any ideas?
Try to remove your scrollListener and add a view with custom touch listener on top of your recyclerView and check if you need to pull more data from it.
class MyTouchListener implements View.OnTouchListener {
boolean isFirstRowCompletelyVisible;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://user put his finger on the screen.
isFirstRowCompletelyVisible = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0;
break;
case MotionEvent.ACTION_MOVE:// user move his finger on screen.
if (isFirstRowCompletelyVisible) {
//calculate movement and decide if you want to pull new data from your server.
}
break;
case MotionEvent.ACTION_UP:
isFirstRowCompletelyVisible = false;
break;
}
return false;
}
}
Currently I'm using ListView and it's working fine. But I have text in a ListView that is like a paragraph and I just want to show those 2 lines of text and make the rest of the text scrollable, but I'm having an issue that if I make the TextView scrollable inside of the ListView, then the TextView get the focus of its parent (ListView) and won't let it be scrolled.
So can I achieve this scrollable TextView functionality that won't disturb the scrolling property of the ListView?
Thank you.
I was able to do this in the following way:
Into the getView method of the ListAdapter obtain the TextView object of the line, and write
textView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
v.getParent().getParent().requestDisallowInterceptTouchEvent(true);
return false ;
}
});
when you will touch the TextView, take control of the scrolling
What you are trying to achieve really is impossible. How can the OS know that you are trying to scroll the list vs the list row paragraph? You would essentially need to scroll to the bottom of the list row paragraph before the actual list itself could scroll. This is confusing to the user, and not common UX.
I would suggest you look into ExpandableListView. It allows you to have collapsed versions of each row, in your case just 2 lines of text for each list row. When the user taps on the row, it could expand to the full paragraph form, and the list would be scrollable the whole time. There are plenty of tutorials you should be able to find online.
Nothing is Impossible yet way to do things are may b difficult. Directly this thing can`t be achieved but indirectly yes it can be achieved, and yes i achieved.
how did i achieve is a bit complex but yes will share that how did i achieve.
In a ListView when i click on Textview i block the Touch mode of the listView so that their toch method don't intercept each other, and that can be done by using requestDisallowInterceptTouchEvent(true);
this block the TouchListener of the parent (ListView).
Now when click on TextView i allow its touch listener and also setMovementMethod()
but for Movement i made a custom class and Class is Following
public class myScrollMethod extends ScrollingMovementMethod {
#Override
public void onTakeFocus(TextView widget, Spannable text, int dir) {
// TODO Auto-generated method stub
super.onTakeFocus(widget, text, dir);
}
#Override
public boolean onKeyDown(TextView widget, Spannable buffer,
int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
for (int i = 0, scrollAmount = getScrollAmount(widget); i < scrollAmount; i++) {
down(widget, buffer);
}
return true;
case KeyEvent.KEYCODE_DPAD_UP:
for (int i = 0, scrollAmount = getScrollAmount(widget); i < scrollAmount; i++) {
up(widget, buffer);
}
return true;
default:
return super.onKeyDown(widget, buffer, keyCode, event);
}
}
private int getScrollAmount(TextView widget) {
final int visibleLineCount = (int) ((1f * widget.getHeight()) / widget
.getLineHeight());
int scrollAmount = visibleLineCount - 1;
if (scrollAmount < 1) {
scrollAmount = 1;
}
return scrollAmount;
}
}
After that when i click on parent i enable the TouchIntercepter of the parent set true and that get hold on its parent and start scrolling.
By this way i have successfully achieved this requirement
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();
}
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.
...
}
}