Android从零撸美团(三) - Android多标签tab滑动切换 - 自定义View快速实现高度定制封装

这是【从零撸美团】系列文章第三篇
【从零撸美团】是一个高仿美团的开源项目,旨在巩固 Android 相关知识的同时,帮助到有须要的小伙伴。
GitHub 源码地址:github.com/cachecats/L…java

Android从零撸美团(一) - 统一管理 Gradle 依赖 提取到单独文件中android

Android从零撸美团(二) - 仿美团下拉刷新自定义动画git

Android从零撸美团(四) - 美团首页布局解析及实现 - Banner+自定义View+SmartRefreshLayout下拉刷新上拉加载更多github


每一个项目基本都会有多个 Tab ,以期在有限的屏幕空间展示更多的功能。 有需求就会有市场,现在也出现了不少优秀的 tab 切换框架,使用者众多。bash

可是深刻思考以后仍是决定本身造轮子~app

由于框架虽好,可不要贪杯哦~ 使用第三方框架最大的问题在于并不能彻底知足实际需求,有的是 icon 图片 跟文字间距没法调整,有的后期会出现各类各样问题,不利于维护。 最重要的是本身写一个也不是很复杂,有研究框架填坑的时间也就写出来了。框架

先看怎么用:一句代码搞定ide

tabWidget.init(getSupportFragmentManager(), fragmentList);
复制代码

再上效果图: 函数

在这里插入图片描述

你没看错,长得跟美团如出一辙,毕竟这个项目就叫【从零撸美团】 ㄟ( ▔, ▔ )ㄏ布局

1、思路

底部 tab 布局有不少实现方式,好比 RadioButton、FragmentTabHost、自定义组合View等。这里采用的是自定义组合View方式,由于可定制度更高。 滑动切换基本都是采用 ViewPager + Fragment ,集成简单,方案较成熟。这里一样采用这种方式。

2、准备

开始以前须要准备两样东西:

  1. 五个 tab 的选中和未选中状态的 icon 图片共计10张
  2. 五个 Fragment

这是最基本的素材,有了素材以后就开始干活吧~ 因为要实现点击选中图片和文字都变色成选中状态,没有选中就变成灰色,因此要对每组 icon 创建一个 selector xml文件实现状态切换。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_vector_home_pressed" android:state_activated="true" />
    <item android:drawable="@drawable/ic_vector_home_normal" android:state_activated="false" />
</selector>
复制代码

这里用了 android:state_activated 做为状态标记,由于最经常使用的 pressedfocused 都达不到长久保持状态的要求,都是松开手指以后就恢复了。在代码中手动设置 activated 值就好。 注意:此处设置的是 icon 图片,因此用 android:drawable,与下面文字使用的 android:color 有区别。

设置完图片资源后,该设置文字颜色的 selector 了,由于文字的颜色也要跟着变。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/meituanGreen" android:state_activated="true" />
    <item android:color="@color/gray666" android:state_activated="false" />
</selector>
复制代码

注意图片用 android:drawable,文字用 android:color

3、实现

准备工做作完以后,就开始正式的自定义View啦。

1. 写布局

首先是布局文件:

widget_custom_bottom_tab.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_tab_widget"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <!--下面的tab标签布局-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="3dp"
        android:paddingTop="3dp"
        >

        <LinearLayout
            android:id="@+id/ll_menu_home_page"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_home"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_home" />

            <TextView
                android:id="@+id/tv_menu_home"
                style="@style/menuTextStyle"
                android:text="首页" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_nearby"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_nearby"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_nearby" />

            <TextView
                android:id="@+id/tv_menu_nearby"
                style="@style/menuTextStyle"
                android:text="附近" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_discover"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_discover"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_discover" />

            <TextView
                android:id="@+id/tv_menu_discover"
                style="@style/menuTextStyle"
                android:text="发现" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_order"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_order"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_order" />

            <TextView
                android:id="@+id/tv_menu_order"
                style="@style/menuTextStyle"
                android:text="订单" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_menu_mine"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_menu_mine"
                style="@style/menuIconStyle"
                android:src="@drawable/selector_icon_menu_mine" />

            <TextView
                android:id="@+id/tv_menu_mine"
                style="@style/menuTextStyle"
                android:text="个人" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>
复制代码

最外层用竖向排列的 LinearLayout 包裹,它有两个子节点,上面是用于滑动和装载 FragmentViewPager,下面是五个 Tab 的布局。 为了方便管理把几个 ImageViewTextView 的共有属性抽取到 styles.xml 里了:

