visual indication of over scroll in android - android

I am trying to add some visual indication, that there are no more pages in the desired fling direction in the ViewPager. However I am struggling to find a place, where to put relevant code.
I have tried extending ViewPager class with following code, but the Toast is not displaying (ev.getOrientation() returns always 0). I have also tried the same with history points, but ev.getHistorySize() returns also 0.
What am I missing?
Class example:
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* #see android.support.v4.view.ViewPager#onTouchEvent(android.view.MotionEvent)
*/
#Override
public boolean onTouchEvent(MotionEvent ev) {
boolean result = super.onTouchEvent(ev);
switch (ev.getAction() & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
if (ev.getOrientation() > 0) {
Toast.makeText(getContext(), "left", 0).show();
}
}
return result;
}
}

If you look at the v4 support library you will see there's a class used by ViewPager called EdgeEffectCompat (this provides the glow effect when you reach the beginning or end of a view pager in ICS+) If you look at the implementation in the compat library you will see that it has an if-statement to see if the build version is 14+ (ICS) or not. If it is, then it ends up eventually (if you trace long enough) using the normal EdgeEffect class that was inroduced in ICS. Otherwise it uses BaseEdgeEffectImpl which basically has nothing in it.
If you want, you can make your own custom ViewPager that uses EdgeEffect of your own. You can look at the android source code to see how they implemented EdgeEffect here which you can pretty much copy (just make sure to copy the overscroll_edge and overscroll_glow drawables in the AOSP /res/drawable directories to your own project since they are internal to android) or go ahead and create your own version.
Good luck.
(By the way, that's how they create the cool looking edge tilt effect in the launcher menu on ICS... so you can pretty much be as creative as you want with this ;)

I was trying to get the exact same effect that was asked in this question. I struggle with it and then I read #wnafee answer (I couldn't do it with out it).
But then I struggle to implement what was sound pretty simple from the answer.
I had so much trouble with implementing it, that I might didn't understand the answer correctly, but there were too many issues of inaccessible APIs since I wasn't working in the same package of the Compatibility library.
After I tried some approaches (none of them succeeded, and they were pretty complicated) I went to a slightly different direction, and now it works like a charm. I used some reflection, for the ones who never used it, don't worry it is really the basic of reflection.
I'm not sure if it's the best solution out there, but it worked for me, so if you would like to use it you are welcome. Please read Wnafee example since it explains some of the stuff that I did.
In order to accomplish this task you should just follow my three parts solution. (Will take you between 3-10 minutes)
Part I:
As Wnafee said I just made my own EdgeEffect class by copy paste the source code from here,
(just make sure to copy the overscroll_edge and overscroll_glow
drawables in the AOSP /res/drawable directories to your own project
since they are internal to android)
I only did 2 really small changes:
I declare that the class extends EdgeEffectCompat (I called my class EdgeEffectForEarlyVersions). public class EdgeEffectForEarlyVersions extends EdgeEffectCompat. The reason for doing this change is that the mLeftEdge and mRightEdge are of the type EdgeEffectCompat.
At the first line of the constructor of "my" new class I added a call to the parent constructor super(context);. Since there is no default constructor to EdgeEffectCompat you have to Explicitly call the constructor.
Part II
Besides that I wrote the another function. The purpose of the function is that in case of an early version (before ICS) we would like to use the EdgeEffectForEarlyVersions that we just copied. In order to get that purpose I used reflection.
This is the function:
private static void changeEdgeEffectCompactOnEarlyVersions(ViewPager viewPager, Context context)
{
/* In case that the version is earlier than 14 there is only empty implementation for the edge effect, therefore we change it.
* for more information look on the following links:
* 1. http://stackoverflow.com/questions/10773565/visual-indication-of-over-scroll-in-android
* 2. http://grepcode.com/file/repo1.maven.org/maven2/com.google.android/support-v4/r7/android/support/v4/view/ViewPager.java#ViewPager.0mLeftEdge
* 3. http://grepcode.com/file/repo1.maven.org/maven2/com.google.android/support-v4/r7/android/support/v4/widget/EdgeEffectCompat.java#EdgeEffectCompat
*/
if (Build.VERSION.SDK_INT < 14)
{
try
{
Class<ViewPager> viewPagerClass = ViewPager.class;
//Get the left edge field, since it is private we used getDeclaredField and not getDeclared
Field leftEdge = viewPagerClass.getDeclaredField("mLeftEdge");
leftEdge.setAccessible(true);
//Get the right edge field, since it is private we used getDeclaredField and not getDeclared
Field rightEdge = viewPagerClass.getDeclaredField("mRightEdge");
rightEdge.setAccessible(true);
EdgeEffectForEarlyVersions leftEdgeEffect = new EdgeEffectForEarlyVersions(context);
EdgeEffectForEarlyVersions rightEdgeEffect = new EdgeEffectForEarlyVersions(context);
//Set the mLeftEdge memeber of viewPager not to be the default one, but to be "our" edgeEffect
leftEdge.set(viewPager, leftEdgeEffect);
//Set the mRightEdge memeber of viewPager not to be the default one, but to be "our" edgeEffect
rightEdge.set(viewPager, rightEdgeEffect);
}
catch (Exception ex)
{
Log.e("refelection", ex.getMessage());
}
}
}
Part III
Now all there is left to do, is to call that function after you have the ViewPager Instance and nothing more.
I Hope it will help someone.

wnafee explained the solution well but for the lazy among us, i made an actual working implementation quite some time ago.
https://github.com/inovex/ViewPager3D
And if you just want overscroll take a look here:
https://github.com/inovex/ViewPager3D/issues/1

You have a lot of options, you can show a Toast, display a Dialog, make a TextView or image to appear over your UI, etc. Or because you know the amount of View items in the ViewPager, you could add different View at positions 0 and/or n + 1 with the message and make it bounce to the last View that actually contains your data.
You could implement:
viewPager.setOnPageChangeListener(new OnPageChangeListener() {
public void onPageSelected(int position) {
//TODO If position is the 0 or n item, add a view at 0 or at n+1 to indicate there is no more pages with data.
}
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// TODO Show a Toast, View or do anything you want when position = your first/last item;
}
public void onPageScrollStateChanged(int state) {
}
});

