前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android从零撸美团(三) - Android多标签tab滑动切换 - 自定义View快速实现高度定制封装

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

作者头像
solocoder
发布2022-04-06 13:01:57
1K0
发布2022-04-06 13:01:57
举报
文章被收录于专栏:大前端客栈

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

每个项目基本都会有多个 Tab ,以期在有限的屏幕空间展现更多的功能。 有需求就会有市场,如今也出现了很多优秀的 tab 切换框架,使用者众多。 但是深入思考之后还是决定自己造轮子~ 因为框架虽好,可不要贪杯哦~ 使用第三方框架最大的问题在于并不能完全满足实际需求,有的是 icon 图片 跟文字间距无法调整,有的后期会出现各种各样问题,不利于维护。 最重要的是自己写一个也不是很复杂,有研究框架填坑的时间也就写出来了。

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

代码语言:javascript
复制
tabWidget.init(getSupportFragmentManager(), fragmentList);

再上效果图:

在这里插入图片描述
在这里插入图片描述

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

一、思路

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

二、准备

开始之前需要准备两样东西:

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

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

代码语言:javascript
复制
<?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 了,因为文字的颜色也要跟着变。

代码语言:javascript
复制
<?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

三、实现

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

1. 写布局

首先是布局文件:

widget_custom_bottom_tab.xml

代码语言:javascript
复制
<?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"
    >

&lt;android.support.v4.view.ViewPager
    android:id=<span class="hljs-string">"@+id/vp_tab_widget"</span>
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"0dp"</span>
    android:layout_weight=<span class="hljs-string">"1"</span> /&gt;