<!--菜单栏的图标样式-->
    <style name="menuIconStyle" >
        <item name="android:layout_width">25dp</item>
        <item name="android:layout_height">25dp</item>
    </style>

    <!--菜单栏的文字样式-->
    <style name="menuTextStyle">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">@drawable/selector_menu_text_color</item>
        <item name="android:textSize">12sp</item>
        <item name="android:layout_marginTop">3dp</item>
    </style>
复制代码

有了布局文件以后,就开始真正的自定义 View 吧。

2. 写 Java 代码自定义View

新建 java 文件 CustomBottomTabWidget 继承自 LinearLayout。为何继承 LinearLayout 呢?由于咱们的布局文件根节点就是 LinearLayout 呀,根节点是什么就继承什么。

先上代码吧:

package com.cachecats.meituan.widget.bottomtab;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

import com.cachecats.meituan.R;
import com.cachecats.meituan.base.BaseFragment;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;


public class CustomBottomTabWidget extends LinearLayout {

    @BindView(R.id.ll_menu_home_page)
    LinearLayout llMenuHome;
    @BindView(R.id.ll_menu_nearby)
    LinearLayout llMenuNearby;
    @BindView(R.id.ll_menu_discover)
    LinearLayout llMenuDiscover;
    @BindView(R.id.ll_menu_order)
    LinearLayout llMenuOrder;
    @BindView(R.id.ll_menu_mine)
    LinearLayout llMenuMine;
    @BindView(R.id.vp_tab_widget)
    ViewPager viewPager;

    private FragmentManager mFragmentManager;
    private List<BaseFragment> mFragmentList;
    private TabPagerAdapter mAdapter;

    public CustomBottomTabWidget(Context context) {
        this(context, null, 0);
    }

    public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this);
        ButterKnife.bind(view);

        //设置默认的选中项
        selectTab(MenuTab.HOME);

    }

    /**
     * 外部调用初始化,传入必要的参数
     *
     * @param fm
     */
    public void init(FragmentManager fm, List<BaseFragment> fragmentList) {
        mFragmentManager = fm;
        mFragmentList = fragmentList;
        initViewPager();
    }

    /**
     * 初始化 ViewPager
     */
    private void initViewPager() {
        mAdapter = new TabPagerAdapter(mFragmentManager, mFragmentList);
        viewPager.setAdapter(mAdapter);
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                //将ViewPager与下面的tab关联起来
                switch (position) {
                    case 0:
                        selectTab(MenuTab.HOME);
                        break;
                    case 1:
                        selectTab(MenuTab.NEARBY);
                        break;
                    case 2:
                        selectTab(MenuTab.DISCOVER);
                        break;
                    case 3:
                        selectTab(MenuTab.ORDER);
                        break;
                    case 4:
                        selectTab(MenuTab.MINE);
                        break;
                    default:
                        selectTab(MenuTab.HOME);
                        break;
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });
    }

    /**
     * 点击事件集合
     */
    @OnClick({R.id.ll_menu_home_page, R.id.ll_menu_nearby, R.id.ll_menu_discover, R.id.ll_menu_order, R.id.ll_menu_mine})
    public void onViewClicked(View view) {

        switch (view.getId()) {
            case R.id.ll_menu_home_page:
                selectTab(MenuTab.HOME);
                //使ViewPager跟随tab点击事件滑动
                viewPager.setCurrentItem(0);
                break;
            case R.id.ll_menu_nearby:
                selectTab(MenuTab.NEARBY);
                viewPager.setCurrentItem(1);
                break;
            case R.id.ll_menu_discover:
                selectTab(MenuTab.DISCOVER);
                viewPager.setCurrentItem(2);
                break;
            case R.id.ll_menu_order:
                selectTab(MenuTab.ORDER);
                viewPager.setCurrentItem(3);
                break;
            case R.id.ll_menu_mine:
                selectTab(MenuTab.MINE);
                viewPager.setCurrentItem(4);
                break;
        }
    }

    /**
     * 设置 Tab 的选中状态
     *
     * @param tab 要选中的标签
     */
    public void selectTab(MenuTab tab) {
        //先将全部tab取消选中,再单独设置要选中的tab
        unCheckedAll();

        switch (tab) {
            case HOME:
                llMenuHome.setActivated(true);
                break;
            case NEARBY:
                llMenuNearby.setActivated(true);
                break;
            case DISCOVER:
                llMenuDiscover.setActivated(true);
                break;
            case ORDER:
                llMenuOrder.setActivated(true);
                break;
            case MINE:
                llMenuMine.setActivated(true);
        }

    }


    //让全部tab都取消选中
    private void unCheckedAll() {
        llMenuHome.setActivated(false);
        llMenuNearby.setActivated(false);
        llMenuDiscover.setActivated(false);
        llMenuOrder.setActivated(false);
        llMenuMine.setActivated(false);
    }

    /**
     * tab的枚举类型
     */
    public enum MenuTab {
        HOME,
        NEARBY,
        DISCOVER,
        ORDER,
        MINE
    }
}
复制代码

