I have a screen which uses ViewPager + actionbar Sherlock tabs. I have a setOnPageChangeListener set on the pager and it does the following:
#Override
public void onPageSelected(final int position) {
actionBar.setSelectedNavigationItem(position);
}
This works just fine in portrait mode and even in landscape if I only have few tabs and all the tabs are displayed. However if I add few more tabs in landscape, these collapse into a single drop-down widget. When I page through the ViewPager the setSelectedNavigationItem method is executed but now it has no effect on the drop-down selection: it stays at the last selected value. Which is really bad since user is missing a visual clue: tab may say "One" but user is already on page #6.
Is there a way to programmatically change which tab to display based on the position?
P.S. I know why this happens:
Here's code from com.android.internal.app.ActionBarImpl:
public void setSelectedNavigationItem(int position) {
switch (mActionView.getNavigationMode()) {
case NAVIGATION_MODE_TABS:
selectTab(mTabs.get(position));
break;
case NAVIGATION_MODE_LIST:
mActionView.setDropdownSelectedPosition(position);
break;
default:
throw new IllegalStateException(
"setSelectedNavigationIndex not valid for current navigation mode");
}
}
And when I step through that I can see that the navigation mode is still NAVIGATION_MODE_TABS though tabs are displayed as list. Now - my knee jerk reaction is to put code into onConfigurationChanged to set navigation mode appropriately but shouldn't this happen automatically?
P.P.S. And there's Android bug filed for it already that contains the patch
Base on the Android bug I'm referencing in the question (see P.P.S.) here's workaround that indeed works. Just add it to your pager
mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener()
{
#Override
public void onPageSelected(int position)
{
actionBar.getTabAt(position).select();
ViewParent root = findViewById(android.R.id.content).getParent();
findAndUpdateSpinner(root, position);
}
/**
* Searches the view hierarchy excluding the content view
* for a possible Spinner in the ActionBar.
*
* #param root The parent of the content view
* #param position The position that should be selected
* #return if the spinner was found and adjusted
*/
private boolean findAndUpdateSpinner(Object root, int position)
{
if (root instanceof android.widget.Spinner)
{
// Found the Spinner
Spinner spinner = (Spinner) root;
spinner.setSelection(position);
return true;
}
else if (root instanceof ViewGroup)
{
ViewGroup group = (ViewGroup) root;
if (group.getId() != android.R.id.content)
{
// Found a container that isn't the container holding our screen layout
for (int i = 0; i < group.getChildCount(); i++)
{
if (findAndUpdateSpinner(group.getChildAt(i), position))
{
// Found and done searching the View tree
return true;
}
}
}
}
// Nothing found
return false;
}
});
You need to add some code in the ActionBarSherlock library project in file ( ActionBarImpl.java ) see this link
Related
I have created 30 scrollable tabs using tablayout.
So first three tabs are visible on screen and rest of them are invisible which can be scroll using swipe gesture.
The problem is when I am selecting last tab programmatically but it is not get visible (tab layout not get scrolled to last tab).
How can I make tablayout to scroll to last tab?
I found the solution.
First I had found the width of tablayout and scroll it's x position to width and than called the select() method of last tab.
And it works fine.
Below is the code.
mTabLayout.setScrollX(mTabLayout.getWidth());
mTabLayout.getTabAt(lastTabIndex).select();
Updated:
If above is not working you can use the below code as well, it is also working fine.
new Handler().postDelayed(
new Runnable() {
#Override public void run() {
mTabLayout.getTabAt(TAB_NUMBER).select();
}
}, 100);
write this method in your custom tablayout (Your own layout which extends tablayout). So, in future you can use this method whenever you need instad of code duplication
public void selectTabAt(int tabIndex) {
if (tabIndex >= 0 && tabIndex < getTabCount() && getSelectedTabPosition() != tabIndex) {
final Tab currentTab = getTabAt(tabIndex);
if (currentTab != null) {
this.post(new Runnable() {
#Override
public void run() {
currentTab.select();
}
});
}
}
}
If you don't want yo use CustomLayout. you can just do this
final Tab currentTab = mTabLayout.getTabAt(tabIndex);
if(currentTab != null){
mTabLayout.post(new Runnable() {
#Override
public void run() {
currentTab.select();
}
});
}
I found this solution for me:
TabLayout tabLayout = activity.getTabLayout();
tabLayout.setSmoothScrollingEnabled(true);
tabLayout.setScrollPosition(targetChannelPosition, 0f, true);
Also, if you receive this error: "Only the original thread that created a view hierarchy can touch its views.", you can use this code, in order to run on Ui thread:
// find a way to get the activity containing the tab layout
TabLayout tabLayout = activity.getTabLayout();
activity.runOnUiThread(new Runnable()
{
#Override
public void run()
{
TabLayout.Tab tab = tabLayout.getTabAt(targetChannelPosition);
tab.select();
}
});
Are you calling tab.select() before the TabLayout and its children have actually been measured and drawn? If so, your TabLayout won't animate to the selection with tab.select() (or Kayvan N's suggestion of scrollTo()). Using a Handler will probably work, but that's not an ideal solution.
Provided the layout hasn't been laid out yet, a ViewTreeObserver will allow you to move to your selected tab after the layout process is finished.
private void scrollToTabAfterLayout(final int tabIndex) {
if (getView() != null) {
final ViewTreeObserver observer = mTabLayout.getViewTreeObserver();
if (observer.isAlive()) {
observer.dispatchOnGlobalLayout(); // In case a previous call is waiting when this call is made
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
observer.removeOnGlobalLayoutListener(this);
} else {
//noinspection deprecation
observer.removeGlobalOnLayoutListener(this);
}
mTabLayout.getTabAt(tabIndex).select();
}
});
}
}
}
Please comment if you have any suggestions.
The above answer wouldn't work because first As agirardello mentioned you should not use mTabLayout.getWidth() since it doesn't return what we need (which is the position of the child you want to scroll to) and the updated solution doesn't always work because of a bug in TabLayout (reported here) but a work around is simple.
The tabs on the tabLayout are not direct children of the TabLayout so we need to go one level deeper using
((ViewGroup) mTabLayout.getChildAt(0)).getChildAt(YOUR_DESIRED_TAB_INDEX).getRight()
the only child of tabLayout is a TabLayout.SlidingTabStrip which is also a ViewGroup and getRight() will give us the right most position of our desired tab view. Thus scrolling to that position will give us what we desire. Here is a complete code:
int right = ((ViewGroup) mTabLayout.getChildAt(0)).getChildAt(4).getRight();
mTabLayout.scrollTo(right,0);
mTabLayout.getTabAt(4).select();
NOTE: Make sure you are calling these methods after the layout has been drown (like onResume and not onCreate)
Hope this helps.
new Handler().postDelayed(
new Runnable() {
#Override public void run() {
mTabLayout.getTabAt(TAB_NUMBER).select();
}
}, 100);
The code snippet below works for me
class TriggerOnceListener(private val v: View, private val block: () -> Unit) : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
block()
v.viewTreeObserver.removeOnPreDrawListener(this)
return true
}
}
fun onCreate() {
val position = ***The tab position you want to scroll to, 29 for your case here***
tabLayout.let { it.viewTreeObserver.addOnPreDrawListener(TriggerOnceListener(it)
{ it.setScrollPosition(position, 0f, true) } ) }
}
I dived into Tab.select(), and found Android uses Tablayout.setScrollPosition() to do this scrolling. And in onCreate() the widgets have not been measured, you need to postpone the call until layout is complete.
To select the last tab, use
tabLayout.getTabAt(X).select(); where X is the last tab index
If your TabLayout is used in conjunction with a ViewPager, which is common, simply add the following in the onCreate() method in your Activity:
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager);
viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout);
That some of your tabs are not being shown indicates the tabMode attribute is set to app:tabMode="scrollable".
viewpager.setItem(position) should also set the position of the tab
This solution worked for me. My situation is a little bit different though; in my case, I am using the TabLayout with a ViewPager and adding more views and calling notifyDataSetChange().
The solution is to set a callback on the observer of TabLayout and scroll when the children are actually added to the TabLayout. Here is my example:
/**
Keep in mind this is how I set my TabLayout up...
PagerAdapter pagerAdapter = new PagerAdapter(...);
ViewPager pager = (ViewPager)findViewById(...);
pager.setAdapter(pagerAdapter);
TabLayout tabLayout = (TabLayout)findViewById(...);
tabLayout.setupWithViewPager(pager);
*/
public void loadTabs(String[] topics) {
animateTabsOpen(); // Irrelevant to solution
// Removes fragments from ViewPager
pagerAdapter.clear();
// Adds new fragments to ViewPager
for (String t : topics)
pagerAdapter.append(t, new TestFragment());
// Since we need observer callback to still animate tabs when we
// scroll, it is essential to keep track of the state. Declare this
// as a global variable
scrollToFirst = true;
// Alerts ViewPager data has been changed
pagerAdapter.notifyOnDataSetChanged();
// Scroll to the beginning (or any position you need) in TabLayout
// using its observer callbacks
tabs.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
/**
We use onGlobalLayout() callback because anytime a tab
is added or removed the TabLayout triggers this; therefore,
we use it to scroll to the desired position we want. In my
case I wanted to scroll to the beginning position, but this
can easily be modified to scroll to any position.
*/
if (scrollToFirst) {
tabs.getTabAt(0).select();
tabs.scrollTo(0, 0);
scrollToFirst = false;
}
}
});
}
Here is my code for the PagerAdapter if you need it too lol:
public class PagerAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragments;
private List<String> titles;
public PagerAdapter(FragmentManager fm) {
super(fm);
this.fragments = new ArrayList<>();
this.titles = new ArrayList<>();
}
/**
* Adds an adapter item (title and fragment) and
* doesn't notify that data has changed.
*
* NOTE: Remember to call notifyDataSetChanged()!
* #param title Fragment title
* #param frag Fragment
* #return This
*/
public PagerAdapter append(String title, Fragment frag) {
this.titles.add(title);
this.fragments.add(frag);
return this;
}
/**
* Clears all adapter items and doesn't notify that data
* has changed.
*
* NOTE: Rememeber to call notifyDataSetChanged()!
* #return This
*/
public PagerAdapter clear() {
this.titles.clear();
this.fragments.clear();
return this;
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
#Override
public int getItemPosition(Object object) {
int position = fragments.indexOf(object);
return (position >= 0) ? position : POSITION_NONE;
}
}
I wonder if this is answer will be relevant since its coming very late. i actually achieved it in C# using Xamarin.
tabs.GetChildAt(0).Selected = true;
viewPager.SetCurrentItem(0, true);
tab = tabLayout.getSelectedTabPosition();
tab++;
TabLayout.Tab tabs = tabLayout.getTabAt(tab);
if (tabs != null) {
tabs.select();
}
else {
tabLayout.getTabAt(0).select();
}
if you want next tab on click event then use this code its work perfactly
I'm trying to have a button appear on the last page of a viewPager (which contains images in fragments). In these fragments, I'm creating a button that is invisible until the last page is shown. So far, I've achieved this behavior, but I'm having a bug where in page 2 the button appears, even though I'm "showing" (with setVisibility(View.VISIBLE)) the button only when the current page is greater or equal than 3 (there are 5 pages total, and also, greater or equal than 3 because the viewPager.getCurrentItem() behaves oddly and won't count the last or first page as an index).
My custom fragment's onCreateView() looks like this:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.imageview, container, false);
ivImage = (ImageView) root.findViewById(R.id.ivImageView);
btn_register = (Button) root.findViewById(R.id.button_register);
if( ((WelcomeActivity) this.getActivity()).test()>=3){
btn_register.setVisibility(View.VISIBLE);
}
setImageInViewPager();
return root;
}
In my main activity, the test() method simply returns the current page the viewPager is in:
public int test(){
return(viewPage.getCurrentItem());
}
How could I better detect the last page of a viewpager and use it so that I only display the button when the user is in the last page of the viewpager? thanks!
EDIT:
Here is a video of the problem.
HEre are the contents of my src folder: WelcomeActivity.java, FragmentPagerAdapter.java, Images.java, FragmentImageView.java.
In onPageSelected(), check the position : if(position==5) then show the button. Here I used
5 because you mention that there are 5 pages.
#Override
public void onPageSelected(int position) {
if(position == 5)
btn_register.setVisibility(View.VISIBLE);
}
Try this hope it will help you :)
You need to somehow tie the count of fragments to the individual fragments. So that each fragment knows its index. Can you add your code on how and when you are creating/adding the fragments to the view-pager?
Basing on that you can add a param to the arguments bundle while initialising the fragment there by fragment knows it's index in the view-pager.
Or else try this,
private int mCurrentPageSelected;
// In your activity
#Override
public void onPageSelected(int position) {
mCurrentPageSelected = position;
}
public int getSelectedPagePosition(){
return mCurrentPageSelected;
}
// In your fragment
public void onAttach(){
Log.d(TAG, "Fragment attached and current selected position is" + etActivity().getSelectedPagePosition());
if(getActivity().getSelectedPagePosition() >= 3){
// show the btn
btn_register.setVisibility(View.VISIBLE);
}else{
//hide the button
btn_register.setVisibility(View.GONE);
}
}
i wrote a FragmentActivity with a ViewPager, i did not want to use the actionbar for the tabs (because i did not want the tabs to be positioned over the menu) and also not the PagerTabStrip solution, because i could not customize it as i wanted.
so i wrote an own linearlayout as tabs container:
<LinearLayout
android:id="#+id/main_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:orientation="horizontal" >
</LinearLayout>
with an View.OnClickListener for each of each tab
OnClickListener onClickListener = new View.OnClickListener() {
#Override
public void onClick(final View view) {
mapTabs.get(lastSelectedTab).setSelected(false);
view.setSelected(true);
Runnable runnable = new Runnable() {
#Override
public void run() {
int index = Integer.parseInt(view.getTag().toString());
FragmentActivity.setTab(index);
lastSelectedTab = index;
}
};
handler.post(runnable);
}
};
}
public static void setTab(int index) {
FragmentActivity.viewPager.setCurrentItem(index, true); // true or false does not make any difference
}
this is the method i wrote to add the single tabs to the tab container (main_tabs):
private void addTab(LayoutInflater inflater, ViewGroup tabs, int imageId, View.OnClickListener onClickListener) {
inflater.inflate(R.layout.main_tab, tabs);
int index = tabs.getChildCount() - 1;
ImageView ivTab = (ImageView) tabs.getChildAt(index);
ivTab.setTag(String.valueOf(index)); // stored the tab index
ivTab.setImageResource(imageId);
ivTab.setOnClickListener(onClickListener);
mapTabs.put(index, ivTab); // add an imageview to the tab
}
i used the runnalbe so that i see the highlighting of the tab immediately, and not after the slight delay when the new page appears. i also stored the last selected tab in order to deselect it when the new one gets selected.
i store all tabs in a map in order to access them later for selection.
this is the OnPageChangeListener i wrote (to highlight the associated tab):
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
mapTabs.get(position).setSelected(true); // selected the tag asociated with the current visible page
mapTabs.get(lastSelectedTab).setSelected(false); // deselectes the last selected tab
lastSelectedTab = position;
ListFragment<?> item = FragmentActivity.fragmentAdapter.getItem(position);
setTitle(item.getTitle());
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
everything works fine, i am just not clear about the performance:
when i scroll to the next page, everything goes very fast and smooth. the next page appears immediately without any dealy; only the menu in the action bar (created in the onCreateOptionsMenu(Menu menu, MenuInflater inflater) method of each Fragment) appears with a slight delay (about 500-700 ms).
but when i click on one of the tabs, the selected page appears not immediately but with a slight delay (actually the same time the menu needs to appear as mentioned above: menu and page appear together at the same time). bzw. this happens also when i use the tabstrip solution.
i guess if its possible to achieve a delay-less page change by scrolling, it should be possible somehow to achieve the same performance for the tab clicks.
but what do i do wrong?
and is there a possibility to make the menu appear immediately just like the pages when scrolling? can the different menus not be cached just like the pages?
the solution was extremely simple. i just had to keep all my 4 tabs in the ViewPager so that they don't need to get recreated (and therefore don't cause the short delay after each tab-clicki). i just had to add following lines to the onCreate method.
viewPager = (ViewPager) findViewById(R.id.pager);
viewPager.setOffscreenPageLimit(4);
to avoid delay between tabs click in view pager should set the limit of tabs like
pager.setOffscreenPageLimit(4);
try this
myViewPager.animate().translationX(0f).setDuration(1000);
where 1000 is delay in milliseconds
I have an ActionBar with multiple tabs, each linked to a fragment. The problem I have is that when I use either bar.selectTab(Tab) or bar.setSelectedNavigationItem(int), it doesn't work. Specifically, this problem occurs when the tabs get reduced down to a Spinner in the ActionBar.
There is a known bug with the ActionBar, specifically with the methods mentioned above and specifically when the ActionBar's tabs are reduced to a Spinner.
Here's my workaround. It uses reflection to drill into the ActionBar if the tabs have been reduced to a Spinner. In your Activity class, create a method like so:
/**
* A documented and yet to be fixed bug exists in Android whereby
* if you attempt to set the selected tab of an action bar when the
* bar's tabs have been collapsed into a Spinner due to screen
* real-estate, the spinner item representing the tab may not get
* selected. This bug fix uses reflection to drill into the ActionBar
* and manually select the correct Spinner item
*/
private void select_tab(ActionBar b, int pos) {
try {
//do the normal tab selection in case all tabs are visible
b.setSelectedNavigationItem(pos);
//now use reflection to select the correct Spinner if
// the bar's tabs have been reduced to a Spinner
View action_bar_view = findViewById(getResources().getIdentifier("action_bar", "id", "android"));
Class<?> action_bar_class = action_bar_view.getClass();
Field tab_scroll_view_prop = action_bar_class.getDeclaredField("mTabScrollView");
tab_scroll_view_prop.setAccessible(true);
//get the value of mTabScrollView in our action bar
Object tab_scroll_view = tab_scroll_view_prop.get(action_bar_view);
if (tab_scroll_view == null) return;
Field spinner_prop = tab_scroll_view.getClass().getDeclaredField("mTabSpinner");
spinner_prop.setAccessible(true);
//get the value of mTabSpinner in our scroll view
Object tab_spinner = spinner_prop.get(tab_scroll_view);
if (tab_spinner == null) return;
Method set_selection_method = tab_spinner.getClass().getSuperclass().getDeclaredMethod("setSelection", Integer.TYPE, Boolean.TYPE);
set_selection_method.invoke(tab_spinner, pos, true);
} catch (Exception e) {
e.printStackTrace();
}
}
Example usage of this might be:
private void delete_fragment_and_tab(String fragment_tag) {
//remove the fragment
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(getFragmentManager().findFragmentByTag(fragment_tag));
//now remove the tab from the ActionBar
//and select the previous tab
ActionBar b = getActionBar();
Tab tab = b.getSelectedTab();
bar.removeTab(tab);
select_tab(bar, bar.getNavigationItemCount() -1);
}
I am facing one issue regarding tab swipe. My project is built on Android 3.2. I am implementing tab swipe using support library 4.0 (android-support-v4.jar). Everything implemented is working fine but when I deploy my app to an ICS device, then in portrait mode I am getting a spinner in action bar for tab selection. In portrait mode, the tab selection is not changing when swipe is done although content is changing, and everything is working fine in landscape mode.
final ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
actionBar.setDisplayHomeAsUpEnabled(true);
// Set up the ViewPager with the sections adapter.
ViewPager mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
actionBar.setSelectedNavigationItem(position);
}
});
I have tried putting breakpoint actionBar.setSelectedNavigationItem(position); on this line and even on portrait mode it's getting called but the selection is not changing.
Can anybody help with this?
EDITED:
Found a similar problem but don't see exactly how it is solved and how to integrate it in my code.
Problem:
Due to an insufficient real-state the platform uses collapsed navigation (i.e. Spinner). The system auto-determines NAVIGATION_MODE_TABS for landscape & NAVIGATION_MODE_LIST for portrait, changing the orientation from landscape to portrait updates the UI but for some reason does not update the navigation mode property to NAVIGATION_MODE_LIST and hence mActionView.setDropdownSelectedPosition(position) is not called. See the following code of ActionBarImpl : setSelectedNavigationItem
public void setSelectedNavigationItem(int position) {
switch (mActionView.getNavigationMode()) {
case NAVIGATION_MODE_TABS:
selectTab(mTabs.get(position));
break;
case NAVIGATION_MODE_LIST:
mActionView.setDropdownSelectedPosition(position);
break;
default:
throw new IllegalStateException(
"setSelectedNavigationIndex not valid for current navigation mode");
}
}
Workaround solution:
Through reflection we can get the tab spinner object and call setSelection method.
private Spinner getTabSpinner()
{
try
{
int id = getResources().getIdentifier("action_bar", "id", "android");
View actionBarView = findViewById(id);
Class<?> actionBarViewClass = actionBarView.getClass();
Field mTabScrollViewField = actionBarViewClass.getDeclaredField("mTabScrollView");
mTabScrollViewField.setAccessible(true);
Object mTabScrollView = mTabScrollViewField.get(actionBarView);
if (mTabScrollView == null) {
return null;
}
Field mTabSpinnerField = mTabScrollView.getClass().getDeclaredField("mTabSpinner");
mTabSpinnerField.setAccessible(true);
Object mTabSpinner = mTabSpinnerField.get(mTabScrollView);
if (mTabSpinner != null)
{
return (Spinner)mTabSpinner;
}
}
catch (Exception e) {
return null;
}
return null;
}
Then call the above method in onPageSelected event.
public void onPageSelected(int position) {
actionBar.setSelectedNavigationItem(position);
Spinner spinner = getTabSpinner();
if (spinner != null) {
spinner.setSelection(position);
}
}
Referred this post https://gist.github.com/2657485