Welcome to follow the official account: JueCode

The bottom navigation bar has been a very common control in app, such as wechat, Jianshu, QQ and so on. Click the bottom label to switch the interface. The main means of implementation are

  • RadioGroup
  • FragmentTabLayout
  • TabLayout
  • Bottom Navigation

TabLayout is usually used as the top navigation bar. Today we will implement a bottom navigation bar based on FragmentTabLayout. Let’s take a look at the implementation:


Today’s quest will follow the following steps:

  • FrameTabLayout layout
  • Custom control
  • Interface encapsulation
  • One line of code using
  • FrameTabLayout source code analysis

All right, get ready to drive

1. FrameTabLayout layout

Myfragment_tab_layout is a unique layout that uses the system id. We can’t name it android: ID. We can implement r.layout. myFragment_tab_layout for the specific layout. Layout is actually relatively simple, there are a few points need to pay attention to the next

The id is android: ID/tabContent FrameLayout, which is obviously used to place content. In our case, it is used to place Fragment, and this ID is used by the system and cannot be changed

Id is an Android: ID/Tabs TabWidget. As the name suggests, the TabWidget places the bottom tabs, such as Home, Contact, etc. Balabala, yes, you guessed it, this ID is also unchangeable

In order to differentiate, I have deliberately used two loud colors as the distinction. The green area in the image above is FrameLayout, and the orange area is TabWidget

<android.support.v4.app.FragmentTabHost xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@android:id/tabhost"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.juexingzhe.testfragmenttablayout.MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:background="@android:color/holo_green_dark" />
        <TabWidget
            android:id="@android:id/tabs"
            android:layout_width="match_parent"
            android:layout_height="? attr/actionBarSize"
            android:layout_gravity="bottom"
            android:background="@android:color/holo_orange_dark" />
    </LinearLayout>
</android.support.v4.app.FragmentTabHost>
Copy the code

Specific why id can not be changed, after we analyze the source code when we know, first press, the guest officer continue to look back ~ ~ ~

2. Customize MyFragmentTabLayout

DividerDrawable is used to set the dividing line between the tabs in the bottom TAB bar. The dividerDrawable is used to load the layout posted above.

private void init(){
        View view = LayoutInflater.from(getContext()).inflate(R.layout.myfragment_tab_layout, this, true);

        fragmentTabHost = (FragmentTabHost) view.findViewById(android.R.id.tabhost);

        dividerDrawable = null;
}
Copy the code

Before we move on, let’s take a look at how we can use FragmentTabHost without customizing the control. What I’ve posted below is schematic code that can’t be used directly, but it can be a bit cumbersome and directly demonstrates the need for encapsulation.

fragmentTabHost.setup(getContext(), fragmentManager, android.R.id.tabcontent); TabSpec TabSpec = fragmentTabHost. NewTabSpec (...) ; fragmentTabHost.addTab(tabSpec, fragment.class, bundle); FragmentTabHost. GetTabWidget (.) setDividerDrawable (...) ;Copy the code

Let’s look at the rest of the process for customizing the MyFragmentTabLayout control from the diagram above. So this method is basically calling setup, and the prototype of this method is setup(Context Context, FragmentManager Manager, int containerId) so there’s nothing to say about the first Context, You need to pass in the fragmentManager, which is used to manage the fragment. ContainerId is the id of the control that contains the content, which is FrameLayout in green above.

public MyFragmentTabLayout init(FragmentManager fragmentManager) {
        fragmentTabHost.setup(getContext(), fragmentManager, android.R.id.tabcontent);
        return this;
}
Copy the code

The fragmentTabHost initialization is complete after the above procedure. Some small partners are anxious, the bottom TAB bar has not seen the trace?? The number of tabs at the bottom of the TAB bar should not be arbitrary. It is better to make decisions based on the amount of data, which Is what Google does, so tags are initialized during the fragmentTabHost data initialization. See the implementation code below.

  • FragmentTabHost. NewTabSpec TAB bar at the bottom of the method is used to construct, need to pass in a Tag, and a tabview, we here is very simple is above the layout of the photos below
  • Fragmenttabhost. addTab creates the fragment and the bottom TAB bar. Any data that needs to be sent to the fragment can be sent via the bundle
  • SetDividerDrawable we pass null here, which means we don’t need the divider line, the divider line is provided by default:

  • SetOnTabChangedListener is the click event that sets the label
