Resizing an image - flicker issue in collapsingtoolbarlayout - android

I want to have an image in collapsing toolbar layout, so when I scroll the list below, image becomes twice as small, but doesn't disappear completely.
So far I achieved this with following code:
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">
<androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
android:background="#fff"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent"
android:id="#+id/appBar"
android:elevation="1dp"
android:layout_height="300dp">
<com.google.android.material.appbar.CollapsingToolbarLayout android:layout_width="match_parent"
app:expandedTitleGravity="bottom"
android:minHeight="200dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_collapseMode="pin"
android:layout_gravity="bottom"
android:orientation="vertical">
<ImageView android:layout_width="200dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_height="200dp"
android:id="#+id/image"
android:scaleType="centerCrop"
android:src="#drawable/saya_no_uta"/>
<TextView android:id="#+id/doStuff" android:layout_width="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textColor="#fff"
android:layout_height="wrap_content" android:text="Do stuff"/>
<LinearLayout
android:id="#+id/content"
android:background="#00f"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="#id/image"
android:layout_width="match_parent"
android:layout_height="100dp">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent">
<TextView android:layout_width="match_parent"
android:text="#string/large_text"
android:layout_height="wrap_content"/>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Kotlin:
class CollapsingToolbarFragment : Fragment() {
companion object {
const val TAG = "CollapsingToolbarLayout"
}
var originalHeight: Float = 0.0f
var lastVerticalOffset: Int = Int.MAX_VALUE
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentCollapsingToolbarBinding.inflate(inflater, container, false)
originalHeight = convertDpToPixel(200.0f)
binding.lifecycleOwner = viewLifecycleOwner
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
if (lastVerticalOffset == verticalOffset) {
return#OnOffsetChangedListener
}
lastVerticalOffset = verticalOffset
val totalScrollRange = appBarLayout.totalScrollRange
Log.i("Hello", "total: $totalScrollRange, offset: $verticalOffset")
val size = originalHeight.toInt() + verticalOffset
binding.image.layoutParams.height = size
binding.image.layoutParams.width = size
binding.image.requestLayout()
})
return binding.root
}
fun convertDpToPixel(dp: Float): Float {
return dp * (requireContext().resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
}
}
It works like it's supposed to, but there are side effects. When I quickly scroll up or down, for a split second I can see my textview with text "Do stuff" go above or below its place by a small margin, which is really annoying. This might be due to the fact that I'm just manually calling requestLayout() But what other ways do I have to resize image in OnOffsetChangedListener?
Or maybe I could take some other approach?

In case someone was wondering, I was able to get better results by using scaleX, scaleY and translationY properties on ImageView, instead of requesting layout. I also moved my edit TextView outside of constraint layout.
This is how my onOffsetChanged looks like on a different code base:
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (scrollRange == -1) {
scrollRange = binding.appBarLayout.totalScrollRange
}
val delta = 1.0f - abs(verticalOffset).toFloat() / scrollRange.toFloat() * 0.5f
binding.avatar.scaleX = 0.2f + delta * 0.8f
binding.avatar.scaleY = 0.2f + delta * 0.8f
binding.avatar.translationY = delta * 0.5f * abs(verticalOffset)
}

You can write your custom collapsingtoolbarlayout to rewrite OffsetUpdateListener to change the image size.

Related

ConstraintLayout child views retain height of 0 when parent view height is reset