just to complement #goBeepit dev answer when you create your own edgeffect class and you extend from EdgeEffectCompat some methods requires to be boolean. you can change those methods to boolean type and make then return true in any case, this way everything works fine

You can overload the setUserVisibleHint(boolean) function in your fragments. Pseudo code:
void setUserVisibleHint(boolean isVisibleToUser) {
// If this fragment is becoming visible
if (isVisibleToUser == true) {
// Check if it is the last fragment in the viewpager
if (indexOfThis == getActivity().indexOfLast) {
// Display right limit reached
Toast(..., "No more Frags to right",...)
}
// Check if it is the first fragment in the viewpager
else if (indexOfThis == getActivity().indexOfFirst) {
// Display Left Limit reached
Toast(..., "No more Frags to left",...)
}
}
}
I have not used this function for this purpose, but have used it for other reasons and it does fire appropriately. Hope this helps...

I've implemented a bounce back effect based on Renard's ViewPager3D: https://stackoverflow.com/a/17425468/973379

Usually with ViewPager, one uses a PagerAdapter such as FragmentPagerAdapter or FragmentStatePagerAdapter to flood the ViewPager with contents(your content are going to be views).
Now, when you use a PagerAdapter, you have one method called getCount(), http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html#getCount%28%29 ,which will give you the size of the content.
Since you now, know the size you can easily display a message with an if control statement.
Try this code : http://developer.android.com/reference/android/support/v4/view/ViewPager.html
Note: I dont think you need a custom ViewPager. You will also need to understand Fragments for ViewPager. Look at samples in ApiDemos. Its a great source.

Related

ViewPager2 default position