public MyFragmentTabLayout creat() {if (fragmentTabLayoutAdapter == null) return null;
        TabInfo tabInfo;
        for (int i = 0; i < fragmentTabLayoutAdapter.getCount(); i++){
            tabInfo = fragmentTabLayoutAdapter.getTabInfo(i);
            TabSpec tabSpec = fragmentTabHost.newTabSpec(tabInfo.getTabTag()).setIndicator(tabInfo.getTabView());
            fragmentTabHost.addTab(tabSpec, tabInfo.getFragmentClass(), tabInfo.getBundle());
            fragmentTabHost.getTabWidget().setDividerDrawable(dividerDrawable);
            fragmentTabHost.setOnTabChangedListener(new OnTabChangeListener() { @Override public void onTabChanged(String tabId) { int currentTab = fragmentTabHost.getCurrentTab(); fragmentTabLayoutAdapter.onClick(currentTab); }}); }return this;
}
Copy the code

Bottom label layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <ImageView
        android:id="@+id/img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:gravity="center"
        android:id="@+id/tab_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
Copy the code

The above code is wrapped by the interface, so let’s move on

3. Interface encapsulation

We also leave an interface as hook in the control. Users can customize data, label layout and click events for the control through the interface

public interface FragmentTabLayoutAdapter{

        int getCount();

        TabInfo getTabInfo(int pos);

        View createView(int pos);

        void onClick(int pos);

}
Copy the code

Let’s revisit the custom procedure above. The number of tags is obtained by getCount; The data needed to construct each tag is obtained from getTabInfo, with pos being the location of the tag; The layout of each label is obtained from createView, with pos as above; OnClick is the tag click event, with pos as above.

4. Use a line of code

The first step is to declare the controls in a layout file. This layout file simply references our custom controls. There’s nothing to explain.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.juexingzhe.testfragmenttablayout.MainActivity"
    android:orientation="vertical">

    <com.example.juexingzhe.testfragmenttablayout.MyFragmentTabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
Copy the code

Then you pass in a line of code, pass in the fragmentManager to initialize it, pass in the implementation of the interface FragmentTabLayoutAdapter, which we’ve also extracted to provide a default implementation, All you need to do is implement createView to customize the layout you want to display and implement onClick to customize each TAB’s click event.

fragmentTabHost.init(getSupportFragmentManager())
               .setFragmentTabLayoutAdapter(new DefaultFragmentTabAdapter(Arrays.asList(fragmentClass), Arrays.asList(textViewArray), Arrays.asList(drawables)){
                   @Override
                   public View createView(int pos) {
                       View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.tab_item, null);
                       ImageView imageView = (ImageView) view.findViewById(R.id.img);
                       imageView.setImageResource(drawables[pos]);
                       TextView textView = (TextView) view.findViewById(R.id.tab_text);
                       textView.setText(textViewArray[pos]);
                       return view;
                   }

                   @Override
                   public void onClick(int pos) {
                       Toast.makeText(MainActivity.this, textViewArray[pos] + " be clicked", Toast.LENGTH_SHORT).show();
                   }
               }).creat();
Copy the code

A line of code will do. We look at the implementation of the DefaultFragmentTabAdapter, the default implementation for two methods getCount and getTabInfo, the first method the earth person all know, the second method is to construct each label need data information.

public class DefaultFragmentTabAdapter implements MyFragmentTabLayout.FragmentTabLayoutAdapter {

    private List<Class> fragmentclass = new ArrayList<>();
    private List<String> fragmentTag = new ArrayList<>();
    private List<Integer> drawables = new ArrayList<>();

    public DefaultFragmentTabAdapter(List<Class> fragmentclass, List<String> fragmentTag, List<Integer> drawables) {
        this.fragmentclass = fragmentclass;
        this.fragmentTag = fragmentTag;
        this.drawables = drawables;
    }

    @Override
    public int getCount() {
        return fragmentTag.size();
    }

    @Override
    public TabInfo getTabInfo(int pos) {
        return new TabInfo.Builder(fragmentTag.get(pos), createView(pos), fragmentclass.get(pos)).build();
    }

    @Override
    public View createView(int pos) {
        return null;
    }

    @Override
    public void onClick(int pos) {

    }
}
Copy the code