I have a nested ConstraintLayout that has its own child views inside a LinearLayout. This LinearLayout can then be collapsed or expanded by the user pressing a Button.
I do this by programatically setting the LinearLayout height to 0 if the UI needs to be collapsed or defining a MeasureSpec and passing this new measured height to the LinearLayout if the UI needs to expanded.
The issue I'm encountering is that this works the first time the user collapses and expands the UI, subsequent interactions leaves a blank space where the ConstraintLayout child views should be. Looking at Layout Inspector it is showing me that the ConstrainLayout child views have a height of 0dp while the the parent ConstraintLayout has the correct height.
After some debugging it seems that replacing the ConstraintLayout to a LinearLayout fixes this weird height issue but I would like to know what's going on under the hood when the Views are updating their heights.
I've attached my XML layout file and the code that handles the collapse/expand logic below
MainFragment.kt
private var isCollapsed = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
val collapsableView = view.findViewById<LinearLayout>(R.id.nested_content_holder)
val button = view.findViewById<Button>(R.id.collapse_button)
button.setOnClickListener {
val newHeight = if (isCollapsed) {
collapsableView.measure(
View.MeasureSpec.makeMeasureSpec(
collapsableView.width,
View.MeasureSpec.EXACTLY
),
ViewGroup.LayoutParams.WRAP_CONTENT
)
collapsableView.measuredHeight
} else {
0
}
val newLayoutParams = collapsableView.layoutParams
newLayoutParams.height = newHeight
collapsableView.layoutParams = newLayoutParams
isCollapsed = !isCollapsed
}
return view
}
main_fragment.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:id="#+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<LinearLayout
android:id="#+id/main_content_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="#+id/collapse_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="collapse" />
<TextView
android:id="#+id/title_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="I am a title" />
<LinearLayout
android:id="#+id/nested_content_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/teal_700"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="#+id/child_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:text="I am a child One"
android:textColor="#color/white" />
<TextView
android:id="#+id/child_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:text="I am a child Two"
android:textColor="#color/white" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/nested_list"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/item_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:text="I am a Nested list item one"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/item_two"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:text="I am a Nested list item two"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/item_one" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Just as I understand, the desired behavior is that when the button is clicked, the linear layout is either shown or not.
If I'm right, then I think you are complicating too much.
Have you considered changing your linear layout visibility from View.VISIBLE to View.GONE instead?
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
val collapsableView = view.findViewById<LinearLayout>(R.id.nested_content_holder)
val button = view.findViewById<Button>(R.id.collapse_button)
button.setOnClickListener {
if (isCollapsed) {
collapsableView.visibility = View.VISIBLE
} else {
collapsableView.visibility = View.GONE
}
isCollapsed = !isCollapsed
}
return view
}

Guidelines Inside ScrollView - use Viewport %