&lt;!--下面的tab标签布局--&gt;
&lt;LinearLayout
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"wrap_content"</span>
    android:paddingBottom=<span class="hljs-string">"3dp"</span>
    android:paddingTop=<span class="hljs-string">"3dp"</span>
    &gt;

    &lt;LinearLayout
        android:id=<span class="hljs-string">"@+id/ll_menu_home_page"</span>
        android:layout_width=<span class="hljs-string">"0dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:layout_weight=<span class="hljs-string">"1"</span>
        android:gravity=<span class="hljs-string">"center"</span>
        android:orientation=<span class="hljs-string">"vertical"</span>&gt;

        &lt;ImageView
            android:id=<span class="hljs-string">"@+id/iv_menu_home"</span>
            style=<span class="hljs-string">"@style/menuIconStyle"</span>
            android:src=<span class="hljs-string">"@drawable/selector_icon_menu_home"</span> /&gt;

        &lt;TextView
            android:id=<span class="hljs-string">"@+id/tv_menu_home"</span>
            style=<span class="hljs-string">"@style/menuTextStyle"</span>
            android:text=<span class="hljs-string">"首页"</span> /&gt;
    &lt;/LinearLayout&gt;

    &lt;LinearLayout
        android:id=<span class="hljs-string">"@+id/ll_menu_nearby"</span>
        android:layout_width=<span class="hljs-string">"0dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:layout_weight=<span class="hljs-string">"1"</span>
        android:gravity=<span class="hljs-string">"center"</span>
        android:orientation=<span class="hljs-string">"vertical"</span>&gt;

        &lt;ImageView
            android:id=<span class="hljs-string">"@+id/iv_menu_nearby"</span>
            style=<span class="hljs-string">"@style/menuIconStyle"</span>
            android:src=<span class="hljs-string">"@drawable/selector_icon_menu_nearby"</span> /&gt;

        &lt;TextView
            android:id=<span class="hljs-string">"@+id/tv_menu_nearby"</span>
            style=<span class="hljs-string">"@style/menuTextStyle"</span>
            android:text=<span class="hljs-string">"附近"</span> /&gt;
    &lt;/LinearLayout&gt;

    &lt;LinearLayout
        android:id=<span class="hljs-string">"@+id/ll_menu_discover"</span>
        android:layout_width=<span class="hljs-string">"0dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:layout_weight=<span class="hljs-string">"1"</span>
        android:gravity=<span class="hljs-string">"center"</span>
        android:orientation=<span class="hljs-string">"vertical"</span>&gt;

        &lt;ImageView
            android:id=<span class="hljs-string">"@+id/iv_menu_discover"</span>
            style=<span class="hljs-string">"@style/menuIconStyle"</span>
            android:src=<span class="hljs-string">"@drawable/selector_icon_menu_discover"</span> /&gt;

        &lt;TextView
            android:id=<span class="hljs-string">"@+id/tv_menu_discover"</span>
            style=<span class="hljs-string">"@style/menuTextStyle"</span>
            android:text=<span class="hljs-string">"发现"</span> /&gt;
    &lt;/LinearLayout&gt;

    &lt;LinearLayout
        android:id=<span class="hljs-string">"@+id/ll_menu_order"</span>
        android:layout_width=<span class="hljs-string">"0dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:layout_weight=<span class="hljs-string">"1"</span>
        android:gravity=<span class="hljs-string">"center"</span>
        android:orientation=<span class="hljs-string">"vertical"</span>&gt;

        &lt;ImageView
            android:id=<span class="hljs-string">"@+id/iv_menu_order"</span>
            style=<span class="hljs-string">"@style/menuIconStyle"</span>
            android:src=<span class="hljs-string">"@drawable/selector_icon_menu_order"</span> /&gt;

        &lt;TextView
            android:id=<span class="hljs-string">"@+id/tv_menu_order"</span>
            style=<span class="hljs-string">"@style/menuTextStyle"</span>
            android:text=<span class="hljs-string">"订单"</span> /&gt;
    &lt;/LinearLayout&gt;

    &lt;LinearLayout
        android:id=<span class="hljs-string">"@+id/ll_menu_mine"</span>
        android:layout_width=<span class="hljs-string">"0dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:layout_weight=<span class="hljs-string">"1"</span>
        android:gravity=<span class="hljs-string">"center"</span>
        android:orientation=<span class="hljs-string">"vertical"</span>&gt;

        &lt;ImageView
            android:id=<span class="hljs-string">"@+id/iv_menu_mine"</span>
            style=<span class="hljs-string">"@style/menuIconStyle"</span>
            android:src=<span class="hljs-string">"@drawable/selector_icon_menu_mine"</span> /&gt;

        &lt;TextView
            android:id=<span class="hljs-string">"@+id/tv_menu_mine"</span>
            style=<span class="hljs-string">"@style/menuTextStyle"</span>
            android:text=<span class="hljs-string">"我的"</span> /&gt;
    &lt;/LinearLayout&gt;
&lt;/LinearLayout&gt;

</LinearLayout>

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

代码语言:javascript
复制
    <!--菜单栏的图标样式-->
    <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 呀,根节点是什么就继承什么。

先上代码吧:

代码语言:javascript
复制
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&lt;BaseFragment&gt; 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&lt;BaseFragment&gt; fragmentList) {
    mFragmentManager = fm;
    mFragmentList = fragmentList;
    initViewPager();
}

/**
 * 初始化 ViewPager
 */