I'm creating a slideshow with ViewPager2. For example, the slideshow has 3 items and I want to show the second item when the activity opens. I use setCurrentItem(int item, boolean smoothScroll) method but it doesn't work and nothing happens. How can I achieve it?
viewPager.adapter = adapter
viewPager.setCurrentItem(1, true)
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
setCurrentItem(int item, boolean smoothScroll) works correctly in ViewPager but in ViewPager2 it does not work as expected. Finally, I faced this problem by adding setCurrentItem(int item, boolean smoothScroll) method into a delay like this:
Handler().postDelayed({
view.viewPager.setCurrentItem(startPosition, false)
}, 100)
Do not use timers, you will run into a lot of probable states in which the user has a slow phone and it actually takes a lot longer than 100 ms to run, also, you wouldn't want too slow of a timer making it ridiculously un-reliable.
Below we do the following, we set a listener to our ViewTreeObserver and wait until a set number of children have been laid out in our ViewPager2's RecyclerView (it's inner working). Once we are sure x number of items have been laid out, we start our no-animation scroll to start at the position.
val recyclerView = (Your ViewPager2).getChildAt(0)
recyclerView.apply {
val itemCount = adapter?.itemCount ?: 0
if(itemCount >= #(Position you want to scroll to)) {
viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// False for without animation scroll
(Your ViewPager2).scrollToPosition(#PositionToStartAt, false)
}
}
}
First off, I think that the accepted answer shouldn't be #hosseinAmini 's, since it's suggesting to use a delay to work around the problem. You should first be looking for what the assumed bug is caused by, rather than trusting unreasonable solutions like that.
#Rune's proposal is correct, instead; so I'm quoting their code in my answer:
viewPager.post {
viewPager.setCurrentItem(1, true)
}
The only thing I'd argue about is the aforementioned one's belief that their solution is just deferring the execution of that lambda in the next run cycle. This wouldn't make anything buggy work properly. Rather, what it is actually being done is deferring the execution of that lambda to once the view has been attached to a window, which implies it's also been added to a parent view. Indeed, there looks to be an issue as to changing the current ViewPager2 item before being attached to a window. Some evidence to support this claim follows:
Using whichever Handler won't work nearly as effectively.
Handler(Looper.getMainLooper()).post {
viewPager.setCurrentItem(1, true) // Not working properly
}
From a theoretical standpoint, it might incidentally work due to the ViewPager2 being attached to a window acquiring priority in the message queue of the main looper, but this shouldn't ever be relied upon as there's just no guarantee that it'll work (it's even more likely it won't) and if it even turned out to be working, further investigation running multiple tests should make my point clear.
View.handler gets null, which means the view hasn't been attached to any window yet.
View.handler // = null
Despite Android UI being tied to the main looper, which will always uniquely correspond to the main thread –hence also called the UI thread,– a weird design choice stands in the handler not being associated to each view until they get attached to a window. A reason why this may lay on the consequent inability of views to schedule any work on the main thread while they're not part of the hierarchy, which may turn useful when implementing a view controller that schedules view updates while unaware of their lifecycle (in which case it would employ the View's handler, if any — or just skip scheduling whatever it was going to if none).
EDIT:
Also, #josias has pointed out in a comment that it'd be clearer to use:
viewPager.doOnAttach {
viewPager.setCurrentItem(1, true)
}
Thanks for that suggestion! It expresses better the actual intent, rather than relying on the behavior of the View.post method.
Do not use timers and all that stuff with 'post', it's not the reliable solution and just a piece of code that smells.
Instead, try use viewPager.setCurrentItem(1, false). That 'false' is about smoothScroll, you can't smooth scroll your viewPager2 when your activity is just opened. Tested it on a fragment in onViewCreated() method, it also didn't work with "true", but works with "false"
As it was mentioned above you have to use setCurrentItem(position, smoothScroll) method on ViewPager2 in order to show selected item. To make it work you have to define a callback, here is an example:
ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
};
And then you have to register it as follow:
viewPager.registerOnPageChangeCallback(callback);
Also do not forget to unregister it:
viewPager.unregisterOnPageChangeCallback(callback);
When you call setCurrentItem(position) method it will call onPageSelected(int position) method from your callback passing your argument, and then method createFragment(int position) from FragmentStateAdapter class will be called to show your fragment.
I tried changing viewpager2 page in Handler().dely() and viewPager2.post{} and even 'viewPager2.get(0).post all didn't work for me, I'm using ViewPager with FragmentStateAdapter with Tablayout.
What worked for me is changing the position of the RecylerView in ViewPager2 after binding FragmentStateAdapter to yourViewPager2View.adapter manually:
(yourViewPager2View[0] as RecyclerView).scrollToPosition(moveToTabNumber)
Why
My problem is onCreateFragment(position:Int):Fragmeet function in FragmentStateAdapter always starting fragment at 0 position no matter what pageNumber I set the page
viewPager.setCurrentItem = pageNumber
I checked where it's called in FragmentStateAdapter it's called in FragmentStateAdapter:
onBindViewHolder(final #NonNull FragmentViewHolder holder, int position)`
so all I needed is to force onBindViewHolder to call onCreateFragment(position:Int) with the page number I wanted.
mViewPager.setCurrentItem(1, true); ---> this is sufficient as you written above
That should work,
in doubt, just check your position:
#Override
public void onPageSelected(int i) {
if (LOG_DEBUG) Log.v(TAG, " ++++++++ onPageSelected: " + i);
mViewPager.setCurrentItem(i);
//TODO You can use this position: to write other dependent logic
}
and also check
getItem(int position) in PagerAdapter
or else paste your code.
I noticed that it works fine when the view is initially created if you opt to not animate it.
viewPager2.setCurrentItem(index, false)
This is usually fine depending on your use case - this initial/default item probably doesn't need to be animated in.
I met the same problem. In my case, I make the viewPager2 Gone by default until network requests succeed, I fix it by setting the CurrentItem after I make the viewPager2 visible.
My answer may not be helpful now but i see no harm to post my expreince, i just came to this problem using ViewPager and ViewPager2 and unexpectedly solved it by just changing some line codes order.
Here is (java) solution for ViewPager:
reviewWordViewPager.addOnPageChangeListener(changeListener);
reviewWordViewPager.setCurrentItem(viewPosition, true/false);
reviewWordTabIndicator.setupWithViewPager(reviewWordViewPager, true);
(Java) solution for ViewPager2:
wordViewPager.registerOnPageChangeCallback(viewPager2OnPageChangeCallback);
wordViewPager.setCurrentItem(vpPosition, true/false);
new TabLayoutMediator(tabIndicator, wordViewPager,
((tab, position) -> tab.setText(viewPagerTitle[position]))).attach();
I did not look up for ViewPager2 whether it needs the following old code used in ViewPager
#Override
public int getItemPosition(#NonNull Object object) {
// refresh all fragments when data set changed
return POSITION_NONE;
}
But surprisingly no need for it in ViewPager2 to solve the problem i've been having, hope it helps others
In case you use context.startActivity to start new activities no need to use wordViewPager.setCurrentItem(item, smoothScroll) in your onResume function to get back to the last selected tab before you started new activity you just save ViewPager/ViewPager2 position like vpPisition = wordViewPager.getCurrentItem(); in onStop function.
vpPisition is a global variable.
as #Daniel Kim but a java version
RecyclerView rvOfViewPager2 = (RecyclerView) viewPager2.getChildAt(0);
rvOfViewPager2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
rvOfViewPager2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
viewPager2.setCurrentItem(currentTabId, false);
}
});
First You need to Initilaze the Main activity under any listener or button You want then After that You need to put this Line..
here MainActvity is the Viewpager Main Class You are using and and 2 is the position where you want to move
MainActivity main = (MainActivity ) mContext;
main.selectTab(2, true);