注释应该写的很清楚了,这里再强调几个点:

  1. 实现了三个构造方法,这三个构造方法分别对应于不一样的建立方式。若是不肯定怎么建立它就都实现吧,不会出错。 既然不肯定到底走哪一个方法,那把初始化方法写到哪一个里面呢?这儿有个小技巧,就是把一个参数的 super(context),和两个参数的 super(context, attrs) 分别改为:this(context, null, 0)this(context, attrs, 0)。这样不管走的哪一个构造函数,最终都会走到三个参数的构造函数里,咱们只要把初始化操做放在这个函数里就好了。
  2. 构造函数里的这行代码:
    View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this);
    复制代码
    widget_custom_bottom_tab.xml 文件与 java 代码绑定了起来,注意最后 一个参数是 this 而不是 null
  3. 本项目用到了 ButterKnifefindViewById() 解脱出来。
  4. 切换选中未选中状态的原理是每次点击的时候,先调用 unCheckedAll () 将全部 tab 都置为未选中状态,再单独设置要选中的 tab 为选中状态 llMenuHome.setActivated(true);
  5. 实现 tab 的点击事件与 ViewPager 的滑动绑定须要在两个地方写逻辑: 1)tab 的点击回调里执行下面两行代码,分别使 tab 变为选中状态和让 ViewPager 滑动到相应位置。
    selectTab(MenuTab.HOME);
    //使ViewPager跟随tab点击事件滑动
    viewPager.setCurrentItem(0);
    复制代码
    2)在 ViewPager 的监听方法 onPageSelected() 中,每滑动到一个页面,就调用 selectTab(MenuTab.HOME) 方法将对应的 tab 设置为选中状态。
  6. 记得在构造方法里设置默认的选中项:
    //设置默认的选中项
     selectTab(MenuTab.HOME);
    复制代码

好啦,到这自定义 View 已经完成了。下面看看怎么使用。

4、使用

在主页的布局文件里直接引用:

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:orientation="vertical"
    tools:context="com.cachecats.meituan.app.MainActivity">

    <com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget
        android:id="@+id/tabWidget"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
复制代码

而后在 Activity 里一句话调用:

tabWidget.init(getSupportFragmentManager(), fragmentList);
复制代码

就是这么简单! 是否是很爽很清新?

贴出 MainActivity 完整代码:

package com.cachecats.meituan.app;

import android.os.Bundle;

import com.cachecats.meituan.MyApplication;
import com.cachecats.meituan.R;
import com.cachecats.meituan.app.discover.DiscoverFragment;
import com.cachecats.meituan.app.home.HomeFragment;
import com.cachecats.meituan.app.mine.MineFragment;
import com.cachecats.meituan.app.nearby.NearbyFragment;
import com.cachecats.meituan.app.order.OrderFragment;
import com.cachecats.meituan.base.BaseActivity;
import com.cachecats.meituan.base.BaseFragment;
import com.cachecats.meituan.di.DIHelper;
import com.cachecats.meituan.di.components.DaggerActivityComponent;
import com.cachecats.meituan.di.modules.ActivityModule;
import com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends BaseActivity {

    @BindView(R.id.tabWidget)
    CustomBottomTabWidget tabWidget;
    private List<BaseFragment> fragmentList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        DaggerActivityComponent.builder()
                .applicationComponent(MyApplication.getApplicationComponent())
                .activityModule(new ActivityModule(this))
                .build().inject(this);

        //初始化
        init();
    }

    private void init() {
        //构造Fragment的集合
        fragmentList = new ArrayList<>();
        fragmentList.add(new HomeFragment());
        fragmentList.add(new NearbyFragment());
        fragmentList.add(new DiscoverFragment());
        fragmentList.add(new OrderFragment());
        fragmentList.add(new MineFragment());

        //初始化CustomBottomTabWidget
        tabWidget.init(getSupportFragmentManager(), fragmentList);
    }
}

复制代码

整个代码很简单,只须要构造出 Fragment 的列表传给 CustomBottomTabWidget 就好啦。

总结:本身造轮子可能前期封装花些时间,但本身写的代码本身最清楚,几个月后再改需求改代码能快速的定位到要改的地方,便于维护。 而且最后封装完用起来也很简单啊,不用在 Activity 里写那么多配置代码,总体逻辑更清晰,耦合度更低。


以上就是用自定义 View 的方式实现高度定制化的多 tab 标签滑动切换实例。
源码地址:github.com/cachecats/L…
欢迎下载,欢迎 star,欢迎点赞~

相关文章
相关标签/搜索