常见的App主页实现

一、概述

再过两个月,我就30周岁了,开启人生的第四个十年。人生有几个十年,我已经度过了三个。我内心并未感觉自己三十岁,但每次听到同事们说出他们的年龄,心里暗暗比较比他们大了好几岁,才意识到他们真好,我曾经也是他们这样。自古三十而立,成家立业,我现在既没有成家,也不知道我这个写码的工作算不算立业,因为我还不知道过了30岁,还能写几年代码。但不管未来如何,当下好好写码,保持学习,未来也应该不会很差吧。

上面纯属扯淡,下面开始今天的正文。现在主流的app首页都是底部几个Tab,上面是Fragment展示内容,就像下图这样。实现这样的需求很简单,我想这应该是Android工程师必备的能力吧。虽然说很容易,没有什么技术难点,但我看到过一些人的实现并不是很完美,多多少少有点问题,在某些场景下会出现bug。
在这里插入图片描述

二、代码实现

上面内容部分有两种方式实现:

  1. 使用FragmentContainerView,承载显示内容的Fragment;
  2. 使用ViewPager2,和FragmentStateAdapter。

下面的Tab也有两种方式实现:

  1. 普通的控件实现,如:RadioButton;
  2. 使用BottomNavigationView。

上面的两种方式可以与下面两种方式任一配合实现。

第一种 FragmentContainerView 搭配 RadioButton

布局:

<?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">
    
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="#f2f2f2" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@color/white"
        android:orientation="horizontal"
        android:paddingBottom="4dp">

        <RadioButton
            android:id="@+id/radio1"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@null"
            android:button="@null"
            android:checked="true"
            android:drawableTop="@drawable/nav_1"
            android:gravity="center"
            android:paddingTop="10dp"
            android:text="Android"
            android:textColor="@color/nav_color"
            android:textSize="12sp" />
            
        <FrameLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <RadioButton
                android:id="@+id/radio2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@null"
                android:button="@null"
                android:drawableTop="@drawable/nav_2"
                android:gravity="center"
                android:paddingTop="10dp"
                android:text="Kotlin"
                android:textColor="@color/nav_color"
                android:textSize="12sp" />

            <!--tvBadge:用于显示数量-->
            <TextView
                android:id="@+id/tvBadge"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginStart="14dp"
                android:layout_marginTop="4dp"
                android:background="@drawable/shape_red_badge"
                android:gravity="center"
                android:minWidth="16dp"
                android:paddingHorizontal="4dp"
                android:paddingVertical="1dp"
                android:textColor="@color/white"
                android:textSize="10sp"
                android:visibility="gone"
                tools:text="99+"
                tools:visibility="visible" />
        </FrameLayout>

        <RadioButton
            android:id="@+id/radio3"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@null"
            android:button="@null"
            android:drawableTop="@drawable/nav_3"
            android:gravity="center"
            android:paddingTop="10dp"
            android:text="Jetpack"
            android:textColor="@color/nav_color"
            android:textSize="12sp" />

        <RadioButton
            android:id="@+id/radio4"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@null"
            android:button="@null"
            android:drawableTop="@drawable/nav_4"
            android:gravity="center"
            android:paddingTop="10dp"
            android:text="Java"
            android:textColor="@color/nav_color"
            android:textSize="12sp" />
    </LinearLayout>
</LinearLayout>

nav_1.xml(nav_2/3/4 类似)

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/icon_nav_1_checked" android:state_checked="true" />
    <item android:drawable="@drawable/icon_nav_1" />
</selector>

nav_color.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/blue" android:state_checked="true" />
    <item android:color="@color/black" />
</selector>

HomeActivity 代码:

class MainActivity : FragmentActivity, CompoundButton.OnCheckedChangeListener {
    
    

	//Fragment对应的Tag,用于在FragmentManager添加和寻找Fragment
    companion object {
    
    
        const val TAG_1 = "tag1"
        const val TAG_2 = "tag2"
        const val TAG_3 = "tag3"
        const val TAG_4 = "tag4"
        const val TAG_CHECKED = "checkedTag"
    }
    //当前选中的tab
    private var checkedTag = TAG_1
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
    