Get ViewPager height and width in pixels only once inside Fragment

I know there are a lot of similar questions, but suggested solutions don't work in my case.
What is my problem.
I have fragment nested inside activity.
Fragment layout includes ViewPager.
What is my idea ? As far as my application won't support landscape (this can cause some additional changes), my ViewPager is loading images from the network.Images can be quietly large and heavy, so I have written server side API which helps to convert image to the desired size on the server and return resized image url.
This will help to get rid of OutOfMemory errors.
But the problem is that I need to send ViewPager's height and width in my request.
I am finding it by id in onViewCreated
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBannerSlider = (AutoScrollViewPager) view.findViewById(R.id.banner_slider);
mBannerSlider.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener(){
#Override
public void onGlobalLayout() {
mSliderHeight = mBannerSlider.getHeight();
mSliderWidth = mBannerSlider.getWidth();
Toast.makeText(getActivity(),"View " + mSliderHeight + " " + mSliderWidth,Toast.LENGTH_SHORT).show();
}
requestBannerImages(mSliderHeight........);
});
The problem here is that this method is called on every page change.
Of course I can use some helper variable to determine if it is first time or not.
Also there is way to send callback from activity to the fragment using this activitiy's method.
#Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
What is the best solution in this way, maybe I can do this better in another way.
Thanks in advance.
EDIT
I have a plenty of view where I need to get dimensions, so I need to add listener to every view and that after first measurement remove it ?
I would do it with the tree observer. To avoid repeated calls I would remove the listeners once i get the value. This assures you that you will get the dimensions and it wont get call anymore.
if (Build.VERSION.SDK_INT < 16) {
v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
} else {
v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
}
I took the code from: https://stackoverflow.com/a/16190337/2051397
I understand your dilemma, mainly because I have implemented a multimedia app. However setting dimensions in every view or many UI elements related to icons/images is time consuming. If not so, then you end up with lots of code doing the same stuff.
In my opinion, you don't have to set dimensions for every view, for the sake of looks. You may set max height of a view/UI or/and set setAdjustViewBound to true, let the UI framework handle the images. There is a cost of performance though if the image is large, in your case probably not.
You may refer to Google notes on setMaxHeight of ImageView. Let me know of your view on this, interested to know.