I would like to add content to an app that starts at about 70% down vertically and can be scrolled upwards to cover the top 70% views.
I thought of using two children ConstraintLayout's inside a parent ConstraintLayout - the two children would be on top of each other. One would contain the views that would populate the first 70% of the screen while the other would contain a NestedScrollView which has an invisible <View> that takes up 70% of the height and then the additional content that can be scrolled up.
I'm facing a problem with marking the 70% spot - using a Guideline inside the NestedScrollView isn't working because the %s are fluid (it matches to 70% of the content inside the NestedScrollView instead of 70% of the viewable screen). Using a Guideline outside the NestedScrollView doesn't work because well... constraints have to be siblings to compile.
How can I accomplish this?
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red5F"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
// A bunch of content that should fill up the first 70% of the screen and be covered by the overlay if user scrolls
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
android:id="#+id/scrollView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/overlayInnerLayout">
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/verticalGuidelineOverlay"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7"/>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="#+id/spacerView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/verticalGuidelineOverlay"
app:layout_constraintLeft_toLeftOf="parent"/>
// More content here that the user could scroll upwards that would start at the 70% point and eventually cover the entire screen.
</ConstraintLayout>
</NestedScrollView>
</ConstraintLayout>
</ConstraintLayout>
Video w/example here: https://imgur.com/a/BTolYUu
Try out this method,
<?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/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:weightSum="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="horizontal">
<RelativeLayout
android:id="#+id/transparentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="0.3" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/bg">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20sp"
android:text="#string/lorem_ipsum"
tools:ignore="MissingConstraints"
android:textSize="18sp"/>
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
set 70% height programatically using layoutParams
val transparentView = findViewById<RelativeLayout>(R.id.transparentView)
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
val height = Math.min(metrics.widthPixels, metrics.heightPixels) //height
val params = transparentView.layoutParams
params.height = (height * 70) / 70
transparentView.layoutParams = params
you will get the required result : enter link description here
Remove guidelines and use a view like this as a spacer view. It's height constrained to be 1.15 of it's width. You can change it around a littile to get what you want
<View
android:id="#+id/spacerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
Also just as an advice
you're not supposed to use match_parent in ConstraintLayout, use 0dp and constraint it to both sides.
Top layout can be replaced with FrameLayout, cause you don't really use any constraints
You can use a customized BottomSheetDialogFragment that has a theme of Theme_Translucent_NoTitleBar, and change the y value of the root layout of the dialog whenever the user drags it up or down.
class MyDialogFragment(height: Int) : BottomSheetDialogFragment(), View.OnTouchListener {
private val outsideWindowHeight = height
private val rootLayout by lazy {
requireView().findViewById<LinearLayout>(R.id.dialog_root)
}
private var oldY = 0
private var baseLayoutPosition = 0
private var defaultViewHeight = 0
private var isClosing = false
private var isScrollingUp = false
private var isScrollingDown = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return BottomSheetDialog(
requireContext(),
android.R.style.Theme_Translucent_NoTitleBar
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view: View = inflater.inflate(
R.layout.fragment_dialog, container,
false
)
view.setBackgroundResource(R.drawable.rounded_background)
(dialog as BottomSheetDialog).apply {
setCancelable(false)
behavior.peekHeight =
(outsideWindowHeight * 0.3).toInt() // Minimum height of the BottomSheet is 30% of the root layout (to leave the 70% to the main layout)
}
return view
}
#SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rootLayout.setOnTouchListener(this)
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
// Get finger position on screen
val y = event!!.rawY.toInt()
// Switch on motion event type
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// save default base layout height
defaultViewHeight = rootLayout.height
oldY = y
baseLayoutPosition = rootLayout.y.toInt()
}
MotionEvent.ACTION_UP -> {
// If user was doing a scroll up
if (isScrollingUp) {
// Reset baselayout position
rootLayout.y = 0f
// We are not in scrolling up anymore
isScrollingUp = false
}
// If user was doing a scroll down
if (isScrollingDown) {
// Reset baselayout position
rootLayout.y = 0f
// Reset base layout size
rootLayout.layoutParams.height = defaultViewHeight
rootLayout.requestLayout()
// We are not in scrolling down anymore
isScrollingDown = false
}
}
MotionEvent.ACTION_MOVE -> {
if (rootLayout.y <= -100) {
return true
}
if (!isClosing) {
val currentYPosition = rootLayout.y.toInt()
// If we scroll up
if (oldY > y) {
// First time android rise an event for "up" move
if (!isScrollingUp) {
isScrollingUp = true
}
rootLayout.y = rootLayout.y + (y - oldY)
} else {
// First time android rise an event for "down" move
if (!isScrollingDown) {
isScrollingDown = true
}
// change position because view anchor is top left corner
rootLayout.y = rootLayout.y + (y - oldY)
rootLayout.requestLayout()
}
// Update position
oldY = y
}
}
}
return true
}
}
fragment_dialog.xml (Nothing fancy):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/tv_bottom_sheet_heading"
android:layout_width="wrap_content"
android:layout_height="#dimen/dp_56"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:gravity="center"
android:text="#string/bottom_sheet_option_heading"
android:textColor="#android:color/black"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_camera"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_camera_alt_black_24dp"
android:drawableLeft="#drawable/ic_camera_alt_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_camera"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_gallery"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_insert_photo_black_24dp"
android:drawableLeft="#drawable/ic_insert_photo_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_gallery"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="#dimen/md_bottom_sheet_separator_top_margin"
android:layout_marginBottom="#dimen/dp_8"
android:background="#color/grayTextColor" />
<TextView
android:id="#+id/tv_btn_remove_photo"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_delete_black_24dp"
android:drawableLeft="#drawable/ic_delete_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_remove_photo"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Material button"
android:textAppearance="#style/TextAppearance.AppCompat.Medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/longText1"
android:textColor="#color/white"
android:textSize="22sp" />
</LinearLayout>
And send the height of the root ViewGroup of the main layout to the dialog in the main activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val root = findViewById<ConstraintLayout>(R.id.root)
root.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
root.viewTreeObserver
.removeOnGlobalLayoutListener(this)
val dialogFragment = MyDialogFragment(root.height)
dialogFragment.show(supportFragmentManager, "dialog_tag")
}
})
}
}
Preview:

Android CollapsingToolbar/ AppBarLayout scroll behind status bar behaviour top padding

