Android博客挑错系列之一FragmentTabHost和ViewPager实现底部导航栏

博客文章指出常见实现FragmentTabHost和ViewPager结合的底部导航栏方式会浪费资源,创建过多Fragment实例。提出两种解决方案:自定义底部栏配合ViewPager或重用TabWidget。作者分享了通过修改TabWidget源码创建MartianTabWidget,实现公共的OnTabSelectionChanged接口以简化点击事件处理,并已在实际项目淘小说中应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

决定写这个系列,是因为大部分的国内博客都是拿来主义——照搬,一点也不考察博客内容的正确性,

希望这个系列能对国内的这种风气起到一点作用。


首先就拿一个常见的FragmentTabHost和ViewPager实现手势切换底部工具栏来开开刀吧

一个实现普遍的实现如博客使用FragmentTabHost和ViewPager实现仿微信主界面侧滑  (无意冒犯,Google搜索的排名靠前,如需删除请告知)

这种实现方法确实能够实现底部工具栏的切换,但是却增加了一倍Fragment的数量。


文中调用FragmentTabHost的addTab将fragmentArray中Fragments加入FragmentTabHost中,

(注:fragment是以class的形式传入,在FragmentTabHost的内部会将这些fragment的类实例化)

而在initPager函数中又新建了一遍所有的fragment,并加入到ViewPager中。

因此文中存在8个fragment的实例,而这个界面往往是放在应用启动后的第一个界面,这种资源的浪费是很严重的。


那么如何解决呢?


一种方法是自己写一个底部的工具栏,然后配合ViewPager进行Tab的切换。

虽然方法简单,但略显繁琐,有点重复造轮子的感脚。


另外一种方法是重用FragmentTabHost中的TabWidget类,具体的xml代码如下:


  


  
    
    
   
        
    
   

       
   


  

TabWidget的具体用法资料比较少,可直接看其源代码TabWidget.java

简单说下,通过TabWidget.addView()来加入Tab的图标,

通过TabWidget.setCurrentTab()来设置当前focus在哪个tab上。


但是这种方法存在两个问题,一个是Tab的点击响应实现不方便,一个是2.3及以下的系统不兼容。

可以看到源码中OnTabSelectionChanged并不是public,因此只有和TabWidget在一个package中的才能访问这个interface。

而不兼容问题导致在应用中TabWidget显示成一个白条。。。


因此小星最后的解决方法是实现自己的一个TabWidget,但并不是完全自己写,而是有选择的拷贝TabWidget里的代码。

最终修改后的TabWidget的源码如下,小星将其命名为MartianTabWidget:

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;

/**
 * Created by xuzb on 14-12-29.
 */
public class MartianTabWidget extends LinearLayout implements View.OnFocusChangeListener {

    private OnTabSelectionChanged mSelectionChangedListener;

    // This value will be set to 0 as soon as the first tab is added to TabHost.
    private int mSelectedTab = -1;

    public MartianTabWidget(Context context) {
        super(context);
        initTabWidget();
    }