    //如果不是销毁重建,才添加fragment
            val fragment1 = Fragment1.newInstance()
            supportFragmentManager.beginTransaction()
                .add(R.id.fragmentContainerView, fragment1 , TAG_1)
                .commitNow()
        } else {
    
    //重建时,恢复checkedTag
            checkedTag = savedInstanceState.getString(TAG_CHECKED)!!
        }
        radio1.setOnCheckedChangeListener(this)
        radio2.setOnCheckedChangeListener(this)
        radio3.setOnCheckedChangeListener(this)
        radio4.setOnCheckedChangeListener(this)
    }
    
    //保存checkedTag,待重建时恢复
    override fun onSaveInstanceState(outState: Bundle) {
    
    
        super.onSaveInstanceState(outState)
        outState.putString(TAG_CHECKED, checkedTag)
    }
    
    //Radio选中事件监听
    override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
    
    
        if (isChecked) {
    
    
            changeRadioButton(buttonView.id)//将上一个选中的RadioButton置为非选中状态
            when (buttonView.id) {
    
    //根据当前选中的tab,切换显示的Fragment
                R.id.radio1 -> {
    
    
                    changeFragment(TAG_1) {
    
     Fragment1.newInstance() }
                }
                R.id.radio2 -> {
    
    
                    changeFragment(TAG_2) {
    
     Fragment2.newInstance() }
                }
                R.id.radio3 -> {
    
    
                    changeFragment(TAG_3) {
    
     Fragment3.newInstance() }
                }
                R.id.radio4 -> {
    
    
                    changeFragment(TAG_4) {
    
     Fragment4.newInstance() }
                }
            }
        }
    }
    
    //将id不是checkedId的RadioButton置为不选中状态
    private fun changeRadioButton(checkedId: Int) {
    
    
        if (checkedId != R.id.radio1) radio1.isChecked = false
        if (checkedId != R.id.radio2) radio2.isChecked = false
        if (checkedId != R.id.radio3) radio3.isChecked = false
        if (checkedId != R.id.radio4) radio4.isChecked = false
    }
    
    //切换显示的Fragment
    private fun changeFragment(fragmentTag: String, createFragment: () -> Fragment) {
    
    
        val fragmentManager = supportFragmentManager
        val beginTransaction = fragmentManager.beginTransaction()
        //先找找之前有没有添加过该Fragment
        var fragment = fragmentManager.findFragmentByTag(fragmentTag)
        if (fragment== null) {
    
    //没有添加过
            fragment= createFragment()//创建Fragment,并添加,fragmentTag一定要传
            beginTransaction.add(R.id.fragmentContainerView, fragment, fragmentTag)
        }
        //找到当前显示的Fragment,并隐藏
        fragmentManager.findFragmentByTag(checkedTag)?.let {
    
    
            beginTransaction.hide(it)
        }
        //显示选中的Fragment,注意这里提交事务使用的commitNow
        beginTransaction.show(fragment).commitNow()
        checkedTag = fragmentTag
    }     
    
}

需要注意几点:

  1. onCreate中,一定要判断savedInstanceState为空时才添加Fragment。如果不为空的话,说明Activity销毁重建了,FragmentManager会恢复之前添加的Fragment;
  2. 显示隐藏Fragment提交事务时,要使用FragmentTransaction的 commitNow 方法,而不能使用commit方法。commitNow顾名思义是立马就执行;commit是添加一个Action任务,等待执行,如果主线程繁忙,那可能会慢一点才能执行这个任务。 而我们的操作是需要马上生效的,所以当我们操作很快时,会发现使用commit会出现页面重叠的现象。
  3. 不建议用List或其他方式保存Fragment实例。当Activity销毁重建后,FragmentManager 恢复的Fragment 与 保存的 Fragment 不是同一对象。如果要获得Fragment,通过FragmentManager.findFragmentByTag 获取。所以在添加Fragment时一定要给它Tag。
  4. 如果在Fragment 中添加 Fragment,要使用Fragment的childFragmentManager 来操作,一定不能使用Activity中的FragmentManager,不然会出现Fragment错乱重叠等问题。
  5. Fragment必须要有无参构造方法(包括DialogFragment)。Activity重建时,FragmentManager 恢复 Fragment 是通过反射调用无参的构造方法创建Fragment,如果没有无参构造,会出现异常。如果Fragment需要传递参数,最好使用setArguments。