Displaying settings internally represented with sections in Android Listview

I'm writing an application for iOS and Android in parallell and I am facing a small problem.
I am displaying a list of settings to the user and the settings data is internally represented in settings for section, like this:
Section
Section object
Section object
Section
Section object
etc.
In iOS, when the user clicks a setting object, or when the system wants to paint the view for it, it calls a method with an NSIndexPath object. For example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
and the sections are handled automatically.
In Android, the listviews are "flattened", lacking better terminology. So that a method is called only with a row index:
public Object getItem(int position);
Now it is actually a quite hard problem to solve trying to represet sections directly in the Adapter (MySettingsAdapter extends BaseAdapter).
Right now this problem is solved by flattening the entire underlaying data structure, but it's a lot of duplicated code for almost nothing. The nicer solution I can think of is to do something like the following in my MySettingsAdapter:
class MySettingsAdapter extends BaseAdapter {
MyInternalDataStructure settingsData;
int sections;
int rowsForSection[sections];
public MySettingsAdapter (MyInternalDataStructure settingsData) {
this.settingsData = settingsData;
this.sections = settingsData.sections;
for (int i = 0; i < sections; i++) {
rowsForSection[i] = settingsData.settingsInSection[i].size();
}
}
#Override
public Object getItem(int position) {
int sectionFromPostion;
int rowFromPosition;
// Calculate section and row here...
return settingsData.getSetting(sectionFromPostion, rowFromPosition);
}
}
And I just can't get the calculations for sectionFromPostion and rowFromPostion right...
Unfortunately Android does not have quite the same ability in regards to sections as you can achieve with the UITableView. However Android does provide a solution, the ExpandableListView. While similar to a ListView it works a bit differently and interacts with a different type of adapter.
Android provides the SimpleExpandableListAdapter that you can use with the ExpandableListView. I'll warn you now. It's clunky and pretty restricting. Additionally, it requires you to organize your data into a List of Maps which in itself can be a pain to do.
Alternatively, you can create your own adapter for the ExpandableListView by implementing the BaseExpandableListAdapter. It's very similar to implementing the BaseAdapter. It just has a few extra bells and whistles to support a tier like structure.
Basically all these Expandable...[foo] classes introduce the idea of having a group (the section) and children (the data under a section). Instead of having an index to the data in your adapter, you'll have a groupPosition and a childPosition. Meanwhile the ExpandableListView has this sorta murky middle notion of positions as it works with group/child positions, packed positions, and flattened positions.
As a side note. Depending on how your data is organized, I'd suggest checking out the Rolodex Adapters found in this 3rd party library. They are meant to make working with ExpandableListViews easier and has plenty of code examples and a demo app to help get you going.

FragmentPagerAdapter with ViewPager and two Fragments. Go to the first from the second and update first's text

