I am working on chat app so I want time TextView to be like whats app it's constraint can change depend on the text
So I tried the view tree observer in the message layout and it somehow worked but there something missing
private fun adjustTimeTextView() {
var freeSpace: Float
var lines: Int
val messageViewObserver = messageView.messageLayout.viewTreeObserver
messageViewObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
messageView.messageLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
val viewWidth = messageView.messageLayout.width
Log.e("message view width", viewWidth.toString())
val layout = messageView.messageTextTextView.layout
if (layout != null) {
lines = layout.lineCount
Log.e("line", lines.toString())
val offset = layout.getLineWidth(lines - 1)
freeSpace = viewWidth - offset
if (freeSpace < 220) {
Log.e("minmum", "low free space")
val constraintSet = ConstraintSet()
constraintSet.clone(messageView.messageLayout)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.TOP)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.BOTTOM)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.START)
constraintSet.connect(
messageView.messageLayout.messageTimeTextView.id,
ConstraintSet.TOP,
messageView.messageLayout.messageTextTextView.id,
ConstraintSet.BOTTOM
)
constraintSet.applyTo(messageView.messageLayout)
} else {
if (lines > 1) {
val constraintSet = ConstraintSet()
constraintSet.clone(messageView.messageLayout)
constraintSet.clear(
messageView.messageLayout.messageTimeTextView.id,
ConstraintSet.START
)
constraintSet.applyTo(messageView.messageLayout)
}
}
}
}
})
}
and here is my layout 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<android.support.constraint.Guideline
android:id="#+id/endGuideline"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.65"/>
<android.support.constraint.Guideline
android:id="#+id/startGuideline"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.35"/>
<android.support.constraint.ConstraintLayout
android:id="#+id/messageLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constrainedWidth="true"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="#+id/endGuideline"
android:background="#drawable/sender_message_background"
android:orientation="vertical"
app:layout_constraintWidth_default="wrap"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toBottomOf="#+id/messageDateTextView" android:layout_marginTop="8dp">
<TextView
android:id="#+id/messageTextTextView"
tools:text="hi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" android:layout_marginEnd="8dp"/>
<TextView
android:id="#+id/messageTimeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12:34 pm"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="#+id/messageTextTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/messageTextTextView"
app:layout_constraintStart_toEndOf="#+id/messageTextTextView"
android:layout_marginStart="8dp" app:layout_constraintVertical_bias="0.51"/>
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
The problem is that when I open an existing chat the constraint as first image appear didn't work as expected in the long message but as soon as I click on type a message edit text the constraint changed for the screen chat not for all chat so if i scrolled the constraints of chat on screen is unchanged unless i click on type a message edit text and so on.
am i missing listener or request focus or something else or what is missing
Here is picture one
Here is picture two
In my opinion, the solution here is to use app:layout_constrainedWidth="true". Requesting layout is a heavy task and should not be performed on scroll.
it worked by make recyclerView request layout
i added this code to recyclerview adapter
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
recyclerView.requestLayout()
}
})
}
Related
I am trying to create a horizontal recycler view with dynamic height by following this stack overflow post. The solution seems working. But the recycler view items disappear when I try to remove an item from recycler view.
recyclerview layout:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingStart="13dp"
android:paddingEnd="13dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Recycler view item layout:
<androidx.cardview.widget.CardView
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="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
app:cardBackgroundColor="#BCAAAA"
app:cardElevation="2dp">
<TextView
android:padding="36dp"
android:id="#+id/textViewName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="22sp"
tools:text="Test String"/>
</androidx.cardview.widget.CardView>
Activity:
val flexBoxLayoutManager = FlexboxLayoutManager(this, FlexDirection.ROW, FlexWrap.NOWRAP)
with(recyclerView) {
layoutManager = flexBoxLayoutManager
adapter = RecyclerViewAdapter()
setHasFixedSize(false)
}
Adapter:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var textViewName: TextView = itemView.findViewById(R.id.textViewName)
init {
updateLayoutParamsToAllowHorizontalScrolling()
}
private fun updateLayoutParamsToAllowHorizontalScrolling() {
(itemView.layoutParams as? FlexboxLayoutManager.LayoutParams)?.let {
it.flexShrink = 0.0f
it.alignSelf = AlignItems.FLEX_START
}
}
}
override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
val item = listItems[position]
var msg = "Sample item no:$position "
for(i in 0..position){
msg += "dynamic content \n"
}
holder.textViewName.text = msg
holder.itemView.setOnClickListener {
val pos = listItems.indexOf(item)
listItems.removeAt(pos)
notifyItemRemoved(pos)
}
}
A screen recording of the output:
Is there any way to fix this issue? Or Are there any other approach to implement horizontal recyclerveiw with dynamic height?
I have fixed the issue without using FlexBoxLayoutManger.
Used LinearLayoutManager with horizontal orientation and the following code in onBindViewHolder in RecyclerViewAdapter.
holder.itemView.post {
val wMeasureSpec =
View.MeasureSpec.makeMeasureSpec(holder.itemView.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
holder.itemView.measure(wMeasureSpec, hMeasureSpec)
if (holder.itemView.measuredHeight > holder.itemView.height) {
holder.itemView.layoutParams =
(holder.itemView.layoutParams as ViewGroup.LayoutParams)
.apply {
height = holder.itemView.measuredHeight
}
}
}
I have an activity with recycleView this is what I should show in design
I use this code to show it
val arr:ArrayList<string> = arrayListOf("English","Intermediate","English","English","arr","UICollectionViewFlowLayoutFlowFlowFlow","English","UICollectionViewDelegate","English","Intermediate","UIViewController","viewDidLoad","Intermediate","String","Intermediate","arr","Intermediate","UIKit","Intermediate","English","columnLayout","English","languageLabel")
recyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL)
recyclerView.layoutManager = GridLayoutManager(this, 3)
adaAdapter = CustomAdapter(this, arr)
recyclerView.adapter = adaAdapter
With this adapter
class CustomAdapter(val context: DreamsActivity, val items: ArrayList<String>) : RecyclerView.Adapter<CustomAdapter.ViewHolderProductList>() {
// holds this device's screen width,
private var screenWidth = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderProductList {
val displayMetrics = DisplayMetrics()
context.windowManager.getDefaultDisplay().getMetrics(displayMetrics)
screenWidth = displayMetrics.widthPixels
val layoutInflater = LayoutInflater.from(parent.context)
val cellForRow = layoutInflater.inflate(R.layout.dreams_collection,parent, false)
val devicewidth: Int = (displayMetrics.widthPixels / 3) - 10
val params = cellForRow.getLayoutParams() as GridLayoutManager.LayoutParams
params.width = devicewidth
cellForRow.setLayoutParams(params)
return ViewHolderProductList(cellForRow)
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolderProductList, position: Int) {
val item = items[position]
holder.typeTv.text = item
}
inner class ViewHolderProductList(itemView: View) : RecyclerView.ViewHolder(itemView) {
var typeTv: TextView = itemView.findViewById(R.id.typeTv)
var mainL: ConstraintLayout = itemView.findViewById(R.id.mainL)
}
}
With this XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MainActivity">
<RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="15dp"
android:background="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
And Item layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/mainL"
android:layout_width="60dp"
android:padding="10dp"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/typeTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:text="TextView"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
But I got this
And don't know how to do it as I need the number of items to be dynamic according to the text length but the GridLayoutManager only sets the number of columns static.
Any help is greatly appreciated
The desired layout tends to be Chips rather than a RecyclerView
Here is a demo:
Material app gradle dependency: implementation 'com.google.android.material:material:1.4.0'
Layout:
<com.google.android.material.chip.ChipGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/my_chip_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
app:chipSpacing="16dp"
app:chipSpacingHorizontal="4dp"
app:chipSpacingVertical="4dp" />
Adding chips:
val arr =
arrayListOf(
"English",
"Intermediate",
"English",
"English",
"arr",
"UICollectionViewFlowLayoutFlowFlowFlow",
"English",
"UICollectionViewDelegate",
"English",
"Intermediate",
"UIViewController",
"viewDidLoad",
"Intermediate",
"String",
"Intermediate",
"arr",
"Intermediate",
"UIKit",
"Intermediate",
"English",
"columnLayout",
"English",
"languageLabel"
)
fun addChips(context: Context) {
val chipGroup = findViewById<ChipGroup>(R.id.my_chip_group)
for (i in 0 until arr.size) {
val chip = Chip(context)
chip.layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
chip.text = arr[i]
chipGroup.addView(chip)
}
}
What you're asking for is something with the visual look of a Chip from Material Components. While the documentation for Chips suggests putting them in a ChipGroup, this loses the benefit of recycling views if you have more than enough to cover the screen. On the other hand, if they need to behave like a RadioGroup, where you can select only one to be toggled at a time, then ChipGroup would be easier to use.
To do this with RecylerView, you can change your item view layouts to use a Chip instead of a TextView. Set the item's layout width to wrapContent. Then use Google's Flexbox Layout library to get the layout with variable length items.
In Gradle dependencies: implementation 'com.google.android.flexbox:flexbox:3.0.0'
In your activity:
recyclerView.layoutManager = FlexboxLayoutManager(this)
.apply { flexDirection = FlexDirection.ROW }
Result:
There's a related question here that is helpful.
I am using tab layout with custom views.
The issues I am facing are the following -
1) The text is being cut off with small screen devices as you can see here -
small device -
big device -
2) the second issue I am facing is as you can see, the background does not go wrap content on "Chats" tabs. It does through wrap the "Market" tab title as it is longer. I need it to wrap quallity with short and long texts.
here is my xml -
<androidx.appcompat.widget.Toolbar
android:id="#+id/activity_dashboard_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:titleTextColor="#color/transparent">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:src="#drawable/user_default_image"
app:civ_border_color="#color/black"
app:civ_border_width="1dp" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="#+id/activity_dashboard_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#+id/activity_dashboard_toolbar"
app:tabIndicator="#null" />
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/activity_dashboard_viewpager"
android:layout_width="match_parent"
android:layout_height="600dp"
app:layout_constraintTop_toBottomOf="#+id/activity_dashboard_tablayout" />
private fun initTabLayoutAndViewPager() {
setSupportActionBar(toolbar)
supportActionBar?.title = null
viewpager.adapter = DashboardViewPagerAdapter(this)
TabLayoutMediator(tabLayout, viewpager, TabLayoutMediator.TabConfigurationStrategy { _, _ -> }).attach()
val chatsView = View.inflate(this, R.layout.dashboard_activity_cusom_tab, null)
val callsView = View.inflate(this, R.layout.dashboard_activity_cusom_tab, null)
val walletView = View.inflate(this, R.layout.dashboard_activity_cusom_tab, null)
val marketView = View.inflate(this, R.layout.dashboard_activity_cusom_tab, null)
(chatsView as TextView).text = pageTitles[0]
(callsView as TextView).text = pageTitles[1]
(walletView as TextView).text = pageTitles[2]
(marketView as TextView).text = pageTitles[3]
chatTextView = chatsView
callsTextView = callsView
walletTextView = walletView
marketTextView = marketView
tabLayout.getTabAt(0)?.customView = chatTextView
tabLayout.getTabAt(1)?.customView = callsTextView
tabLayout.getTabAt(2)?.customView = walletTextView
tabLayout.getTabAt(3)?.customView = marketTextView
viewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
when(position) {
DashboardTabs.CHATS.type -> {
ViewPagerUtils.setSelectedTab(chatTextView, callsView, walletView, marketView)
}
DashboardTabs.CALLS.type -> {
ViewPagerUtils.setSelectedTab(callsView, chatTextView, walletView, marketView)
}
DashboardTabs.WALLET.type -> {
ViewPagerUtils.setSelectedTab(walletView, callsView, chatTextView, marketView)
}
DashboardTabs.MARKET.type -> {
ViewPagerUtils.setSelectedTab(marketView, callsView, walletView, chatTextView)
}
}
}
})
}
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="#font/roboto_medium"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textStyle="bold"
android:textSize="18sp"
android:maxLines="1"
tools:text="Chats">
</TextView>
edit -
After resolving the issue with the answer provided here I am stuck a gravity issue -
how can I make all tabs in the center? tried both gravity and layout_gravity and none helped me.
make tab layout mode scrollable app:tabMode="scrollable" or app:tabMode="auto" like below
<com.google.android.material.tabs.TabLayout
android:id="#+id/activity_dashboard_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"
app:layout_constraintTop_toBottomOf="#+id/activity_dashboard_toolbar"
app:tabIndicator="#null" />
Inside a ScrollView I am dynamically switching between two fragments with different heights.
Unfortunately that leads to jumping. One can see it in the following animation:
I am scrolling down until I reach the button "show yellow".
Pressing "show yellow" replaces a huge blue fragment with a tiny yellow fragment. When this happens, both buttons jump down to the end of the screen.
I want both buttons to stay at the same position when switching to the yellow fragment. How can that be done?
Source code available at https://github.com/wondering639/stack-dynamiccontent respectively https://github.com/wondering639/stack-dynamiccontent.git
Relevant code snippets:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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/myScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="0dp"
android:layout_height="800dp"
android:background="#color/colorAccent"
android:text="#string/long_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button_fragment1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="show blue"
app:layout_constraintEnd_toStartOf="#+id/button_fragment2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<Button
android:id="#+id/button_fragment2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="show yellow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/button_fragment1"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#+id/button_fragment2">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.dynamiccontent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// onClick handlers
findViewById<Button>(R.id.button_fragment1).setOnClickListener {
insertBlueFragment()
}
findViewById<Button>(R.id.button_fragment2).setOnClickListener {
insertYellowFragment()
}
// by default show the blue fragment
insertBlueFragment()
}
private fun insertYellowFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, YellowFragment())
transaction.commit()
}
private fun insertBlueFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, BlueFragment())
transaction.commit()
}
}
fragment_blue.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#0000ff"
tools:context=".BlueFragment" />
fragment_yellow.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="#ffff00"
tools:context=".YellowFragment" />
HINT
Please note that this is of course a minimum working example to show off my issue. In my real project, I also have views below the #+id/fragment_container. So giving a fixed size to #+id/fragment_container is not an option for me - it would cause a large blank area when switching to the low, yellow fragment.
UPDATE: Overview of proposed solutions
I implemented the proposed solutions for testing purposes and added my personal experiences with them.
answer by Cheticamp, https://stackoverflow.com/a/60323255
-> available in https://github.com/wondering639/stack-dynamiccontent/tree/60323255
-> FrameLayout wraps content, short code
answer by Pavneet_Singh, https://stackoverflow.com/a/60310807
-> available in https://github.com/wondering639/stack-dynamiccontent/tree/60310807
-> FrameLayout gets the size of the blue fragment. So no content wrapping. When switching to the yellow fragment, there's a gap between it and the content following it (if any content follows it). No additional rendering though!
** update ** a second version was provided showing how to do it without gaps. Check the comments to the answer.
answer by Ben P., https://stackoverflow.com/a/60251036
-> available in https://github.com/wondering639/stack-dynamiccontent/tree/60251036
-> FrameLayout wraps content. More code than the solution by Cheticamp. Touching the "show yellow" button twice leads to a "bug" (buttons jump down to the bottom, actually my original issue). One could argue about just disabling the "show yellow" button after switching to it, so I wouldn't consider this a real issue.
Update: To keep the other views right below the framelayout and to handle the scenario automatically, we need to use onMeasure to implement the auto-handling so do the following steps
• Create a custom ConstraintLayout as (or can use MaxHeightFrameConstraintLayout lib):
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import kotlin.math.max
/**
* Created by Pavneet_Singh on 2020-02-23.
*/
class MaxHeightConstraintLayout #kotlin.jvm.JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr){
private var _maxHeight: Int = 0
// required to support the minHeight attribute
private var _minHeight = attrs?.getAttributeValue(
"http://schemas.android.com/apk/res/android",
"minHeight"
)?.substringBefore(".")?.toInt() ?: 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
_minHeight = minHeight
}
var maxValue = max(_maxHeight, max(height, _minHeight))
if (maxValue != 0 && && maxValue > minHeight) {
minHeight = maxValue
}
_maxHeight = maxValue
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
and use it in your layout in place of ConstraintLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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/myScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.pavneet_singh.temp.MaxHeightConstraintLayout
android:id="#+id/constraint"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="0dp"
android:layout_height="800dp"
android:background="#color/colorAccent"
android:text="Some long text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button_fragment1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="show blue"
app:layout_constraintEnd_toStartOf="#+id/button_fragment2"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<Button
android:id="#+id/button_fragment2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="show yellow"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toEndOf="#+id/button_fragment1"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<Button
android:id="#+id/button_fragment3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="show green"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toEndOf="#+id/button_fragment2"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/button_fragment3" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="additional text\nMore data"
android:textSize="24dp"
app:layout_constraintTop_toBottomOf="#+id/fragment_container" />
</com.example.pavneet_singh.temp.MaxHeightConstraintLayout>
</androidx.core.widget.NestedScrollView>
This will keep track of height and apply it during every fragment change.
Output:
Note: As mentioned in comments before, setting minHeight will result in additional rendering pass and it cannot be avoided in the current version of ConstraintLayout.
Old approach with custom FrameLayout
This is an interesting requirement and my approach is to solve it by creating a custom view.
Idea:
My idea for the solution is to adjust the height of the container by keeping the track of the largest child or total height of children in the container.
Attempts:
My first few attempts were based on modifying the existing behaviour of NestedScrollView by extending it but it doesn't provide access to all the necessary data or methods. Customisation resulted in poor support for all scenarios and edge cases.
Later, I achieved the solution by creating a custom Framelayout with different approach.
Solution Implementation
While implementing the custom behaviour of height measurement phases, I dug deeper and manipulated the height with getSuggestedMinimumHeight while tracking the height of children to implement the most optimised solution as it will not cause any additional or explicit rendering because it will manage the height during the internal rendering cycle so create a custom FrameLayout class to implement the solution and override the getSuggestedMinimumHeight as:
class MaxChildHeightFrameLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
// to keep track of max height
private var maxHeight: Int = 0
// required to get support the minHeight attribute
private val minHeight = attrs?.getAttributeValue(
"http://schemas.android.com/apk/res/android",
"minHeight"
)?.substringBefore(".")?.toInt() ?: 0
override fun getSuggestedMinimumHeight(): Int {
var maxChildHeight = 0
for (i in 0 until childCount) {
maxChildHeight = max(maxChildHeight, getChildAt(i).measuredHeight)
}
if (maxHeight != 0 && layoutParams.height < (maxHeight - maxChildHeight) && maxHeight > maxChildHeight) {
return maxHeight
} else if (maxHeight == 0 || maxHeight < maxChildHeight) {
maxHeight = maxChildHeight
}
return if (background == null) minHeight else max(
minHeight,
background.minimumHeight
)
}
}
Now replace the FrameLayout with MaxChildHeightFrameLayout in activity_main.xml as:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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/myScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="0dp"
android:layout_height="800dp"
android:background="#color/colorAccent"
android:text="Some long text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button_fragment1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="show blue"
app:layout_constraintEnd_toStartOf="#+id/button_fragment2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<Button
android:id="#+id/button_fragment2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="show yellow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/button_fragment1"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<com.example.pavneet_singh.temp.MaxChildHeightFrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:minHeight="2dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#+id/button_fragment2"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
getSuggestedMinimumHeight() will be used to calculate the height of the view during the view rendering lifecycle.
Output:
With more views, fragment and different height. (400dp, 20dp, 500dp respectively)
A straightforward solution is to adjust the minimum height of the ConstraintLayout within the NestedScrollView before switching fragments. To prevent jumping, the height of the ConstraintLayout must be greater than or equal to
the amount by which the NestedScrollView has scrolled in the "y" direction
plus
the height of the NestedScrollView.
The following code encapsulates this concept:
private fun adjustMinHeight(nsv: NestedScrollView, layout: ConstraintLayout) {
layout.minHeight = nsv.scrollY + nsv.height
}
Please note that layout.minimumHeight will not work for ConstraintLayout. You must use layout.minHeight.
To invoke this function, do the following:
private fun insertYellowFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, YellowFragment())
transaction.commit()
val nsv = findViewById<NestedScrollView>(R.id.myScrollView)
val layout = findViewById<ConstraintLayout>(R.id.constraintLayout)
adjustMinHeight(nsv, layout)
}
It is similar for insertBlueFragment(). You can, of course, simplify this by doing findViewById() once.
Here is a quick video of the results.
In the video, I have added a text view at the bottom to represent additional items that may exist in your layout below the fragment. If you delete that text view, the code will still work, but your will see blank space at the bottom. Here is what that looks like:
And if the views below the fragment don't fill the scroll view, you will see the additional views plus white space at the bottom.
Your FrameLayout inside activity_main.xml has a height attribute of wrap_content.
Your child fragment layouts are determining the height differences you're seeing.
Should post up your xml for the child fragments
Try setting a specific height to the FrameLayout in your activity_main.xml
I solved this by creating a layout listener that keeps track of the "previous" height and adds padding to the ScrollView if the new height is less than before.
HeightLayoutListener.kt
class HeightLayoutListener(
private val activity: MainActivity,
private val root: View,
private val previousHeight: Int,
private val targetScroll: Int
) : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
root.viewTreeObserver.removeOnGlobalLayoutListener(this)
val padding = max((previousHeight - root.height), 0)
activity.setPaddingBottom(padding)
activity.setScrollPosition(targetScroll)
}
companion object {
fun create(fragment: Fragment): HeightLayoutListener {
val activity = fragment.activity as MainActivity
val root = fragment.view!!
val previousHeight = fragment.requireArguments().getInt("height")
val targetScroll = fragment.requireArguments().getInt("scroll")
return HeightLayoutListener(activity, root, previousHeight, targetScroll)
}
}
}
To enable this listener, add this method to both of your fragments:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listener = HeightLayoutListener.create(this)
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
}
These are the methods that the listener calls in order to actually update the ScrollView. Add them to your activity:
fun setPaddingBottom(padding: Int) {
val wrapper = findViewById<View>(R.id.wrapper) // add this ID to your ConstraintLayout
wrapper.setPadding(0, 0, 0, padding)
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(wrapper.width, View.MeasureSpec.EXACTLY)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
wrapper.measure(widthMeasureSpec, heightMeasureSpec)
wrapper.layout(0, 0, wrapper.measuredWidth, wrapper.measuredHeight)
}
fun setScrollPosition(scrollY: Int) {
val scroll = findViewById<NestedScrollView>(R.id.myScrollView)
scroll.scrollY = scrollY
}
And you need to set arguments to your fragments in order for the listener to know what the previous height and the previous scroll position were. So make sure to add them to your fragment transactions:
private fun insertYellowFragment() {
val fragment = YellowFragment().apply {
this.arguments = createArgs()
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.commit()
}
private fun insertBlueFragment() {
val fragment = BlueFragment().apply {
this.arguments = createArgs()
}
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.commit()
}
private fun createArgs(): Bundle {
val scroll = findViewById<NestedScrollView>(R.id.myScrollView)
val container = findViewById<View>(R.id.fragment_container)
return Bundle().apply {
putInt("scroll", scroll.scrollY)
putInt("height", container.height)
}
}
And that should do it!
What is the best strategy to achieve this feature:
I Have a horizontal RecyclerView with cards.
Each card will fulfil the entire screen, but I want it to show part of the next card and previous one if it has more than one item.
I know I can achieve this by setting my card android:layout_width at the adapter to have a specific DP like 250dp instead of match_parent.
But it doesn't look like a proper solution.
This is my code:
Activity with RecyclerView:
class ListPokemon : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val items = createListPokemons()
recyclerView.adapter = PokemonAdapter(items)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.setHasFixedSize(true)
val pagerSnapHelper = PagerSnapHelper()
pagerSnapHelper.attachToRecyclerView(recyclerView)
}
private fun createListPokemons(): List<Pokemon> {
val pokemons = ArrayList<Pokemon>()
pokemons += createPokemon("Pikachu")
pokemons += createPokemon("Bulbasaur")
pokemons += createPokemon("Charmander")
pokemons += createPokemon("Squirtle")
return pokemons
}
private fun createPokemon(name: String) = Pokemon(name = name, height = 1, weight = 69, id = 1)
}
Layout of Activity:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
</android.support.constraint.ConstraintLayout>
Adapter:
class PokemonAdapter(val list: List<Pokemon>) : RecyclerView.Adapter<PokemonAdapter.PokemonVH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonAdapter.PokemonVH {
return PokemonVH(LayoutInflater.from(parent.context)
.inflate(R.layout.pokemon_item, parent, false))
}
override fun onBindViewHolder(holder: PokemonAdapter.PokemonVH, position: Int) {
holder.textViewName.text = list[position].name
}
override fun getItemCount(): Int {
return list.size
}
class PokemonVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
var textViewName: TextView = itemView.findViewById(R.id.textViewName)
}
}
Layout of Adapter:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:layout_gravity="center_horizontal"
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="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:padding="36dp"
android:id="#+id/textViewName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="22sp"
tools:text="Teste String"/>
</LinearLayout>
</android.support.v7.widget.CardView>
This is my result:
I would like to show part of the next card at this situation. How can I do this?
Thanks.
What you need to do is set padding to your RecyclerView, set clipToPadding to false, use a SnapHelper with it, and you need to make sure the margins on your cards are less than or equal to the padding in the RecylerView.
So, let's say you want the distance from the cards to the sides of the screen to be 16dp and you want the distance between the cards to be 8dp. You'll have to set the margins on each card to 4dp, so the total margin is 8dp. And you have to set the padding to 12dp, given there's already a margin of 4dp on each side of the card.
It'll look a bit like this:
Your list:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
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"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
Your cards:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
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"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
app:cardElevation="2dp"/>
I think the padding solution is not a good for all cases, because forces the last item to have padding to the right.
Personally i use runtime width calculation of each item and i am very satisfied with this. So you can do the following:
onBindViewHolder
if (position == data.size - 1) {
holder.itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
} else {
if (width == null) {
holder.itemView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
holder.itemView.viewTreeObserver.removeOnGlobalLayoutListener(this)
width = holder.itemView.width
params.width = width!! - partOfPage
holder.itemView.requestLayout()
}
})
} else {
params.width = width!! - partOfPage
holder.itemView.requestLayout()
}
}
The outcome is that all middle items are rendered showing a part of the next page, but the last one is rendered full width.
Change your CardView width from "match_parent" to "0dp". And add, layout_weight as "80" (or similar). Make your parent view (RecyclerView) layout_weightSum as "100".
android:layout_width="0dp"
android:layout_weight="80"