1. Brief explanation
A NavigationBar custom control that supports landscape and portrait orientation
Adapted based on this big guy’s article: https://blog.csdn.net/qq910689331/article/details/81941887
Added vertical support and some comments based on the original code
Usage effect:
2. Upload the code
The code is only 400 lines long, so it is very convenient to just copy it and use it. But one thing to note: the compatible fragment in the control is the AndroidX version. If you need to use it on an older version, you need to modify it yourself.
package com.example.lxt.View import android.annotation.SuppressLint import android.content.Context import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment class BottomBar: View { val TAG = "BottomBar"; constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} companion object{ @JvmField val HORIZONTAL = 0; @JvmField valVERTICAL = 1; } var containerId = 0 //The control id that stores the fragment var firstCheckedIndex = 0 //Default page item (first) var itemCount = 0 //Number of pages var fragmentList: MutableList<Fragment> = ArrayList() // fragment list var titleList: MutableList<String> = ArrayList() // title list var iconResBeforeList: MutableList<Int> = ArrayList() // Icon resource list (before selection) var iconResAfterList: MutableList<Int> = ArrayList() // Icon resource list (after selection) val iconBitmapBeforeList: MutableList<Bitmap> = ArrayList() // Icon image list (before selection) val iconBitmapAfterList: MutableList<Bitmap> = ArrayList() // Icon image list (after selection) var fragmentClassList: MutableList<Class<*>> = ArrayList() // fragment class list var buttomOrientation = HORIZONTAL // Orientation var titleSizeInDp = 12 // title size var iconWidth = 22 // icon width var iconHeight = 22 // icon height var titleIconMargin = 2 // Spacing between icon and title var titleColorBefore = Color.parseColor("#515151") //The color of the title before it is selected var titleColorAfter = Color.parseColor("#ff2704") //The color of the title after it is selected var currentCheckedIndex = 0 //The number of currently selected items val paint = Paint() // Draw a picture val iconRectList: MutableList<Rect> = ArrayList() // Image space of the icon //Set the control id (FrameLayout) where the fragment is stored fun setContainer(containerId: Int): BottomBar { this.containerId = containerId return this } fun getCurrentFragmentByIndex(index: Int): Fragment { return fragmentList[index] } @JvmName("getCurrentFragment1") fun getCurrentFragment(): Fragment? { return currentFragment } @JvmName("getCurrentCheckedIndex1") fun getCurrentCheckedIndex(): Int { return currentCheckedIndex } //Interface ----------------------------------------------- var switchListener: OnSwitchListener? = null interface OnSwitchListener { fun result(currentFragment: Fragment?) } fun setOnSwitchListener(listener:OnSwitchListener) { this.switchListener=listener } //Set direction fun setOrientation(orientation:Int):BottomBar{ this.buttomOrientation=orientation return this } //Set the title color of selected/unselected fun setTitleBeforeAndAfterColor(beforeResCode: String?, AfterResCode: String?): BottomBar { //Supports the form "#333333" titleColorBefore = Color.parseColor(beforeResCode) titleColorAfter = Color.parseColor(AfterResCode) return this } //Set title size fun setTitleSize(titleSizeInDp: Int): BottomBar? { this.titleSizeInDp = titleSizeInDp return this } //Set the width of the icon fun setIconWidth(iconWidth: Int): BottomBar? { this.iconWidth = iconWidth return this } //Set the height of the icon fun setIconHeight(iconHeight: Int): BottomBar? { this.iconHeight = iconHeight return this } //Set the spacing between the icon and title fun setTitleIconMargin(titleIconMargin: Int): BottomBar? { this.titleIconMargin = titleIconMargin return this } //Add page (initialize fragment first and then add) fun addItem(fragment: Fragment, title: String, iconResBefore: Int, iconResAfter: Int): BottomBar { fragmentList.add(fragment) // Add fragment titleList.add(title) //Add title iconResBeforeList.add(iconResBefore) //Add the icon resource before selection iconResAfterList.add(iconResAfter) //Add icon resource after selection return this } //Add page (add fragment class) fun addItem(fragmentClass: Class<*>, title: String, iconResBefore: Int, iconResAfter: Int): BottomBar? { fragmentClassList.add(fragmentClass) titleList.add(title) iconResBeforeList.add(iconResBefore) iconResAfterList.add(iconResAfter) return this } //Set preferences fun setFirstChecked(firstCheckedIndex: Int): BottomBar { this.firstCheckedIndex = firstCheckedIndex return this } //Construction (used to initialize the fragment before adding it) fun buildWithEntity() { itemCount = fragmentList.size // Pre-create bitmap Rect and cache it // Pre-create the Rect of the icon and cache it for (i in 0 until itemCount) { val beforeBitmap: Bitmap = getBitmap(iconResBeforeList[i])!! iconBitmapBeforeList.add(beforeBitmap) val afterBitmap: Bitmap = getBitmap(iconResAfterList[i])!! iconBitmapAfterList.add(afterBitmap) val rect = Rect() // Create a rectangular space iconRectList.add(rect) } // initParamHorizontal() currentCheckedIndex = firstCheckedIndex switchFragment(currentCheckedIndex) invalidate() } //Construction (used to add fragment classes and create objects through reflection. This does not apply if parameters need to be passed for fragment initialization) fun buildWithClass() { itemCount = fragmentClassList.size // Pre-create bitmap Rect and cache it // Pre-create the Rect of the icon and cache it for (i in 0 until itemCount) { val beforeBitmap: Bitmap = getBitmap(iconResBeforeList[i])!! iconBitmapBeforeList.add(beforeBitmap) val afterBitmap: Bitmap = getBitmap(iconResAfterList[i])!! iconBitmapAfterList.add(afterBitmap) val rect = Rect() iconRectList.add(rect) val clx = fragmentClassList[i] try { val fragment = clx.newInstance() as Fragment fragmentList.add(fragment) } catch (e: Exception) { e.printStackTrace() } } // initParamHorizontal() currentCheckedIndex = firstCheckedIndex switchFragment(currentCheckedIndex) invalidate() } // Get the image from the resource private fun getBitmap(resId: Int): Bitmap? { val bitmapDrawable = context!!.resources.getDrawable(resId) as BitmapDrawable return bitmapDrawable.bitmap } private var parentItemWidth = 0 // width of a single option private var titleBaseLine = 0 // Baseline of the first title (same when horizontal) private val titleXList : MutableList<Int> = ArrayList() // The starting point of the x-axis for each title private var parentItemHeight = 0 //Height of a single option private val titleBaseLines : MutableList<Int> = ArrayList() // Baseline of each title (when vertical) // Initialization parameters private fun initParamHorizontal() { if (itemCount != 0) { //Single option width and height parentItemWidth = getWidth() / itemCount // Calculate the width of a single option: the total width of the view/number of pages parentItemHeight = getHeight() // The height of the view is the height of the item //Icon width and height val iconWidth: Int = dp2px(iconWidth.toFloat()) val iconHeight: Int = dp2px(iconHeight.toFloat()) //Icon and title margin val textIconMargin: Int = dp2px(titleIconMargin.toFloat() / 2) // Specify 5dp first. Divide it by half here to get the normal margin. I don’t know why. It may be because of the picture. // title height val titleSize: Int = dp2px(titleSizeInDp.toFloat()) paint.textSize = titleSize.toFloat() val rect = Rect() paint.getTextBounds(titleList[0], 0, titleList[0].length, rect) // Determine the space occupied by the text before drawing the text val titleHeight = rect.height() // Calculate the starting top coordinates of the icon and the baseLine of the text val iconTop = (parentItemHeight - iconHeight - textIconMargin - titleHeight) / 2 // Total height - icon height - icon title spacing - title height titleBaseLine = parentItemHeight - iconTop //Assign values to the parameters of icon's rect val firstRectX = (parentItemWidth - iconWidth) / 2 // The left starting point of the first icon // Calculate the upper left and lower right of each icon in turn for (i in 0 until itemCount) { val rectX = i * parentItemWidth + firstRectX val temp = iconRectList[i] temp.left = rectX temp.top = iconTop temp.right = rectX + iconWidth temp.bottom = iconTop + iconHeight } titleXList.clear() // Calculate the x-axis starting point of each title in turn for (i in 0 until itemCount) { val title = titleList[i] paint.getTextBounds(title, 0, title.length, rect) // Determine the space occupied by the text before drawing the text, and store the spatial data in rect titleXList.add( ((parentItemWidth - rect.width())/2) + (parentItemWidth*i) ) // (total width - text length)/2 } } } private fun initParamVertical() { if (itemCount != 0) { //Single option width and height parentItemHeight = getHeight() / itemCount // Calculate the height of a single option: the total width of the view/number of pages parentItemWidth = getWidth() // The width of the view is the width of the item //Icon width and height val iconWidth: Int = dp2px(iconWidth.toFloat()) val iconHeight: Int = dp2px(iconHeight.toFloat()) //Icon and title margin val textIconMargin: Int = dp2px(titleIconMargin.toFloat() / 2) // Specify 5dp first. Divide it by half here to get the normal margin. I don’t know why. It may be because of the picture. // title height val titleSize: Int = dp2px(titleSizeInDp.toFloat()) paint.textSize = titleSize.toFloat() val rect = Rect() paint.getTextBounds(titleList[0], 0, titleList[0].length, rect) // Determine the space occupied by the text before drawing the text val titleHeight = rect.height() // Calculate the baseline for each title in turn val iconTop = (parentItemHeight - iconHeight - textIconMargin - titleHeight) / 2 // Total height - icon height - icon title spacing - title height titleBaseLine = parentItemHeight - iconTop titleBaseLines.clear() for (i in 0 until itemCount){ titleBaseLines.add(titleBaseLine + (i*parentItemHeight)) } //Assign values to the parameters of icon's rect val firstRectX = (parentItemWidth - iconWidth) / 2 // The left starting point of the first icon // Calculate the upper left and lower right of each icon in turn for (i in 0 until itemCount) { val temp = iconRectList[i] temp.left = firstRectX temp.top = iconTop + (i*parentItemHeight) temp.right = firstRectX + iconWidth temp.bottom = iconTop + (i*parentItemHeight) + iconHeight } titleXList.clear() // Calculate the x-axis starting point of each title in turn for (i in 0 until itemCount) { val title = titleList[i] paint.getTextBounds(title, 0, title.length, rect) // Determine the space occupied by the text before drawing the text, and store the spatial data in rect titleXList.add( (parentItemWidth - rect.width())/2) // (total width - text length)/2 } } } fun dp2px(dp: Float): Int { val scale = context.resources.displayMetrics.density return (dp * scale + 0.5f).toInt() } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if(buttomOrientation== HORIZONTAL){ initParamHorizontal() }else{ initParamVertical() } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //Here let the view draw the background for us if specified. if (itemCount != 0) { // draw icon paint.isAntiAlias = false // Turn off anti-aliasing for (i in 0 until itemCount) { var bitmap: Bitmap? = if (i==currentCheckedIndex) { iconBitmapAfterList[i] } else { iconBitmapBeforeList[i] } val rect = iconRectList[i] bitmap?.let { canvas.drawBitmap(it, null, rect, paint) } // Fill the image resources to the corresponding position } // draw title paint.isAntiAlias = true // Turn on anti-aliasing for (i in 0 until itemCount) { val title = titleList[i] if (i == currentCheckedIndex) { paint.color = titleColorAfter } else { paint.color = titleColorBefore } if (titleXList.size == itemCount) { val x = titleXList[i] var y = if(buttomOrientation== HORIZONTAL) titleBaseLine.toFloat() else titleBaseLines[i].toFloat() canvas.drawText(title, x.toFloat(), y, paint) } } } } // I observed Weibo and Zhangmeng and found that the response is only when pressing and releasing in the same area. If you press this area and then move your finger out and then release it, there will be no response. var target = -1 @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { // press MotionEvent.ACTION_DOWN -> { var x = event.x.toInt() var y = event.y.toInt() var number = if(buttomOrientation== HORIZONTAL) withinWhichArea(x) else withinWhichArea(y) Log.i(TAG, "Click on the x-axis: " + x + " / Click on the y-axis: " + y + "/" + number) target=number } // Release MotionEvent.ACTION_UP -> { // Make sure it is within this view if ( if(buttomOrientation== HORIZONTAL) event.y > 0 else event.x > 0 ) { // Still in this button when released if (if(buttomOrientation== HORIZONTAL) target == withinWhichArea(event.x.toInt()) else target == withinWhichArea(event.y.toInt()) ) { switchFragment(target) //Switch page currentCheckedIndex = target //Modify the currently selected item invalidate() // refresh } target=-1 } } } return true // Why can't up execute return super here? It's because the value of return super all depends on whether you are clickable. When the down event comes, it is not clickable, so return false. In other words, you have not set onTouchListener, and the control is ENABLE, so the return value of dispatchTouchEvent is also false. , so the dispatchTransformedTouchEvent in the view group also returns false. In this way, the first touch target in the view group is empty, so the intercept flag is decisively false, and then the step of looping to get the children can no longer be entered. Directly Calling dispatch-TransformedTouchEvent and passing in the child item is null, so the view group's own dispatch-TouchEvent is called directly. } // Calculate the currently clicked area through the x-axis coordinate of the click // private fun withinWhichArea(x: Int): Int { return x / parentItemWidth } val withinWhichArea = { x: Int -> if(buttomOrientation== HORIZONTAL) x / parentItemWidth else x / parentItemHeight} var currentFragment: Fragment? = null //The current fragment page // Note that only the AndroidX version is supported here. Old versions can be modified by yourself. private fun switchFragment(whichFragment: Int) { val fragment = fragmentList[whichFragment] var transactionx = (context as AppCompatActivity).supportFragmentManager.beginTransaction() if (fragment.isAdded) { if (currentFragment != null) { transactionx.hide(currentFragment!!).show(fragment) } else { transactionx.show(fragment) } } else { if (currentFragment != null) { transactionx.hide(currentFragment!!).add(containerId, fragment) } else { transactionx.add(containerId, fragment) } } currentFragment = fragment transactionx.commit() switchListener?.result(currentFragment) } // Initialization parameters fun clear() { firstCheckedIndex = 0 itemCount = 0 titleList.clear() iconResBeforeList.clear() iconResAfterList.clear() iconBitmapBeforeList.clear() iconBitmapAfterList.clear() fragmentClassList.clear() buttomOrientation = HORIZONTAL titleSizeInDp = 12 iconWidth = 22 iconHeight = 22 titleIconMargin=2 currentCheckedIndex = 0 iconRectList.clear() parentItemWidth = 0 titleBaseLine = 0 titleXList.clear() parentItemHeight = 0 titleBaseLines.clear() target=-1 if (fragmentList.size > 0) { val transaction = (context as AppCompatActivity?)!!.supportFragmentManager.beginTransaction() for (fragment in fragmentList) { transaction.remove(fragment) } transaction.commit() } fragmentList.clear() } }
3. Usage examples
xml layout file:
<?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"> <!-- Portrait --> <!-- <LinearLayout--> <!-- android:layout_width="match_parent"--> <!-- android:layout_height="match_parent"--> <!-- android:orientation="horizontal">--> <!-- <com.example.lxt.View.BottomBar--> <!-- android:id="@ + id/bottomBar"--> <!-- android:layout_width="50dp"--> <!-- android:layout_height="match_parent"/>--> <!-- <FrameLayout--> <!-- android:id="@ + id/fragment"--> <!-- android:layout_width="match_parent"--> <!-- android:layout_height="match_parent"--> <!-- android:orientation="vertical">--> <!-- </FrameLayout>--> <!-- </LinearLayout>--> <!-- Horizontal --> <FrameLayout android:id="@ + id/fragment" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical"> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.lxt.View.BottomBar android:id="@ + id/bottomBar" android:layout_width="match_parent" android:layout_height="50dp"/> </LinearLayout> </LinearLayout>
java:
public void init_fragment(){ accountInfoFrag = new AccountInformationFragment(); liveInfoFrag = new LiveInformationFragment(); bottomBar.setContainer(R.id.fragment) //Set the container control .setOrientation(BottomBar.HORIZONTAL) //Set the direction .setFirstChecked(0) // Set preferences .setTitleBeforeAndAfterColor("#7f7f7f", "#00BFFF") //Set the color of selected and unselected titles .addItem(accountInfoFrag,"Account Information",R.mipmap.account_info_keyup, R.mipmap.account_info_keydown) //Add page: fragment object, title name, icon before selection, icon after selection .addItem(liveInfoFrag,"Other information",R.mipmap.other_info_keyup, R.mipmap.other_info_keydown) //Add page .addItem(accountInfoFrag,"Account Information",R.mipmap.account_info_keyup, R.mipmap.account_info_keydown) // Add page .addItem(liveInfoFrag,"Other information",R.mipmap.other_info_keyup, R.mipmap.other_info_keydown) //Add page .addItem(accountInfoFrag,"Account Information",R.mipmap.account_info_keyup, R.mipmap.account_info_keydown) // Add page .buildWithEntity(); // Build }
Finish spreading flowers