I'm not familiar with FragmentPagerAdapter, so this is going to be one of those questions that we (you) read the description critically.
Structure: I have a FragmentPagerAdapter (code below), that will hold two fragments at a time. The first displays book excerpts, and the second a list of book titles.
Goal: I want to achieve what is described in the title: the user can navigate to the second fragment in the pager, click on a title, and then I want to move the user back to the first fragment and tell the first fragment to update the text. The first fragment has a triggerRefresh method for that.
Code: I believe my problem happens because of the way FragmentPagerAdapter reuses/creates the Fragments (which I don't understand). This is my class:
static class MyFragmentPagerAdapter extends FragmentPagerAdapter {
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
switch(position) {
case 0:
return new ExcerptsFragment();
case 1:
return new BookListFragment();
default:
throw new IllegalArgumentException("not this many fragments: " + position);
}
}
}
This is how I created the relevant members:
ViewPager mViewPager = (ViewPager) findViewById(R.id.pager);
MyFragmentPagerAdapter mFragmentPagerAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mFragmentPagerAdapter);
And this is what I've tried elsewhere in my Activity, when I receive the callback from the book titles Fragment with the title selected:
mViewPager.setCurrentItem(0); // back to excerpts screen page. It's OK.
// Here's the problem! How to identify the fragment 0
// to ExcerptsFragment and call its triggerRefresh()?!?
Series of problems:
Calling the adapter's getView() won't work because it will return a new instance of ExcerptsFragment, which is not the one currently attached (as expected, throws the exception).
I've seen many people here (example) just storing fragments in the getView(). Is that right? Because by looking at the official examples, seems like an anti-pattern to me (defeat the automatic reference by holding the items). And that is also the opinion here and here (and looks right to me).
Any suggestions? I wouldn't be surprised if I'm not understanding all of this one bit...
Disclaimer: Although this had worked perfectly fine for me before, you should be aware of the classic pitfalls of depending on internal, private behavior. While I wrote tests that would eventually warn me if the internal implementation changed, I have since moved on to greener pastures. And you should, too. As such, the value of this question and its answer is only historical, in my opinion.
Sorry about that question, I think it was the hour.
To solve that problem, I implemented this solution as is. Seems to work just fine. So, I believe it was just a matter of finding the (currently attached) fragment instance by figuring out how its Id is named. The link above explains how it's made.
I opted to answer my own question instead of deleting it because I believe novices like me on these pagers will benefit from a "real case scenario". Most of the answers I've seen talk most about the theory, which is the right way BTW... but without a real example to work on sometimes people like me get lost.
Anyway, here is the last piece of code that I needed (the commented part above):
int n = 0;
mViewPager.setCurrentItem(n); // in the question I had stopped here.
ExcerptsFragment f = (ExcerptsFragment) ContainerActivity.this
.getSupportFragmentManager().findFragmentByTag(getFragmentTag(n));
f.triggerRefresh();
// ... below the helper method: used the solution from the link.
private String getFragmentTag(int pos){
return "android:switcher:"+R.id.pager+":"+pos;
}
So, I'm having a feeling that this is a robust solution, because I'm not holding any references to fragments (thus risking the references being outdated). I kept my own code at a minimum, therefore minimizing the chances of me doing something stupid.
Of course, if you have something to add, to show us, to tell what is wrong in doing it or what can be improved, I'll be glad to hear from you.
I searched for a solution to this problem a while myself. Your approach in principle works, but it will break your code if ever the code of the fragment tag creation in the Android base class implementation changes. This is a quite nasty dependency!
A more elegant approach would be to turn the problem around and keep an instance of your base activity in your fragment. Implement a setter for the tag in your activity and call that inside the fragment upon creation - the tag there is simply available with getTag().
An example implementation can be found here.
I solved this problem by using WeakReferences to the fragments upon creation. See : https://stackoverflow.com/a/23843743/734151
If you find anything wrong with this approach, please comment.

How to use Android Spinner like a drop-down list