    public MartianTabWidget(Context context, AttributeSet attrs) {
        super(context, attrs);
        initTabWidget();
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public MartianTabWidget(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initTabWidget();
    }

    private void initTabWidget() {
        setChildrenDrawingOrderEnabled(true);

        // Deal with focus, as we don't want the focus to go by default
        // to a tab other than the current tab
        setFocusable(true);
        setOnFocusChangeListener(this);
    }

    /**
     * Returns the tab indicator view at the given index.
     *
     * @param index the zero-based index of the tab indicator view to return
     * @return the tab indicator view at the given index
     */
    public View getChildTabViewAt(int index) {
        return getChildAt(index);
    }

    /**
     * Returns the number of tab indicator views.
     * @return the number of tab indicator views.
     */
    public int getTabCount() {
        return getChildCount();
    }

    /**
     * Sets the current tab.
     * This method is used to bring a tab to the front of the Widget,
     * and is used to post to the rest of the UI that a different tab
     * has been brought to the foreground.
     *
     * Note, this is separate from the traditional "focus" that is
     * employed from the view logic.
     *
     * For instance, if we have a list in a tabbed view, a user may be
     * navigating up and down the list, moving the UI focus (orange
     * highlighting) through the list items.  The cursor movement does
     * not effect the "selected" tab though, because what is being
     * scrolled through is all on the same tab.  The selected tab only
     * changes when we navigate between tabs (moving from the list view
     * to the next tabbed view, in this example).
     *
     * To move both the focus AND the selected tab at once, please use
     * {@link #setCurrentTab}. Normally, the view logic takes care of
     * adjusting the focus, so unless you're circumventing the UI,
     * you'll probably just focus your interest here.
     *
     *  @param index The tab that you want to indicate as the selected
     *  tab (tab brought to the front of the widget)
     *
     */
    public void setCurrentTab(int index) {
        if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
            return;
        }

        if (mSelectedTab != -1) {
            getChildTabViewAt(mSelectedTab).setSelected(false);
        }
        mSelectedTab = index;
        getChildTabViewAt(mSelectedTab).setSelected(true);

        if (isShown()) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (mSelectedTab == -1) {
            return i;
        } else {
            // Always draw the selected tab last, so that drop shadows are drawn
            // in the correct z-order.
            if (i == childCount - 1) {
                return mSelectedTab;
            } else if (i >= mSelectedTab) {
                return i + 1;
            } else {
                return i;
            }
        }
    }

    @Override
    public void addView(View child) {
        if (child.getLayoutParams() == null) {
            final LinearLayout.LayoutParams lp = new LayoutParams(
                    0,
                    ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
            lp.setMargins(0, 0, 0, 0);
            child.setLayoutParams(lp);
        }

        // Ensure you can navigate to the tab with the keyboard, and you can touch it
        child.setFocusable(true);
        child.setClickable(true);

        super.addView(child);

        // TODO: detect this via geometry with a tabwidget listener rather
        // than potentially interfere with the view's listener
        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
        child.setOnFocusChangeListener(this);
    }

    @Override
    public void removeAllViews() {
        super.removeAllViews();
        mSelectedTab = -1;
    }

    /**
     * Provides a way for {@link android.widget.TabHost} to be notified that the user clicked on a tab indicator.
     */
    public void setTabSelectionListener(OnTabSelectionChanged listener) {
        mSelectionChangedListener = listener;
    }

    /** {@inheritDoc} */
    public void onFocusChange(View v, boolean hasFocus) {
        if (v == this && hasFocus && getTabCount() > 0) {
            getChildTabViewAt(mSelectedTab).requestFocus();
            return;
        }

        if (hasFocus) {
            int i = 0;
            int numTabs = getTabCount();
            while (i < numTabs) {
                if (getChildTabViewAt(i) == v) {
                    setCurrentTab(i);
                    mSelectionChangedListener.onTabSelectionChanged(i, false);
                    if (isShown()) {
                        // a tab is focused so send an event to announce the tab widget state
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
                    }
                    break;
                }
                i++;
            }
        }
    }

    // registered with each tab indicator so we can notify tab host
    private class TabClickListener implements View.OnClickListener {

        private final int mTabIndex;

        private TabClickListener(int tabIndex) {
            mTabIndex = tabIndex;
        }

        public void onClick(View v) {
            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
        }
    }

    /**
     * Let {@link android.widget.TabHost} know that the user clicked on a tab indicator.
     */
    public static interface OnTabSelectionChanged {
        /**
         * Informs the TabHost which tab was selected. It also indicates
         * if the tab was clicked/pressed or just focused into.
         *
         * @param tabIndex index of the tab that was selected
         * @param clicked whether the selection changed due to a touch/click
         * or due to focus entering the tab through navigation. Pass true
         * if it was due to a press/click and false otherwise.
         */
        public void onTabSelectionChanged(int tabIndex, boolean clicked);
    }
}

其中将OnTabSelectionChanged这个interface设置为public,

这样实现MartianTabWidget的tab点击响应事件就可以简单的调用setTabSelectionListener来实现了~


大家如有问题可留言讨论哈~

该方法已经在我们的小说阅读软件淘小说中实现,

大家多多支持哈~后续会有更多干货出来哦~




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值