Why does BottomSheetDialog draw under the navigation bar in landscape? - android

I am showing a simple Android modal bottom sheet menu. In portrait mode, it works fine, but when the device is rotated to landscape the bottom of the sheet draws under the navigation bar and is hidden. This is using a tablet emulator (Pixel C, API 32).
Portrait mode: correct
Landscape mode: bottom of sheet is hidden
Sample code, using the Android Studio "Empty Activity" template project and code from the Material docs.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.showDialog.setOnClickListener { showBottomSheetDialog() }
}
private fun showBottomSheetDialog() {
val modalBottomSheet = ModalBottomSheet()
modalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)
}
}
class ModalBottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.bottom_sheet_dialog, container, false)
companion object {
const val TAG = "ModalBottomSheet"
}
}
bottom_sheet_dialog.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="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/text_view_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:paddingVertical="12dp"
android:text="First Row"
android:textSize="16sp" />
<TextView
android:id="#+id/text_view_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:paddingVertical="12dp"
android:text="Second Row"
android:textSize="16sp" />
<TextView
android:id="#+id/text_view_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:paddingVertical="12dp"
android:text="Third Row"
android:textSize="16sp" />
</LinearLayout>

This appears to be intentional Android tablet UI behaviour. (It's a mystery why it is desirable to start a bottom sheet off in a state where the user can't see its content, but anyway...)
Fixed by adding this into the ModalBottomSheet class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.setOnShowListener {
(it as? BottomSheetDialog)?.apply {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
With thanks to the answers posted in Why does BottomSheetDialog draw under the navigation bar in landscape?

I feel the screen can render only 2 times space in Landscape mode as per your design.
Try to provide scrolling feature so that it will support in both Portrait and Landscape mode even in future if you add more items to BottomSheet.

Related

DialogFragment custom view no background

I'm trying to inflate a custom view in a DialogFragment but the container view's are not showing up. Note I haven't found any post that matches this exactly, so if there is, please share, TIA.
here is my XML (very simple)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="#drawable/billnotifselectiondialog"
android:layout_marginHorizontal="45dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="#+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:background="#color/secondary_200"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My Class:
class BillNotifSelectionDialog: DialogFragment() {
private lateinit var binding: TestDialogLayoutBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = TestDialogLayoutBinding.inflate(LayoutInflater.from(context),container, false).apply {
binding = this
}.root
}
I'm invoking the class in my fragment with
BillNotifSelectionDialog().show(parentFragmentManager, "tag")
Here is the result, I can only see the button, not the ConstraintLayout:
This is a common issue of using ConstraintLayout as the root layout of DialogFragments.
One fix is to Replace it with RelativeLayout; but it's not recommended as per documentation quote:
For better performance and tooling support, you should instead build your layout with ConstraintLayout.
Instead of that you'd fix it programmatically by designating the dialog layout to match the parent with dialog.window.setLayout:
class BillNotifSelectionDialog : DialogFragment() {
//..... rest of code omitted
override fun onStart() {
super.onStart()
dialog!!.window!!.setLayout(MATCH_PARENT, MATCH_PARENT)
}
}
you need to override onCreateDialog instead of onCreateView in DialogFragment.
class BillNotifSelectionDialog : DialogFragment() {
private lateinit var binding: TestDialogLayoutBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = TestDialogLayoutBinding.inflate(layoutInflater)
return AlertDialog.Builder(requireContext())
.setView(binding.root)
.create()
}
to test the result, I changed your xml code that the background color is teal_200 and button color is purple_700
the code result

setOnClickListener doesn't work using Fragment

I'm trying to make a simple button that changes the fragment, it doesn't give any error, it just doesn't work.
XML code:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.follow.FollowFragment">
<ImageView
android:id="#+id/imageView4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="background"
android:scaleType="fitXY"
android:src="#drawable/background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="#+id/loginButton"
android:layout_width="250dp"
android:layout_height="60dp"
android:layout_marginTop="50dp"
android:backgroundTint="#color/btn_col"
android:text="Log In" />
</LinearLayout>
</FrameLayout>
Fragment code:
class FollowFragment : Fragment() {
companion object {
fun newInstance() = FollowFragment()
}
private lateinit var viewModel: FollowViewModel
private lateinit var loginButton: Button
private lateinit var viewFollow: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_follow, container, false)
viewFollow = root
loginButton = viewFollow.findViewById(R.id.loginButton)
var loginFragment = LoginFragment()
println("Hello :(")
loginButton.setOnClickListener {
println("Hello :)")
//parentFragmentManager.beginTransaction().replace(R.id.nav_host_fragment, loginFragment).commit()
}
return inflater.inflate(R.layout.fragment_follow, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
// TODO: Use the ViewModel
}
}
Only the sad hello is printed, it seems to recognize the button, but the listener don't work. The app has a bottom navigation bar, and the screens are fragments.
By returning inflater.inflate(R.layout.fragment_follow, ...) you're inflating a second layout to be used for your fragment instead of the one that you're using to set the listener.
You should be returning root.

Kotlin doOnLayout not setting visibility.GONE

I have a custom ConstraintLayout Myword. when I set the visibility of myword1 to GONE inside kotlin fun doOnLayout, it becomes invisible but myword2 doesn't move down.
But if I set its visibility to GONE in XML or outside doOnLayout, it works as intended.
<learnprogramming.academy.relaf.Myword
android:id="#+id/myword1"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<learnprogramming.academy.relaf.Myword
android:id="#+id/myword2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="#id/myword1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Source code:
lateinit var bannerword : MutableList<Myword>
lateinit var swippon : ConstraintLayout
fun updatewords{
swippon.doOnLayout {
if (swippon.height>1000) bannerword[0].visibility = View.GONE}
class PhrasesFragment: Fragment() {
companion object {
fun newInstance(): PhrasesFragment {
return PhrasesFragment()
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.inglesa_screen, container, false) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bannerword=mutableListOf(myword1,myword2)
swippon=parent
updatewords()
}
}
doOnLayout function when layout is laid out calculates parent Layout height and eventually removes myword1. I actually solved changing doOnLayout with Handler(Looper.getMainLooper()).post{} but is wrong with doOnLayout? How should i have used it in the correct way?

BottomSheetDialogFragment is not showing up

I've followed this tutorial to implement BottomSheetDiaogFragment in my android application.
this is my bottom sheet layout (bottom_sheet.xml):
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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="wrap_content">
<RadioGroup
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioButton
android:id="#+id/rb1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_weight="1"
android:text="#string/rb1" />
<RadioButton
android:id="#+id/rb2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_weight="1"
android:text="#string/rb2" />
</RadioGroup>
</android.support.constraint.ConstraintLayout>
BottomSheetDialogFragment class:
class BottomSheetTaskRepeat : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_sheet, container, false)
}
}
activity:
private val bottomSheetTaskRepeat = BottomSheetTaskRepeat()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bottomSheetTaskRepeat.show(supportFragmentManager, "my_bottom_sheet")
}
The problem is that the bottom sheet is not showing up! Any help is appreciated.
This is a late Answer, I am writing for anyone who will face the same problem, This is what I found:
For some reasons non-constrained views heights do not work in BottomSheetDialogFragment. A view whose height is like wrap_content will not show.(But the shadow will be there), but when you specify its height like 80dp it works.
For this question go and change your RadioGroup height and specify it like to:
android:layout_height="200dp"
Hope that is helpful.
UPDATE:
Since the default container of BottomSheetDailogFragment is FrameLayout and is set to WRAP_CONTENT, You can override that on your Fragment onStart method like this (Kotlin):
override fun onStart() {
super.onStart()
val containerID = com.google.android.material.R.id.design_bottom_sheet
val bottomSheet: FrameLayout? = dialog?.findViewById(containerID)
bottomSheet?.let {
BottomSheetBehavior.from<FrameLayout?>(it).state =
BottomSheetBehavior.STATE_HALF_EXPANDED
bottomSheet.layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
}
view?.post {
val params = (view?.parent as View).layoutParams as (CoordinatorLayout.LayoutParams)
val behavior = params.behavior
val bottomSheetBehavior = behavior as (BottomSheetBehavior)
bottomSheetBehavior.peekHeight = view?.measuredHeight ?: 0
(bottomSheet?.parent as? View)?.setBackgroundColor(Color.TRANSPARENT)
}
}
Try following:
Create and use the parameterless static method newInstance in your BottomSheetDialogFragment and call the show() method on it.
Try LinearLayoutCompat as a root layout instead of ConstraintLayout.
Try a colourful background to the root layout to get an idea.
Try match_parent height for the root layout.
Make sure that the dismiss() or cancel() is not being called on it immediately.
Check visibility.
If it contains a recyclerView, make sure that it has items, getItemCount is not returning 0, and that we are setting up the values properly!
Restart both: PC and Device if your changes are not reflecting because of Android Studio errors.
Instead of using supportFragmentManager you have to use childFragmentManager,if you are inflating from inner fragments.
private val bottomSheetTaskRepeat = BottomSheetTaskRepeat()
bottomSheetTaskRepeat.show(childFragmentManager, "my_bottom_sheet")