I'd like to use CoordinatoryLayout, AppBarLayout and CollapsingToolbarLayout to create a layout that resembles the below example from Google Calendar.
The key things I'm trying to replicate:
Scrolling the content behind the status bar
Rounded corners at the top of the scroll container
Enough room at the top of the screen for the header to not look squashed
The Question
Google Calendar appears to be growing the scroll container as the user scrolls. How would I go about doing this or something similar to achieve the look I'm after?
I've put together a quick example of what I'm trying to build:
activity_scrolling.xml
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:animateLayoutChanges="true"
tools:context="uk.co.exampleapplication.ScrollingActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="#+id/lay_header"
layout="#layout/layout_header" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/scroll_header_background"
android:elevation="16dp"
android:paddingBottom="12dp">
<TextView
android:id="#+id/sectionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/filter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:layout_marginEnd="16dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".ScrollingActivity"
tools:showIn="#layout/activity_scrolling">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:text="#string/large_text" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
layout_header.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:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="60dp"
android:paddingBottom="40dp"
app:layout_collapseMode="parallax"
tools:ignore="HardcodedText">
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="Title"
android:textColor="#FFF"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Subtitle"
android:textColor="#FFF"
android:textSize="16sp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
scroll_header_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
<solid android:color="#FFFFFF" />
<size
android:width="64dp"
android:height="64dp" />
</shape>
My attempt is included below. The header scrolls behind the toolbar as desired but I'd like some additional top padding above my views (about the height of the top inset/ status bar would be sufficient). Google Calendar appears to be solving this by having the container grow as the user scrolls.
Implement an AppBarLayout.OnOffsetChangedListener that will adjust top padding on the ConstraintLayout that holds the TextView and the Button. (Call this view "viewToGrow".) You can also do other things in the listener like change the corner radius of the drawable as the appbar scrolls.
The following example adjusts top padding to give the header more room. This padding is increased as the appbar scrolls up and decreases as it scrolls down. The demo app also removes the corner radius of the drawable during the final 15% of the appbar scroll.
ScrollingActivity
class ScrollingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
val viewToGrow: View = findViewById(R.id.viewToGrow)
val baseTopPadding = viewToGrow.paddingTop
// Determine how much top padding has to grow while the app bar scrolls.
var maxDeltaPadding = 0
val contentView = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(contentView) { _, insets ->
maxDeltaPadding = insets.systemWindowInsetTop
insets
}
// Get key metrics for corner radius shrikage.
var backgroundRadii: FloatArray? = null
var maxRadius: FloatArray? = null
val backgroundDrawable = (viewToGrow.background as GradientDrawable?)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && backgroundDrawable != null) {
backgroundRadii = backgroundDrawable.cornerRadii
maxRadius = floatArrayOf(backgroundRadii!![0], backgroundRadii[1])
}
// Set up the app bar and the offset change listener.
val appBar: AppBarLayout = findViewById(R.id.app_bar_layout)
val appBarTotalScrollRange: Float by lazy {
appBar.totalScrollRange.toFloat()
}
appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
// Add/remove padding gradually as the appbar scrolls.
val percentOfScrollRange = (-verticalOffset / appBarTotalScrollRange)
val deltaPadding = maxDeltaPadding * percentOfScrollRange
val newTopPadding = baseTopPadding + deltaPadding.toInt()
if (newTopPadding != viewToGrow.paddingTop) {
viewToGrow.setPadding(
viewToGrow.paddingLeft,
newTopPadding,
viewToGrow.paddingRight,
viewToGrow.paddingBottom
)
// Change the drawable radius as the appbar scrolls.
if (backgroundRadii != null && maxRadius != null) {
val radiusShrinkage = if (percentOfScrollRange > (1.0f - CORNER_SHRINK_RANGE)) {
(1.0f - percentOfScrollRange) / CORNER_SHRINK_RANGE
} else {
1.0f
}
backgroundRadii[0] = maxRadius[0] * radiusShrinkage
backgroundRadii[1] = maxRadius[1] * radiusShrinkage
backgroundRadii[2] = maxRadius[0] * radiusShrinkage
backgroundRadii[3] = maxRadius[1] * radiusShrinkage
backgroundDrawable!!.cornerRadii = backgroundRadii
}
}
})
}
companion object {
const val CORNER_SHRINK_RANGE = 0.15f
}
}

Custom spacing between recyclerview items