It's taken me quite a while to get my head around the Android Spinner. After several failed implementation attempts, and after reading many questions partially similar to my own but without satisfactory answers, and some without any answers at all, e.g. here and here, I finally get that a "spinner" in Android isn't meant to be the same thing as a "drop-down list" from desktop apps, or a select in HTML. However, what my app (and I'm guessing the apps of all the other posters whose questions are similar) needs is something that works like a drop-down box, not like a spinner.
My two problems are with what I first considered to be idiosynchrasies the OnItemSelectedListener (I've seen these as separate questions on this site but not as one):
An initial selection of the first list item is triggered automatically without the user's interaction.
When the item that was already selected is selected again by the user, it is ignored.
Now I realise that, when you think about it, it makes sense for this to happen on a spinner - it has to start with a default value selected, and you spin it only to change that value, not to "re-select" a value - the documentation actually says: "This callback is invoked only when the newly selected position is different from the previously selected position". And I've seen answers suggesting that you set up a flag to ignore the first automatic selection - I guess I could live with that if there's no other way.
But since what I really want is a drop-down list which behaves as a drop-down list should (and as users can and should expect), what I need is something like a Spinner that behaves like a drop-down, like a combo-box. I don't care about any automatic pre-selection (that should happen without triggering my listener), and I want to know about every selection, even if it's the same one as previously (after all, the user selected the same item again).
So... is there something in Android that can do that, or some workaround to make a Spinner behave like a drop-down list? If there is a question like this one on this site that I haven't found, and which has a satisfactory answer, please let me know (in which case I sincerely apologise for repeating the question).
+1 to David's answer. However, here's an implementation suggestion that does not involve copy-pasting code from the source (which, by the way, looks exactly the same as David posted in 2.3 as well):
#Override
void setSelectionInt(int position, boolean animate) {
mOldSelectedPosition = INVALID_POSITION;
super.setSelectionInt(position, animate);
}
This way you'll trick the parent method into thinking it's a new position every time.
Alternatively, you could try setting the position to invalid when the spinner is clicked and setting it back in onNothingSelected. This is not as nice, because the user will not see what item is selected while the dialog is up.
Ok, I think I've come up with a solution for my own situation with the help of both David's and Felix' answer (I believe David's helped Felix', which in turn helped mine). I thought I'd post it here together with a code sample in case someone else finds this approach useful as well. It also solves both of my problems (both the unwanted automatic selection and the desired re-selection trigger).
What I've done is added a "please select" dummy item as the first item in my list (initially just to get around the automatic selection problem so that I could ignore when it was selected without user interaction), and then, when another item is selected and I've handled the selection, I simply reset the spinner to the dummy item (which gets ignored). Come to think of it, I should've thought of this long ago before deciding to post my question on this site, but things are always more obvious in hindsight... and I found that writing my question actually helped me to think about what I wanted to achieve.
Obviously, if having a dummy item doesn't fit your situation, this might not be the ideal solution for you, but since what I wanted was to trigger an action when the user selected a value (and having the value remain selected is not required in my specific case), this works just fine. I'll try to add a simplified code example (may not compile as is, I've ripped out a few bits from my working code and renamed things before pasting, but hopefully you'll get the idea) below.
First, the list activity (in my case) containing the spinner, let's call it MyListActivity:
public class MyListActivity extends ListActivity {
private Spinner mySpinner;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: other code as required...
mySpinner = (Spinner) findViewById(R.id.mySpinner);
mySpinner.setAdapter(new MySpinnerAdapter(this));
mySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> aParentView,
View aView, int aPosition, long anId) {
if (aPosition == 0) {
Log.d(getClass().getName(), "Ignoring selection of dummy list item...");
} else {
Log.d(getClass().getName(), "Handling selection of actual list item...");
// TODO: insert code to handle selection
resetSelection();
}
}
#Override
public void onNothingSelected(AdapterView<?> anAdapterView) {
// do nothing
}
});
}
/**
* Reset the filter spinner selection to 0 - which is ignored in
* onItemSelected() - so that a subsequent selection of another item is
* triggered, regardless of whether it's the same item that was selected
* previously.
*/
protected void resetSelection() {
Log.d(getClass().getName(), "Resetting selection to 0 (i.e. 'please select' item).");
mySpinner.setSelection(0);
}
}
And the spinner adapter code could look something like this (could in fact be an inner class in the above list activity if you prefer):
public class MySpinnerAdapter extends BaseAdapter implements SpinnerAdapter {
private List<MyListItem> items; // replace MyListItem with your model object type
private Context context;
public MySpinnerAdapter(Context aContext) {
context = aContext;
items = new ArrayList<MyListItem>();
items.add(null); // add first dummy item - selection of this will be ignored
// TODO: add other items;
}
#Override
public int getCount() {
return items.size();
}
#Override
public Object getItem(int aPosition) {
return items.get(aPosition);
}
#Override
public long getItemId(int aPosition) {
return aPosition;
}
#Override
public View getView(int aPosition, View aView, ViewGroup aParent) {
TextView text = new TextView(context);
if (aPosition == 0) {
text.setText("-- Please select --"); // text for first dummy item
} else {
text.setText(items.get(aPosition).toString());
// or use whatever model attribute you'd like displayed instead of toString()
}
return text;
}
}
I guess (haven't tried this) the same effect could be achieved using setSelected(false) instead of setSelection(0), but re-setting to "please select" suits my purposes fine. And, "look, Ma, no flag!" (Although I guess ignoring 0 selections is not that dissimilar.)
Hopefully, this can help someone else out there with a similar use case. :-) For other use cases, Felix' answer may be more suitable (thanks Felix!).
Look. I don't know if this will help you, but since you seem tired of looking for an answer without much success, this idea may help you, who knows...
The Spinner class is derived from AbsSpinner. Inside this, there is this method:
void setSelectionInt(int position, boolean animate) {
if (position != mOldSelectedPosition) {
mBlockLayoutRequests = true;
int delta = position - mSelectedPosition;
setNextSelectedPositionInt(position);
layout(delta, animate);
mBlockLayoutRequests = false;
}
}
This is AFAIK taken from 1.5 source. Perhaps you could check that source, see how Spinner/AbsSpinner works, and maybe extend that class just enough to catch the proper method and not check if position != mOldSelectedPosition.
I mean... that's a huge "maybe" with a lot of "ifs" (android versioning comes to mind etc.), but since you seem frustrated (and I've been there with Android many times), maybe this can give you some "light". And I assume that there are no other obvious answers by looking at your previous research.
I wish you good luck!
Here is an alternative solution to differentiate between any (intended or unintended) programmatic and user-initiated changes:
Create your listener for the spinner as both an OnTouchListener and OnItemSelectedListener
public class SpinnerInteractionListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {
boolean userSelect = false;
#Override
public boolean onTouch(View v, MotionEvent event) {
userSelect = true;
return false;
}
#Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
if (userSelect) {
// Your selection handling code here
userSelect = false;
}
}
}
Add the listener to the spinner registering for both event types
SpinnerInteractionListener listener = new SpinnerInteractionListener();
mSpinnerView.setOnTouchListener(listener);
mSpinnerView.setOnItemSelectedListener(listener);
This wouldn't handle the case in which the re-selection of the same item by the user doesn't trigger the onItemSelected method (which I have not observed), but I guess that could be handled by adding some code to the onTouch method.
Anyway, the problems Amos pointed out were driving me crazy before thinking of this solution, so I thought I'd share as widely as possible. There are many threads that discuss this, but I've only seen one other solution so far that is similar to this: https://stackoverflow.com/a/25070696/4556980.
Modifying the Spinner is useful if you want to have multiple selections simultaneously in the same activity.
If you only desire the user to have a hierarchical selection, for example:
What do you want to eat?
Fruit
Apples
Bananas
Oranges
Fast Food
Burgers
Fries
Hot dogs,
then the ExpandableListView might be better for you. It allows the user to navigate a hierarchy of different groups and choose a child element. This would be similar to having several Spinners for the user to choose from - if you do not desire a simultaneous selection, that is.
I worked through several of the issues mentioned in this thread before I realized that the PopupMenu widget is what I really wanted. That was easy to implement without the hacks and workarounds needed to change the functionality of a Spinner. PopupMenu was relatively new when this thread was started in 2011, but I hope this helps someone searching for similar functionality now.

Categories

Resources