Fragment with ViewPager having blank pages after returning to it from another fragment

In my application I'm trying to create 2 fragments with ViewPagers. The fragments have 3 tabs/pages each with RecyclerViews and they are supposed to be backed by a database, so I'm keeping them in a List.
class MainActivity : AppCompatActivity() {
val fragments = listOf(SwipeFragment(), SwipeFragment())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
button1.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
}
button2.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[1]).commit()
}
}
}
The problem is that after navigating away from the SwipeFragment and going back, the view seems empty. Then, if you navigate to e.g. the leftmost page, the rightmost one "appears" (when you go to it).
This results in the middle page staying empty. (the reason for it being that FragmentStatePagerAdapter keeps only the current page and 2 adjacent ones by default. The middle one doesn't get refreshed in a layout with 3 tabs - I tried it also with 5 tabs and I'm able to bring all of them back by going back and forth).
After some debugging I saw that the fragments that represent pages don't get removed from the FragmentManager, but the main SwipeFragment is. I can't get my head around how FragmentManager really works, even after reading almost all of the source code.
If I were able to somehow safely remove the fragments from the FragmentManager, it may work.
I've tried some techniques for saving state, but the framework does that automatically (which might actually be the cause of this problem).
I'm just a beginner in Android, so there's probably a better way to do this anyway. I'll leave the rest of the files here for reference.
activity_main.xml
<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.example.test.MainActivity">
<FrameLayout
android:id="#+id/placeholder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Button
android:id="#+id/button1"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button1" />
<Button
android:id="#+id/button2"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button2" />
</LinearLayout>
</LinearLayout>
SwipeFragment.kt
class SwipeFragment : Fragment() {
// TODO: set up and keep the database here
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_swipe, container, false)
view.tab_layout.setupWithViewPager(view.pager)
view.pager.adapter = DummyPagerAdapter(activity.supportFragmentManager, index)
return view
}
}
fragment_swipe.xml
<android.support.v4.view.ViewPager
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/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.SwipeFragment">
<android.support.design.widget.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.view.ViewPager>
ItemListFragment.kt
class ItemListFragment() : Fragment() {
// Or keep the database here?
// Probably not the best idea though
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_item_list, container, false)
if (view is RecyclerView) {
view.layoutManager = LinearLayoutManager(view.context)
view.adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
}
return view
}
}
DummyPagerAdapter.kt
class DummyPagerAdapter(manager: FragmentManager, val parentIndex: Int) :
FragmentStatePagerAdapter(manager) {
override fun getItem(position: Int): Fragment = ItemListFragment()
override fun getPageTitle(position: Int): CharSequence = "${ position + 1 }"
override fun getCount(): Int = 3
}
And a basic implementation of RecyclerView.Adapter generated by Android Studio
MyItemRecyclerViewAdapter.kt
class MyItemRecyclerViewAdapter(val values: List<DummyItem>) :
RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
...
}
fragment_item_list.xml
<android.support.v7.widget.RecyclerView 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/list"
android:name="com.example.test.ItemListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:context="com.example.test.ItemListFragment"
tools:listitem="#layout/fragment_item" />
You should use childFragmentManager instead of activity.supportFragmentManager inside SwipeFragment.
view.pager.adapter = DummyPagerAdapter(childFragmentManager, index)
The difference between getSupportFragmentManager() and getChildFragmentManager() is discussed here.
Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.
In my case childFragmentManager didn't help. I think we can use a pattern "Observer" to update existing fragments of ViewPager with new data when return from another fragment.

Categories

Resources