以上几点在其他实现方式中同样生效。

扫描二维码关注公众号,回复: 15953020 查看本文章

第二种 FragmentContainerView 搭配 BottomNavigationView 的实现

布局:

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

	<androidx.fragment.app.FragmentContainerView
    	android:id="@+id/fragmentContainerView"
    	android:layout_height="0dp"
        android:layout_weight="1"/>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@color/white"
        app:itemIconTint="@color/nav_color"
        app:itemTextAppearanceActive="@style/itemTextAppearanceActiveStyle"
        app:itemTextAppearanceInactive="@style/itemTextAppearanceInactiveStyle"
        app:itemTextColor="@color/nav_color"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/nav_menu" />
</LinearLayout>

app:itemIconTint 设置图标选中和未选中的颜色。
app:itemTextColor 设置文字选中和未选中的颜色。
itemTextAppearanceActive、itemTextAppearanceInactive设置文本选中和未选中的大小。
app:labelVisibilityMode设置文本显示模式,labeled为总是显示。

nav_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/item_1"
        android:icon="@drawable/nav_1"
        android:title="Android"/>

    <item
        android:id="@+id/item_2"
        android:icon="@drawable/nav_2"
        android:title="Kotlin"/>

    <item
        android:id="@+id/item_3"
        android:icon="@drawable/nav_3"
        android:title="Jetpack"/>
        
    <item
        android:id="@+id/item_4"
        android:icon="@drawable/nav_4"
        android:title="Java"/>
</menu>

HomeActivity 代码

class MainActivity : FragmentActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
    
    

    //Fragment对应的Tag,用于在FragmentManager添加和寻找Fragment
    companion object {
    
    
        const val TAG_1 = "tag1"
        const val TAG_2 = "tag2"
        const val TAG_3 = "tag3"
        const val TAG_4 = "tag4"
        const val TAG_CHECKED = "checkedTag"
    }

    //当前选中的tab
    private var checkedTag = TAG_1

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
    
    
            val fragment1 = Fragment1()
            supportFragmentManager.beginTransaction().add(R.id.fragmentContainerView, fragment1, TAG_1).commitNow()
        } else {
    
    
            checkedTag = savedInstanceState.getString(TAG_CHECKED)!!
        }
        bottomNavigationView.setOnNavigationItemSelectedListener(this)
    }

    override fun onSaveInstanceState(outState: Bundle) {
    
    
        super.onSaveInstanceState(outState)
        outState.putString(TAG_CHECKED, checkedTag)
    }

    //BottomNavigationView选中切换事件
    override fun onNavigationItemSelected(item: MenuItem): Boolean {
    
    
        if (supportFragmentManager.isStateSaved) return false
        when (item.itemId) {
    
    
            R.id.item_1-> {
    
    
                changeFragment(TAG_1) {
    
     Fragment1() }
            }
            R.id.item_2 -> {
    
    
                changeFragment(TAG_2) {
    
     Fragment2() }
            }
            R.id.item_3 -> {
    
    
                changeFragment(TAG_3) {
    
     Fragment3() }
            }
            R.id.item_4 -> {
    
    
                changeFragment(TAG_4) {
    
     Fragment4() }
            }
        }
        return true
    }
    
	//切换显示的Fragment
    private fun changeFragment(fragmentTag: String, createFragment: () -> Fragment) {
    
    
        if (fragmentTag == checkedTag) return //如果选中的tag是当前显示的tag不处理
        val fragmentManager = supportFragmentManager
        val beginTransaction = fragmentManager.beginTransaction()
        var fragment = fragmentManager.findFragmentByTag(fragmentTag)
        if (fragment == null) {
    
    
            fragment = createFragment()
            beginTransaction.add(R.id.fragmentContainerView, fragment, fragmentTag)
        }
        fragmentManager.findFragmentByTag(this.checkedTag)?.let {
    
    
            beginTransaction.hide(it)
        }
        beginTransaction.show(fragment).commitNow()
        this.checkedTag = fragmentTag
    }

    //设置badge数量
    private fun setCartBadgeCount(count: Int) {
    
    
        if (0 == count) {
    
    //移除
            bottomNavigationView.removeBadge(R.id.nav2)
        } else {
    
    //显示
            bottomNavigationView.getOrCreateBadge(R.id.nav2).run {
    
    
                backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.red)
                maxCharacterCount = 3//最多几位数,超过100,显示为99+
                number = count
            }
        }
    }
}