I need to display items similar to a hamburger.
Each element has a different height, but the distance between them should be always the same. I tried to use LinearLayoutManager, but spaces between items were too big. I created custom LinearLayoutManager but it doesn't work as I expected. How I should make spacing like 16dp or 50 px between ImageView.
I also need to make no spacing for the same type of items, but this problem is less important.
class CustomLinearLayoutManager(context: Context) : LinearLayoutManager(context) {
private val horizontalSpace: Int
get() = width - paddingEnd - paddingStart
private val verticalSpace: Int
get() = height - paddingBottom - paddingTop
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateDefaultLayoutParams())
}
override fun generateLayoutParams(c: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateLayoutParams(c, attrs))
}
override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateLayoutParams(lp))
}
private fun spanLayoutSize(layoutParams: RecyclerView.LayoutParams): RecyclerView.LayoutParams {
if (orientation == HORIZONTAL) {
layoutParams.width = (horizontalSpace / itemCount)
} else if (orientation == VERTICAL) {
layoutParams.height = (verticalSpace / itemCount)
}
return layoutParams
}
override fun canScrollVertically(): Boolean {
return false
}
override fun canScrollHorizontally(): Boolean {
return false
}
}
RecyclerView_item.xml
<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/RecyclerFood_item"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/ingredientPic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:contentDescription="#string/display_image_of_ingredient"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment_layout.xml
<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/DishFragmentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="my.custom.food-app.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/DishList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

How to center horizontally items in recyclerview that uses gridlayoutmanager

I'm preparing a little game in my app and would like to create a recylerview based on gridlayoutmanager and my problem is that I can't center horizontally all elements inside this view.
I have already tried to fix it by some changes in layout file but it didn't help.
Adapter :
class AdapterCheckYrslf(private val word: String) :
RecyclerView.Adapter<AdapterCheckYrslf.CardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_checkrslf_letter, parent, false)
return CardViewHolder(view)
}
override fun getItemCount(): Int {
return word.length
}
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
holder.currentLetter.text = word[position].toUpperCase().toString()
}
class CardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val currentLetter = itemView.findViewById<TextView>(R.id.item_checkyrslf_letter)
}
}
GridSpacingItemDecoration, it makes a space beetwen every item :
class GridSpacingItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State) {
outRect.left = space
outRect.right = space
outRect.bottom = space
outRect.top = space
}
}
ColumnQty :
class ColumnQty(context: Context, viewId: Int) {
private val width: Int
private val height: Int
private var remaining: Int = 0
private val displayMetrics: DisplayMetrics
init {
val view = View.inflate(context, viewId, null)
view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
width = view.measuredWidth
height = view.measuredHeight
displayMetrics = context.resources.displayMetrics
}
fun calculateNoOfColumns(): Int {
var numberOfColumns = displayMetrics.widthPixels / width
remaining = displayMetrics.widthPixels - numberOfColumns * width
if (remaining / (2 * numberOfColumns) < 15) {
numberOfColumns--
remaining = displayMetrics.widthPixels - numberOfColumns * width
}
return numberOfColumns
}
fun calculateSpacing(): Int {
val numberOfColumns = calculateNoOfColumns()
return remaining / (2 * numberOfColumns)
}
}
Activity :
val gridManagerFix = ColumnQty(this, R.layout.item_checkrslf_letter)
check_yrslf_word.layoutManager = GridLayoutManager(this, gridManagerFix.calculateNoOfColumns())
check_yrslf_word.addItemDecoration(GridSpacingItemDecoration(gridManagerFix.calculateSpacing()))
check_yrslf_word.setHasFixedSize(true)
Fragment of layout :
<LinearLayout
android:id="#+id/helper_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal|center_vertical"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="#+id/check_yrslf_end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/check_yrslf_hand">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/check_yrslf_word"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:clipChildren="true"
android:clipToPadding="true"
android:numColumns="auto_fit"
android:overScrollMode="never"
android:scrollbars="none"
android:scrollingCache="true"
android:stretchMode="columnWidth"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
Item layout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
android:id="#+id/item_checkyrslf_cardLetter"
style="#style/Widget.MaterialComponents.CardView"
android:layout_width="27dp"
android:layout_height="27dp"
android:layout_marginBottom="5dp"
android:checkable="false"
app:cardBackgroundColor="#color/grey_3"
app:cardElevation="0dp"
app:checkedIconTint="#color/white"
app:strokeColor="#color/colorAccent"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:id="#+id/item_checkyrslf_letter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="#font/poppins_medium"
android:gravity="center"
android:includeFontPadding="false"
android:text="A"
android:textAppearance="#style/TextAppearance.MaterialComponents.Headline1"
android:textColor="#color/textColor"
android:textSize="16sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
Pictures :
How does this layout look in Preview
That's what I have
I would like to have
Try making the RecyclerView android:layout_width="match_parent" and android:gravity="center"
Edit 1:
Try removing all those added params to your RecyclerView: (Something like this)
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/check_yrslf_word"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Now try it, just for debugging purposes.
I have solved it by changing GridLayoutManager to LinearLayoutManager.

Categories

Resources