背景
android的知识更新相对于后台来说稍微频繁一点,下面主要讲解用最新技术栈来搭建一个最基础的框架,顺便练练手,其中遇到了一点小小坑,特此记录一下。
一、搭建基础类 BaseActivity
abstract class BaseActivity<T : ViewDataBinding> : AppCompatActivity {
constructor() : super()
private lateinit var mLoadingDialog: LoadingDialog
var mBinding: T? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
StatusBar().fitSystemBar(this)
mLoadingDialog = LoadingDialog(this)
mBinding = DataBindingUtil.setContentView(this, getLayoutId())
initData(savedInstanceState)
}
override fun onDestroy() {
super.onDestroy()
mBinding?.unbind()
}
abstract fun initData(savedInstanceState: Bundle?)
abstract fun getLayoutId(): Int
/**
* show 加载中
*/
fun showLoading() {
mLoadingDialog.showDialog(this, false)
}
/**
* dismiss loading dialog
*/
fun dismissLoading() {
mLoadingDialog.dismissDialog()
}
/**
* 设置toolbar名称
*/
protected fun setToolbarTitle(view: TextView, title: String) {
view.text = title
}
/**
* 设置toolbar返回按键图片
*/
protected fun setToolbarBackIcon(view: ImageView, id: Int) {
view.setBackgroundResource(id)
}
}
二、搭建基础类BaseFragment
package com.example.command.base
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.example.command.R
import com.example.command.databinding.BaseFragmentLayoutBinding
import com.example.command.widget.LoadingDialog
import com.kingja.loadsir.core.LoadService
private const val TAG = "BaseFragment"
abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
var mBinding: T? = null
private lateinit var mContext: Context
private lateinit var mLoadingDialog: LoadingDialog
private lateinit var loadService: LoadService<Any>
private lateinit var mBaseContainBinding: BaseFragmentLayoutBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBaseContainBinding =
DataBindingUtil.inflate(inflater, R.layout.base_fragment_layout, container, false)
mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
mBaseContainBinding.baseContainer.addView(mBinding?.root)
return mBaseContainBinding.root
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mLoadingDialog = LoadingDialog(view.context)
initData()
}
abstract fun initData()
override fun onDestroy() {
super.onDestroy()
mBinding?.unbind()
}
abstract fun getLayoutId(): Int
/**
* show 加载中
*/
private fun showLoading() {
mLoadingDialog.showDialog(mContext, false)
}
/**
* dismiss loading dialog
*/
private fun dismissLoading() {
mLoadingDialog.dismissDialog()
}
private var time: Long = 0
private var oldMsg: String? = null
/**
* 相同msg 只显示一个。
*/
fun showToast(msg: String) {
if (msg != oldMsg) {
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show()
time = System.currentTimeMillis()
} else {
if (System.currentTimeMillis() - time > 2000) {
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show()
time = System.currentTimeMillis()
}
}
oldMsg = msg
}
三、编写MainActivity类
(1)activity.main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true"
android:background="@color/color_ffffff">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_view" />
</LinearLayout>
</layout>
这里布局采用的FragmentContainerView+BottomNavigationView方式,而不再采用传统的FrameLayout+GroupBottom的这种方式。
(2)menu
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_baseline_home_24"
android:title="@string/title_home"/>
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_baseline_call_split_24"
android:title="@string/title_dashboard"/>
<item
android:id="@+id/navigation_personal"
android:icon="@drawable/ic_baseline_person_outline_24"
android:title="@string/title_notifications"/>
</menu>
(3) 创建Fragment文件
(1)naviation_home.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation_home"
app:startDestination="@id/fragment_home">
<fragment
android:id="@+id/fragment_home"
android:name="com.example.home.HomeFragment"
tools:layout="@layout/fragment_home"/>
</navigation>
这里需要注意一下,这个android:id="@+id/navigation_home"一定要和men资源下的item id一定是相同的,这样才能绑定Id,一个小坑。比如首页Id是"navigation_home",那么给navigation id也要是"navigation_home"
(2)HomeFragement
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun getLayoutId(): Int {
return R.layout.fragment_home
}
override fun initData() {
}
}
其他类似
(4)MainActivity
package com.example.study
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import com.alibaba.android.arouter.facade.annotation.Route
import com.example.command.base.BaseActivity
import com.example.command.ktx.setupWithNavController
import com.example.command.support.Constants
import com.example.study.databinding.ActivityMainBinding
private const val TAG = "MainActivity"
class MainActivity : BaseActivity<ActivityMainBinding>() {
private var currentNavController: LiveData<NavController>? = null
override fun getLayoutId(): Int {
return R.layout.activity_main
}
override fun initData(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
setupBottomNavigationBar()
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
setupBottomNavigationBar()
}
/**
* navigation绑定BottomNavigationView
*/
private fun setupBottomNavigationBar() {
val navGraphIds =
listOf(R.navigation.naviation_home, R.navigation.navigation_dashboard,R.navigation.navigation_personal)
val controller = mBinding?.navView?.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
controller?.observe(this, Observer { navController ->
//setupActionBarWithNavController(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
run {
val id = destination.id
mBinding?.navView?.visibility = View.VISIBLE
}
}
})
currentNavController = controller
}
override fun onSupportNavigateUp(): Boolean {
return currentNavController?.value?.navigateUp() ?: false
}
}
(4)自定义BottomNavigationView.setupWithNavController方法
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
Log.d(TAG, "setupWithNavController: $selectedItemId and $graphId")
Log.d(TAG, "setupWithNavController: $fragmentTag and $graphId")
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
Log.d(TAG, "setupWithNavController: isStateSaved ")
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
Log.d(TAG, "setupWithNavController: $firstFragmentTag and $newlySelectedItemTag")
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
Log.d(TAG, "setupWithNavController: $firstFragmentTag")
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent)
&& selectedItemId != navHostFragment.navController.graph.id) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
Log.d(TAG, "attachNavHostFragment: ")
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
Log.d(TAG, "obtainNavHostFragment existingFragment : $existingFragment ")
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
三、效果图
(1)
(2)
(3)