A brief mention of the TabInfo data class, which is also in build mode, will not be explained here. Several attributes, tabTag is the Tag that TabSpec passes in; TabView is the layout of the bottom tabs; FragmentClass is the fragment corresponding to each tag; Bundle is the data corresponding to the fragment; BackgroundRes is the background of each tag, and you can set the background change when you click on it.

public class TabInfo { String tabTag; View tabView; Class fragmentClass; Bundle bundle; int backgroundRes; ... }Copy the code

5. Source code analysis of FrameTabLayout

Let’s take a quick look at the source code for FrameTabLayout. The first is the setup method you see when you initialize it, which works in the ensureHierarchy method.

public void setup(Context context, FragmentManager manager, int containerId) {
        ensureHierarchy(context);  // Ensure views required by super.setup()
        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 ourselves. if (getId() == View.NO_ID) { setId(android.R.id.tabhost); }}Copy the code

This method is closely related to layout, and can also explain the layout ID dead problem we mentioned earlier. If we do not find a TabWidget whose ID is Android.r.ida abs, the system will generate a layout for us, where the TabWidget is the bottom TAB bar and the ID is Android.r.ida Abs as in the layout code above; MRealTabContent is the area where the content is placed. It is a FrameLayout layout. The ID is Android.r.ida TabContent, which is the same as the layout code FrameLayout.

private void ensureHierarchy(Context context) {
        // If owner hasn't made its own view hierarchy, then as a convenience // we will construct a standard one here. if (findViewById(android.R.id.tabs) == null) { LinearLayout ll = new LinearLayout(context); ll.setOrientation(LinearLayout.VERTICAL); addView(ll, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); TabWidget tw = new TabWidget(context); tw.setId(android.R.id.tabs); tw.setOrientation(TabWidget.HORIZONTAL); ll.addView(tw, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_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.MATCH_PARENT, 0, 1)); }}Copy the code

Let’s move on to the addTab method, which is binding layout and data. Construct TabInfo from the TabSpec passed in, and then call the addTab(tabSepc) method in TabHost.

public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<? > clss, @Nullable Bundle args) { tabSpec.setContent(new DummyTabFactory(mContext)); final String tag = tabSpec.getTag(); final 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()) { final FragmentTransaction ft = mFragmentManager.beginTransaction(); ft.detach(info.fragment); ft.commit(); } } mTabs.add(info); addTab(tabSpec); }Copy the code

In the addTab(tabSepc) method, mtabWidget. addView(tabIndicator) adds the bottom tag. I’m going to add it setCurrentTab(0), so let’s go down.

Public void addTab(TabSpec TabSpec) {...... mTabWidget.addView(tabIndicator); mTabSpecs.add(tabSpec);if (mCurrentTab == -1) {
            setCurrentTab(0); }}Copy the code

In setCurrentTab method will be called invokeOnTabChangeListener () method, the final call onTabChanged method, FragmentTabHost is realized OnTabChangeListener interface, Let’s go back to FragmentTabHost and look down

private void invokeOnTabChangeListener() {
        if(mOnTabChangeListener ! = null) { mOnTabChangeListener.onTabChanged(getCurrentTabTag()); } } /** * Interface definitionfor a callback to be invoked when tab changed
   */
public interface OnTabChangeListener {
        void onTabChanged(String tabId);
}
Copy the code

The doTabChanged method is called first, and the click event we defined is then processed. Let’s look at the doTabChanged method. If a fragment exists attach it directly, otherwise construct it on fragment.instantiate and add it via add. So you can see the whole process here.

public void onTabChanged(String tabId) {
        if (mAttached) {
            final FragmentTransaction ft = doTabChanged(tabId, null);
            if (ft != null) {
                ft.commit();
            }
        }
        if(mOnTabChangeListener ! = null) { mOnTabChangeListener.onTabChanged(tabId); } } private FragmentTransactiondoTabChanged(@Nullable String tag,
            @Nullable FragmentTransaction ft) {
        final TabInfo newTab = getTabInfoForTag(tag);
        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;
}      
Copy the code

6. Summary

If you can see this, it’s true love. Use FragmentTabHost to watch out for a few id issues in the layout. The easier way to do this is to use the controls I’ve wrapped, and that’s all 🙂

Put the code online, download it yourself if you need it, and don’t forget to like it. Making the address

Today’s custom FragmentTabLayout tour is over, you can get off, your praise is my biggest motivation, thank you!