private void <span class="hljs-function"><span class="hljs-title">initViewPager</span></span>() {
    mAdapter = new TabPagerAdapter(mFragmentManager, mFragmentList);
    viewPager.setAdapter(mAdapter);
    viewPager.addOnPageChangeListener(new ViewPager.<span class="hljs-function"><span class="hljs-title">OnPageChangeListener</span></span>() {
        @Override
        public void onPageScrolled(int position, <span class="hljs-built_in">float</span> positionOffset, int positionOffsetPixels) {

        }

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

        @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()) {
        <span class="hljs-keyword">case</span> R.id.ll_menu_home_page:
            selectTab(MenuTab.HOME);
            //使ViewPager跟随tab点击事件滑动
            viewPager.setCurrentItem(0);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> R.id.ll_menu_nearby:
            selectTab(MenuTab.NEARBY);
            viewPager.setCurrentItem(1);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> R.id.ll_menu_discover:
            selectTab(MenuTab.DISCOVER);
            viewPager.setCurrentItem(2);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> R.id.ll_menu_order:
            selectTab(MenuTab.ORDER);
            viewPager.setCurrentItem(3);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> R.id.ll_menu_mine:
            selectTab(MenuTab.MINE);
            viewPager.setCurrentItem(4);
            <span class="hljs-built_in">break</span>;
    }
}

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

    switch (tab) {
        <span class="hljs-keyword">case</span> HOME:
            llMenuHome.setActivated(<span class="hljs-literal">true</span>);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> NEARBY:
            llMenuNearby.setActivated(<span class="hljs-literal">true</span>);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> DISCOVER:
            llMenuDiscover.setActivated(<span class="hljs-literal">true</span>);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> ORDER:
            llMenuOrder.setActivated(<span class="hljs-literal">true</span>);
            <span class="hljs-built_in">break</span>;
        <span class="hljs-keyword">case</span> MINE:
            llMenuMine.setActivated(<span class="hljs-literal">true</span>);
    }

}


//让所有tab都取消选中
private void <span class="hljs-function"><span class="hljs-title">unCheckedAll</span></span>() {
    llMenuHome.setActivated(<span class="hljs-literal">false</span>);
    llMenuNearby.setActivated(<span class="hljs-literal">false</span>);
    llMenuDiscover.setActivated(<span class="hljs-literal">false</span>);
    llMenuOrder.setActivated(<span class="hljs-literal">false</span>);
    llMenuMine.setActivated(<span class="hljs-literal">false</span>);
}

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

}

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

实现了三个构造方法,这三个构造方法分别对应于不同的创建方式。如果不确定怎么创建它就都实现吧,不会出错。 既然不确定到底走哪个方法,那把初始化方法写到哪个里面呢?这儿有个小技巧,就是把一个参数的 ,和两个参数的 分别改成: 和 。这样无论走的哪个构造函数,最终都会走到三个参数的构造函数里,我们只要把初始化操作放在这个函数里就行了。

构造函数里的这行代码:将 文件与 java 代码绑定了起来,注意最后 一个参数是 而不是 。

本项目用到了 从 解脱出来。

切换选中未选中状态的原理是每次点击的时候,先调用 将所有 tab 都置为未选中状态,再单独设置要选中的 tab 为选中状态

实现 tab 的点击事件与 的滑动绑定需要在两个地方写逻辑: 1)tab 的点击回调里执行下面两行代码,分别使 tab 变为选中状态和让 滑动到相应位置。2)在 的监听方法 中,每滑动到一个页面,就调用 方法将对应的 tab 设置为选中状态。

记得在构造方法里设置默认的选中项:

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

四、使用

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

代码语言:javascript
复制
<?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">

&lt;com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget
    android:id=<span class="hljs-string">"@+id/tabWidget"</span>
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"wrap_content"</span> /&gt;

</LinearLayout>

然后在 Activity 里一句话调用:

代码语言:javascript
复制
tabWidget.init(getSupportFragmentManager(), fragmentList);

就是这么简单!

是不是很爽很清新?

贴出 MainActivity 完整代码:

代码语言:javascript
复制
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&lt;BaseFragment&gt; fragmentList;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    <span class="hljs-built_in">set</span>ContentView(R.layout.activity_main);
    ButterKnife.bind(this);

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

    //初始化
    init();
}

private void <span class="hljs-function"><span class="hljs-title">init</span></span>() {
    //构造Fragment的集合
    fragmentList = new ArrayList&lt;&gt;();
    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 标签滑动切换实例。 源码地址:https://github.com/cachecats/LikeMeiTuan

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/11/03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、思路
  • 二、准备
  • 三、实现
    • 1. 写布局
      • 2. 写 Java 代码自定义View
      • 四、使用
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档