和第一种类似,只是监听事件不一样。另外BottomNavigationView 是支持显示和移除Badge的,显示时也可以不设置数量,这样就只显示一个小红点。

使用ViewPager2实现

布局:

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

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="horizontal" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@color/white"
        app:itemIconTint="@color/nav_color"
        app:itemTextAppearanceActive="@style/itemTextAppearanceActiveStyle"
        app:itemTextAppearanceInactive="@style/itemTextAppearanceInactiveStyle"
        app:itemTextColor="@color/nav_color"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/nav_menu" />
</LinearLayout>

android:orientation="horizontal"设置ViewPager2为水平滑动。

HomeActivity 代码:

class HomeActivity : FragmentActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
    
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)
        bottomNavigationView.setOnNavigationItemSelectedListener(this)
        //页面切换监听
        viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    
    

            override fun onPageSelected(position: Int) {
    
    
                when (position) {
    
    
                    0 -> {
    
    
                        bottomNavigationView.selectedItemId = R.id.item_1
                    }
                    1 -> {
    
    
                        bottomNavigationView.selectedItemId = R.id.item_2
                    }
                    2 -> {
    
    
                        bottomNavigationView.selectedItemId = R.id.item_3
                    }
                    3 -> {
    
    
                        bottomNavigationView.selectedItemId = R.id.item_4
                    }
                }
            }
        })
        viewPager2.adapter = object : FragmentStateAdapter(this) {
    
    

            override fun createFragment(position: Int): Fragment {
    
    
                return when (position) {
    
    
                    0 -> {
    
    
                        Fragment1()
                    }
                    1 -> {
    
    
                        Fragment2()
                    }
                    2 -> {
    
    
                        Fragment3()
                    }
                    3 -> {
    
    
                        Fragment4()
                    }
                    else -> {
    
    
                        throw IllegalArgumentException()
                    }
                }
            }

            override fun getItemCount(): Int {
    
    
                return 4
            }
        }
    }

    //BottomNavigationView选中切换事件
    override fun onNavigationItemSelected(item: MenuItem): Boolean {
    
    
        if (supportFragmentManager.isStateSaved) return false
        when (item.itemId) {
    
    
            R.id.item_1-> {
    
    
                viewPager2.currentItem = 0
            }
            R.id.item_2-> {
    
    
                viewPager2.currentItem = 1
            }
            R.id.item_3-> {
    
    
                viewPager2.currentItem = 2
            }
            R.id.item_4 -> {
    
    
                viewPager2.currentItem = 3
            }
        }
        return true
    }
}

ViewPager2 是基于RecyclerView实现的,FragmentStateAdapter是RecyclerView.Apdater子类,有两个方法需要我们实现:createFragment 根据position返回一个Fragment;getItemCount 返回Fragment数量。Fragment相关的操作、选中状态保存等都不需要我们处理,FragmentStateAdapter已经处理好,并且当Fragment数量很多时,会回收不需要的Fragment。

这里还遗留了一些问题,比如Fragment里面也有水平滑动的View,会被ViewPager2拦截滑动不了。还有这里的写法似乎有些不妥。等我后面多用用这玩意再来补充。

猜你喜欢

转载自blog.csdn.net/ganduwei/article/details/123642565