java - tablayout - tabs android studio example
Android FragmentTabHost-¿No está completamente horneado todavía? (4)
Quería ver si alguien ha tenido éxito con la personalización de las pestañas utilizando FragmentTabHost que viene con el nuevo nivel 17 de la API de Android.
Estaba emocionado de poder anidar un tabHost dentro de mi ViewPager SherlockFragments, pero tengo problemas para hacer cosas simples como mover las pestañas a la parte inferior o cambiar el diseño de las pestañas.
¿Alguien ha visto un buen ejemplo del uso de esta funcionalidad?
Este es el único ejemplo que pude encontrar en los documentos de Android, y no hay casi nada que describa su uso. También parece ignorar lo que se define en el diseño para R.id.fragment1
.
Supongo que mi pregunta sería si alguien ha encontrado un buen tutorial sobre: FragmentTabHost o si tiene una idea de cómo a) colocar las pestañas anidadas en la parte inferior o b) cambiar el diseño de dichas pestañas.
He intentado todos los métodos habituales, pero como parece que el archivo de diseño XML está anulado, no he tenido mucha suerte.
private FragmentTabHost mTabHost;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setContentView(R.layout.fragment_tabs);
mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);
mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);
mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
FragmentStackSupport.CountingFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
LoaderCursorSupport.CursorLoaderListFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
LoaderCustomSupport.AppListFragment.class, null);
mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
return mTabHost;
}
Después de hacer algunas investigaciones, parece que puede haber un error al inicializar el FragmentTabHost en la biblioteca de soporte. El usuario aquí en el código de Google ha proporcionado una sugerencia para esto:
FragmentTabHost.java
private void initFragmentTabHost(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs,
new int[] { android.R.attr.inflatedId }, 0, 0);
mContainerId = a.getResourceId(0, 0);
a.recycle();
super.setOnTabChangedListener(this);
// If owner hasn''t made its own view hierarchy, then as a convenience
// we will construct a standard one here.
/***** HERE COMMENT CODE BECAUSE findViewById(android.R.id.tabs) EVERY TIME IS NULL WE HAVE OWN LAYOUT ******//
// if (findViewById(android.R.id.tabs) == null) {
// LinearLayout ll = new LinearLayout(context);
// ll.setOrientation(LinearLayout.VERTICAL);
// addView(ll, new FrameLayout.LayoutParams(
// ViewGroup.LayoutParams.FILL_PARENT,
// ViewGroup.LayoutParams.FILL_PARENT));
//
// TabWidget tw = new TabWidget(context);
// tw.setId(android.R.id.tabs);
// tw.setOrientation(TabWidget.HORIZONTAL);
// ll.addView(tw, new LinearLayout.LayoutParams(
// ViewGroup.LayoutParams.FILL_PARENT,
// ViewGroup.LayoutParams.WRAP_CONTENT, 0));
//
// FrameLayout fl = new FrameLayout(context);
// fl.setId(android.R.id.tabcontent);
// ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0));
//
// mRealTabContent = fl = new FrameLayout(context);
// mRealTabContent.setId(mContainerId);
// ll.addView(fl, new LinearLayout.LayoutParams(
// LinearLayout.LayoutParams.FILL_PARENT, 0, 1));
// }
}
Diseño XML para el fragmento:
<android.support.v4.app.FragmentTabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="0"/>
<FrameLayout
android:id="@+id/realtabcontent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TabWidget
android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"/>
</LinearLayout>
</android.support.v4.app.FragmentTabHost>
Creo que fue un error establecer el método initFragmentTabHost()
en el constructor. En ese momento, TabHost no hace a sus hijos, eso sucede después. LinearLayout
, por ejemplo, trabaja con sus hijos en el método onMeasure()
( grepcode ). ViewGroup
en el constructor solo ViewGroup
las variables, y establece mChildrenCount = 0
( grepcode ).
Todo lo que pude hacer, solo es disfrazar FragmentTabHost
:
<android.support.v4.app.FragmentTabHost xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@android:id/tabhost"
style="@style/Widget.TabHost"
a:inflatedId="@+id/content" />
Y disfrazar Tabs
(tengo problemas con las alturas de las pestañas, las resuelvo en código):
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
style="@style/Widget.Tab" >
<TextView
a:id="@android:id/title"
style="@style/Widget.TabTitle" />
</LinearLayout>
En codigo:
tabSpec = mTabHost.newTabSpec(tag).setIndicator(createTab(caption));
...
private View createTab(CharSequence title) {
final View v = View.inflate(getActivity(), LAYOUT_TAB, null);
((TextView) v.findViewById(android.R.id.title)).setText(title);
return v;
}
Creo que otra personalización con TabWidget
se puede hacer solo con la manipulación programática, como esto:
final View tabs = (TabWidget) mTabHost.findViewById(android.R.id.tabs);
final ViewGroup parent = (ViewGroup) mTabHost.getChildAt(0);
parent.removeView(tabs);
parent.addView(tabs);
En mi humilde opinión, esto no es bueno.
Finalmente llegué al fondo de esto. Hay un problema con FragmentTabHost.java que siempre creará un elemento TabHost para usted, sin importar lo que defina en XML e infle de antemano.
Como tal, comenté esa parte del código al escribir mi propia versión de FragmentTabHost.java.
Asegúrese de usar su nueva versión de esto en su diseño XML, <com.example.app.MyFragmentTabHost
Y por supuesto inflarlo:
Fragment1.java:
mTabHost = (MyFragmentTabHost) view.findViewById(android.R.id.tabhost);
mTabHost.setup(getActivity(), getChildFragmentManager(), android.R.id.tabcontent);
MyFragmentTabHost.java:
package com.example.app;
import java.util.ArrayList;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TabHost;
/**
* Special TabHost that allows the use of {@link Fragment} objects for
* its tab content. When placing this in a view hierarchy, after inflating
* the hierarchy you must call {@link #setup(Context, FragmentManager, int)}
* to complete the initialization of the tab host.
*
*/
public class MyFragmentTabHost extends TabHost
implements TabHost.OnTabChangeListener {
private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
private FrameLayout mRealTabContent;
private Context mContext;
private FragmentManager mFragmentManager;
private int mContainerId;
private TabHost.OnTabChangeListener mOnTabChangeListener;
private TabInfo mLastTab;
private boolean mAttached;
static final class TabInfo {
private final String tag;
private final Class<?> clss;
private final Bundle args;
private Fragment fragment;
TabInfo(String _tag, Class<?> _class, Bundle _args) {
tag = _tag;
clss = _class;
args = _args;
}
}
static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context mContext;
public DummyTabFactory(Context context) {
mContext = context;
}
@Override
public View createTabContent(String tag) {
View v = new View(mContext);
v.setMinimumWidth(0);
v.setMinimumHeight(0);
return v;
}
}
static class SavedState extends BaseSavedState {
String curTab;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
curTab = in.readString();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(curTab);
}
@Override
public String toString() {
return "FragmentTabHost.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " curTab=" + curTab + "}";
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
public MyFragmentTabHost(Context context) {
// Note that we call through to the version that takes an AttributeSet,
// because the simple Context construct can result in a broken object!
super(context, null);
initFragmentTabHost(context, null);
}
public MyFragmentTabHost(Context context, AttributeSet attrs) {
super(context, attrs);
initFragmentTabHost(context, attrs);
}
private void initFragmentTabHost(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs,
new int[] { android.R.attr.inflatedId }, 0, 0);
mContainerId = a.getResourceId(0, 0);
a.recycle();
super.setOnTabChangedListener(this);
/*** REMOVE THE REST OF THIS FUNCTION ***/
/*** findViewById(android.R.id.tabs) IS NULL EVERY TIME ***/
}
/**
* @deprecated Don''t call the original TabHost setup, you must instead
* call {@link #setup(Context, FragmentManager)} or
* {@link #setup(Context, FragmentManager, int)}.
*/
@Override @Deprecated
public void setup() {
throw new IllegalStateException(
"Must call setup() that takes a Context and FragmentManager");
}
public void setup(Context context, FragmentManager manager) {
super.setup();
mContext = context;
mFragmentManager = manager;
ensureContent();
}
public void setup(Context context, FragmentManager manager, int containerId) {
super.setup();
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
ensureContent();
mRealTabContent.setId(containerId);
// We must have an ID to be able to save/restore our state. If
// the owner hasn''t set one at this point, we will set it ourself.
if (getId() == View.NO_ID) {
setId(android.R.id.tabhost);
}
}
private void ensureContent() {
if (mRealTabContent == null) {
mRealTabContent = (FrameLayout)findViewById(mContainerId);
if (mRealTabContent == null) {
throw new IllegalStateException(
"No tab content FrameLayout found for id " + mContainerId);
}
}
}
@Override
public void setOnTabChangedListener(OnTabChangeListener l) {
mOnTabChangeListener = l;
}
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
tabSpec.setContent(new DummyTabFactory(mContext));
String tag = tabSpec.getTag();
TabInfo info = new TabInfo(tag, clss, args);
if (mAttached) {
// If we are already attached to the window, then check to make
// sure this tab''s fragment is inactive if it exists. This shouldn''t
// normally happen.
info.fragment = mFragmentManager.findFragmentByTag(tag);
if (info.fragment != null && !info.fragment.isDetached()) {
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.detach(info.fragment);
ft.commit();
}
}
mTabs.add(info);
addTab(tabSpec);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
String currentTab = getCurrentTabTag();
// Go through all tabs and make sure their fragments match
// the correct state.
FragmentTransaction ft = null;
for (int i=0; i<mTabs.size(); i++) {
TabInfo tab = mTabs.get(i);
tab.fragment = mFragmentManager.findFragmentByTag(tab.tag);
if (tab.fragment != null && !tab.fragment.isDetached()) {
if (tab.tag.equals(currentTab)) {
// The fragment for this tab is already there and
// active, and it is what we really want to have
// as the current tab. Nothing to do.
mLastTab = tab;
} else {
// This fragment was restored in the active state,
// but is not the current tab. Deactivate it.
if (ft == null) {
ft = mFragmentManager.beginTransaction();
}
ft.detach(tab.fragment);
}
}
}
// We are now ready to go. Make sure we are switched to the
// correct tab.
mAttached = true;
ft = doTabChanged(currentTab, ft);
if (ft != null) {
ft.commit();
mFragmentManager.executePendingTransactions();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.curTab = getCurrentTabTag();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
setCurrentTabByTag(ss.curTab);
}
@Override
public void onTabChanged(String tabId) {
if (mAttached) {
FragmentTransaction ft = doTabChanged(tabId, null);
if (ft != null) {
ft.commit();
}
}
if (mOnTabChangeListener != null) {
mOnTabChangeListener.onTabChanged(tabId);
}
}
private FragmentTransaction doTabChanged(String tabId, FragmentTransaction ft) {
TabInfo newTab = null;
for (int i=0; i<mTabs.size(); i++) {
TabInfo tab = mTabs.get(i);
if (tab.tag.equals(tabId)) {
newTab = tab;
}
}
if (newTab == null) {
throw new IllegalStateException("No tab known for tag " + tabId);
}
if (mLastTab != newTab) {
if (ft == null) {
ft = mFragmentManager.beginTransaction();
}
if (mLastTab != null) {
if (mLastTab.fragment != null) {
ft.detach(mLastTab.fragment);
}
}
if (newTab != null) {
if (newTab.fragment == null) {
newTab.fragment = Fragment.instantiate(mContext,
newTab.clss.getName(), newTab.args);
ft.add(mContainerId, newTab.fragment, newTab.tag);
} else {
ft.attach(newTab.fragment);
}
}
mLastTab = newTab;
}
return ft;
}
}
Me gustaría mencionar algunos problemas más con FragmentTabHost. Estoy usando un ViewPager donde cada página (Vista) contiene un FragmenTabHost y tuve que superar varios problemas:
1) FragmentTabHost asume que es el único FragmentTabHost en su FragmentManager principal (segundo argumento de FragmentTabHost.setup()
). Esto provoca el resto de los problemas ...
2) las "etiquetas" que proporciona cuando llama a addTab()
se transfieren directamente al FragmentManager, por lo que si solo usa etiquetas codificadas para todas sus páginas (algo perfectamente razonable), su primera página creará fragmentos de pestañas, mientras que todas las demás La página reutilizará esas pestañas. Sí, página 2 controles página 1 ...
La solución es generar nombres de etiqueta únicos. Agregué el número de página a las cadenas codificadas:
public Object instantiateItem( ViewGroup container, int position )
{
...
tabHost.addTab( tabHost.newTabSpec( "tab1_" + position ) ...);
tabHost.addTab( tabHost.newTabSpec( "tab2_" + position ) ...);
tabHost.addTab( tabHost.newTabSpec( "tab3_" + position ) ...);
...
}
3) Todos los fragmentos de pestañas se colocan en un contenedor identificado solo por "id de vista" (el tercer argumento de FragmentTabHost.setup()
). Esto significa que cuando FragmentManager resuelve el viewId a una vista, siempre encuentra la primera instancia (desde la primera página). Todas sus otras páginas son ignoradas.
La solución a esto es asignar identificaciones únicas a sus vistas de "contenido de la pestaña", por ejemplo:
public Object instantiateItem( ViewGroup container, int position )
{
View view = m_inflater.inflate(R.layout.page, null);
View tabContent = view.findViewById(R.id.realtabcontent);
tabContent.setId(m_nextViewId);
m_nextViewId++;
MyFragmentTabHost tabHost = (MyFragmentTabHost) view.findViewById(android.R.id.tabhost);
tabHost.setup(m_activity, m_activity.getSupportFragmentManager(), tabContent.getId());
...
}
4) No elimina fragmentos de pestañas cuando se destruye. Mientras que el ViewPager destruye las vistas no utilizadas a medida que se desliza, los FragmentTabHosts contenidos en esas vistas "filtran" los fragmentos de la pestaña. Cuando el ViewPager vuelve a crear una instancia de una página vista anteriormente (usando etiquetas usadas anteriormente), FragmentTabHost notará que los fragmentos de esas pestañas ya existen y simplemente las volverá a unir. Esto explota porque los fragmentos apuntan a vistas que han sido destruidas por ViewPager.
La solución es eliminar fragmentos cuando se destruye FragmentTabHost. onDetachedFromWindow()
agregar este código a onDetachedFromWindow()
en su copia local de FragmentTabHost.java
class MyFragmentTabHost
{
...
protected void onDetachedFromWindow()
{
super.onDetachedFromWindow();
mAttached = false;
boolean removeFragments = false;
if( mContext instanceof Activity )
{
Activity activity = (Activity)mContext;
removeFragments = !activity.isDestroyed();
}
if( removeFragments )
{
FragmentTransaction ft = null;
for (int i = 0; i < mTabs.size(); i++)
{
TabInfo tab = mTabs.get(i);
if (tab.fragment != null)
{
if (ft == null)
{
ft = mFragmentManager.beginTransaction();
}
ft.remove(tab.fragment);
}
}
if (ft != null)
{
ft.commit();
mFragmentManager.executePendingTransactions();
}
}
}
Probablemente también podría evitar estos problemas utilizando un FragmentPagerAdapter o FragmentStatePagerAdapter (hace fragmentos) en lugar de un PagerAdapter estándar (hace vistas) Luego llamarías a FragmentTabHost.setup( ... fragment.getChildFragmentManager() ... )
.
Por lo que probado, la solución es buena. Es importante no inicializar MyFragmentTabHost con su constructor. Al menos si la clase que contiene el MyFragmentTabHost es un fragmento. No he probado con un FragmentActivity ...