Trying programmaticly to space a grid layout evenly - android

I have a GridLayout which should show 25 Buttons spaced evenly. To be able to set an onClickListener without calling each one them I want to do that programmatically.
I made a layout resource file with the grid itself to bind it and being able to inflate it
activity.xml
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:grid="http://schemas.android.com/apk/res-auto"
android:id="#+id/bingo_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:columnCount="5"
android:rowCount="5"
tools:context=".BingoActivity" />
Now I'm creating the fields:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bingoField = (1).rangeTo(25).toSet().toIntArray()
binding = BingoActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.bingoGrid.alignmentMode = GridLayout.ALIGN_BOUNDS
val bingoFieldGrid = binding.bingoGrid
bingoFieldGrid.alignmentMode = GridLayout.ALIGN_BOUNDS
bingoField.forEach {
val button = createButton(it.toString())
val gridLayoutParams = GridLayout.LayoutParams().apply {
rowSpec = spec(GridLayout.UNDEFINED, GridLayout.CENTER, 1f)
columnSpec = spec(GridLayout.UNDEFINED, GridLayout.CENTER, 1f)
height = GridLayout.LayoutParams.WRAP_CONTENT
width = GridLayout.LayoutParams.WRAP_CONTENT
}
bingoFieldGrid.addView(button, gridLayoutParams)
}
#RequiresApi(Build.VERSION_CODES.M)
private fun createButton(buttonText: String): Button {
var isCompleted = false
return Button(baseContext).apply {
setBackgroundColor(getColor(R.color.red))
gravity = Gravity.CENTER
text = buttonText
setOnClickListener {
isCompleted = if (!isCompleted) {
setBackgroundColor(getColor(R.color.green))
true
} else {
setBackgroundColor(getColor(R.color.red))
false
}
}
}
}
So, the fields are auto generated without problems, but the spacing is not right:
I'm quite new to the old layouting, is there a way to easily achieve that?

You're creating two different types of LayoutParams which doesn't make sense. LinearLayout shouldn't be involved at all.
The way they work is each child should get a set of LayoutParams that match the type of LayoutParams that its parent ViewGroup uses. So in this case the parent is GridLayout, so each child should be added using an instance of GridLayout.LayoutParams.
The way GridLayout.LayoutParams work is you define a row Spec and a column Spec that describe how a child should take up cells. We want them to take the single next cell, so we can leave the first parameter as UNDEFINED. We need to give them an equal weight more than 0 so they all share evenly in the leftover space. I'm using 1f for the weight.
I'm using FILL with a size of 0 for the buttons so they fill their cells. The margins put some gap between them.
I'm setting height and width to 0 to prevent them from being oversized. If the rows or columns become too big to fit the screen, the layout goes way too big.
You might want to use MaterialButton instead of a plain Button, so you can easily tint the background color without simply making it a static solid color rectangle.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = BingoBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.bingoGrid.alignmentMode = GridLayout.ALIGN_BOUNDS
for (num in 1..25) {
val button = MaterialButton(this).apply {
setBackgroundColor(resources.getColor(R.color.blue_500))
gravity = Gravity.CENTER
text = num.toString()
setPadding(0)
}
val params = GridLayout.LayoutParams().apply {
rowSpec = spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f)
columnSpec = spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f)
width = 0
height = 0
setMargins((4 * resources.displayMetrics.density).toInt())
}
binding.bingoGrid.addView(button, params)
}
}
AndroidStudio was finnicky about importing the spec function. I had to manually add this at the top:
import android.widget.GridLayout.Spec.*

You could consider Google ConstraintLayout Flows:
To set the number of elements use app:flow_maxElementsWrap="5"
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"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.helper.widget.Flow
android:id="#+id/flow"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:flow_horizontalGap="8dp"
app:flow_maxElementsWrap="5"
app:flow_verticalGap="8dp"
app:flow_verticalStyle="packed"
app:flow_wrapMode="chain" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then add the buttons programmatically to the ConstraintLayout:
val root = findViewById<ViewGroup>(R.id.root)
val size = 25
val array = IntArray(size)
for (i in 0 until size) {
array[i] = i + 1
val button = Button(this).apply {
layoutParams = ViewGroup.LayoutParams(0, 0)
id = i + 1
text = (i + 1).toString()
}
root.addView(button)
}
val flow = findViewById<Flow>(R.id.flow)
flow.referencedIds = array
Hint: you could use WRAP_CONTENT for the button height to avoid stretching out the buttons height.

Related

How to center the middle child in a Compose Row and make it responsive

I'd like to build a row in Jetpack Compose, with 3 elements, where the first and last elements are "stuck" to either sides, and the middle one stays in the center. The elements are not all the same width. It's possible for the first element to be really long, in which case I would like the middle item to move to the right, as much as possible. The images below hopefully illustrate what I mean:
All elements fit nicely
The first element is long and pushes the middle item to the right
The first element is super long, pushes the middle item all the way to the right and uses an ellipsis if necessary.
Wrapping each element in a Box and setting each weight(1f) helps with the first layout, but it doesn't let the first element to grow if it's long. Maybe I need a custom implementation of a Row Arrangement?
Ok, I managed to get the desired behaviour with a combination of custom implementation of an Arrangement and Modifier.weight.
I recommend you investigate the implementation of Arrangement.SpaceBetween or Arrangement.SpaceEvenly to get the idea.
For simplicity, I'm also assuming we'll always have 3 elements to place within the Row.
First, we create our own implementation of the HorizontalOrVertical interface:
val SpaceBetween3Responsively = object : Arrangement.HorizontalOrVertical {
override val spacing = 0.dp
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray,
) = if (layoutDirection == LayoutDirection.Ltr) {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
} else {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = true)
}
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceBetween3Responsively"
}
The placeResponsivelyBetween method needs to calculate the correct gap sizes between the elements, given their measured widths, and then place the elements with the gaps in-between.
fun placeResponsiveBetween(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val gapSizes = calculateGapSize(totalSize, size)
var current = 0f
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
// here the element and gap placement happens
current += it.toFloat() + gapSizes[index]
}
}
calculateGapSize has to try and "place" the second/middle item in the centre of the row, if the first element is short enough. Otherwise, set the first gap to 0, and check if there's space for another gap.
private fun calculateGapSize(totalSize: Int, itemSizes: IntArray): List<Int> {
return if (itemSizes.sum() == totalSize) { // the items take up the whole space and there's no space for any gaps
listOf(0, 0, 0)
} else {
val startOf2ndIfInMiddle = totalSize / 2 - itemSizes[1] / 2
val firstGap = Integer.max(startOf2ndIfInMiddle - itemSizes.first(), 0)
val secondGap = totalSize - itemSizes.sum() - firstGap
listOf(firstGap, secondGap, 0)
}
}
Then we can use SpaceBetween3Responsively in our Row! Some code edited out for simplicity
Row(
horizontalArrangement = SpaceBetween3Responsively,
) {
Box(modifier = Modifier.weight(1f, fill = false)) {
Text(text = "Supercalifragilisticexplialidocious",
maxLines = 1,
overflow = TextOverflow.Ellipsis)
}
Box {
// Button
}
Box {
// Icon
}
}
Modifier.weight(1f, fill = false) is important here for the first element - because it's the only one with assigned weight, it forces the other elements to be measured first. This makes sure that if the first element is long, it's truncated/cut to allow enough space for the other two elements (button and icon). This means the correct sizes are passed into placeResponsivelyBetween to be placed with or without gaps. fill = false means that if the element is short, it doesn't have to take up the whole space it's assigned - meaning there's space for the other elements to move closer, letting the Button in the middle.
Et voila!

How to use a ViewGroup as a shared element for a transition animation?

I'm trying to set up a "shared element" transition animation among two fragments. However, the destination I want is not a single view, but a FrameLayout with two overlapped elements that share size (an arrow and a rotating map) and must move and shrink at the same time.
My target layout looks like this:
<FrameLayout
android:id="#+id/container_arrow"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/map_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<ar.com.lichtmaier.antenas.ArrowView
android:id="#+id/arrow"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
I want to treat all this as a single thing.
Before transitions I was doing this animation on container_arrow using scale and translation properties, and it worked fine.
However, when I use a transition the size animation only affects the outer FrameLayout, but not its children. The inner arrow moves, but doesn't start small and grows, it start big and stays big. If I target the arrow instead, it works.
Looking at ChangeBounds transition code it seems it uses setFrame() to directly adjust the bounds of the target element. That doesn't propagate to its children.
I would need the translation+shrink animation to affect two elements, but transition names must be unique. Is there any way to achieve what I want?
EDIT:
I'm already trying to set the FrameLayout as a group by calling:
ViewCompat.setTransitionName(arrowContainer, "animatedArrow")
ViewGroupCompat.setTransitionGroup(arrowContainer, true) // <-- this
Same thing.. =/
This is precisely what the ViewGroupCompat.setTransitionGroup() API (for API 14+ devices when using AndroidX Transition) or android:transitionGroup="true" XML attribute (for API 21+ devices) is for - by setting that flag to true, that entire ViewGroup is used as a single item when it comes to shared element transitions.
Note that you must also set a transition name on the same element you set as a transition group (using ViewCompat.setTransitionName() / android:transitionName depending on whether you want to support back to API 14 or only API 21+).
I ended up creating my own Transition subclass which is similar to ChangeBounds but uses translation and scale view properties to move the target instead of adjusting bounds. A delta for translation is calculated and it's animated to 0, and an initial scale is also calculated and animated to 1.
Here's the code:
class MoveWithScaleAndTranslation : Transition() {
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun getTransitionProperties() = properties
private fun captureValues(transitionValues: TransitionValues) {
val view = transitionValues.view
val values = transitionValues.values
val screenLocation = IntArray(2)
view.getLocationOnScreen(screenLocation)
values[PROPNAME_POSX] = screenLocation[0]
values[PROPNAME_POSY] = screenLocation[1]
values[PROPNAME_WIDTH] = view.width
values[PROPNAME_HEIGHT] = view.height
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if(startValues == null || endValues == null)
return null
val leftDelta = ((startValues.values[PROPNAME_POSX] as Int) - (endValues.values[PROPNAME_POSX] as Int)).toFloat()
val topDelta = ((startValues.values[PROPNAME_POSY] as Int) - (endValues.values[PROPNAME_POSY] as Int)).toFloat()
val scaleWidth = (startValues.values[PROPNAME_WIDTH] as Int).toFloat() / (endValues.values[PROPNAME_WIDTH] as Int).toFloat()
val scaleHeight = (startValues.values[PROPNAME_HEIGHT] as Int).toFloat() / (endValues.values[PROPNAME_HEIGHT] as Int).toFloat()
val view = endValues.view
val anim = ObjectAnimator.ofPropertyValuesHolder(view,
PropertyValuesHolder.ofFloat("scaleX", scaleWidth, 1f),
PropertyValuesHolder.ofFloat("scaleY", scaleHeight, 1f),
PropertyValuesHolder.ofFloat("translationX", leftDelta, 0f),
PropertyValuesHolder.ofFloat("translationY", topDelta, 0f)
)
anim.doOnStart {
view.pivotX = 0f
view.pivotY = 0f
}
return anim
}
companion object {
private const val PROPNAME_POSX = "movewithscaleandtranslation:posX"
private const val PROPNAME_POSY = "movewithscaleandtranslation:posY"
private const val PROPNAME_WIDTH = "movewithscaleandtranslation:width"
private const val PROPNAME_HEIGHT = "movewithscaleandtranslation:height"
val properties = arrayOf(PROPNAME_POSX, PROPNAME_POSY, PROPNAME_WIDTH, PROPNAME_HEIGHT)
}
}

constraintlayout.widget.Group animation not working with TransitionManager

does anyone has any idea why animating constraintlayout.widget.Group visibility with TransitionManager is not working? Isn't this widget made for these kind of things?
It is working if hiding or showing items after separating views from Group
<androidx.constraintlayout.widget.Group
android:id="#+id/cardHeadersGroup"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:constraint_referenced_ids="cardSystemHeader,cardSimpleHeader,cardCombinedHeader"
app:layout_constraintBottom_toBottomOf="#+id/cardCombinedHeader"
app:layout_constraintEnd_toEndOf="#+id/cardSystemHeader"
app:layout_constraintStart_toStartOf="#+id/cardSimpleHeader"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"/>
val headersGroup = binding.cardHeadersGroup
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
I've been recently working with TransitionManager and ConstraintLayout.Group and found it to be very buggy.
Eventually I decided to dump the whole ConstraintLayout.Group and created an in-code AnimationGroup (similar to the in-xml ConstraintLayout.Group):
class AnimationGroup(vararg val views: View) {
var visibility: Int = View.INVISIBLE
set(value) {
views.forEach { it.visibility = value }
field = value
}
}
and an extension function for the Transition:
private fun Transition.addTarget(animationGroup: AnimationGroup) {
animationGroup.views.forEach { viewInGroup -> this.addTarget(viewInGroup) }
}
That way you can do the following (almost exactly the same code, but simpler xml - no ConstraintLayout.Group):
val headersGroup = AnimationGroup(
binding.cardSystemHeader,
binding.cardSimpleHeader,
binding.cardCombinedHeader
)
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
We can also extract the Group's referenced views with simple extension function:
fun Group.getReferencedViews() = referencedIds.map { rootView.findViewById<View>(it) }

RecyclerView scroll up is not smooth when the height isn't fixed

As stated in the question, I have a RecyclerView that displays images of different height.
The ImageView's height is initially 0dp and the width is match_parent, but in BindViewHolder I set the height to the correct value.
It works fine if you scroll down the list, but when you scroll back up:
As the previous ViewHolder is drawn it's height is initially set to something like 40dp as the image isn't loaded yet, but then suddenly it jumps to 400 as the new height is set to the ImgeView, which makes it very jittery and not smooth at all.
I tried prefetching and caching, but nothing worked.
Should I use ListView and load images on-demand knowing that the number of displayed items will reach 1000, or what can I do?
The is the ViewHolder layout
<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"
app:cardElevation="8dp"
app:cardCornerRadius="8dp"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="#+id/post_image"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
</android.support.constraint.ConstraintLayout>
and this is OnBindViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val target: SimpleTarget<File> = object: SimpleTarget<File>() {
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
thumbnail.setImage(ImageSource.uri(Uri.fromFile(resource)).tilingEnabled())
}
}
if (post?.url?.endsWith(".jpg", true) == true|| post?.url?.endsWith(".png", true) == true) {
thumbnail.visibility = View.VISIBLE
thumbnail.recycle()
thumbnail.setDoubleTapZoomDuration(300)
if(post.preview != null) {
thumbnail.post {
val scale: Float = post.preview.images[0].source.height.toFloat()/post.preview.images[0].source.width.toFloat()
val layoutParams: ViewGroup.LayoutParams = thumbnail.layoutParams
layoutParams.height = (thumbnail.measuredWidth.toFloat() * scale).toInt()
thumbnail.layoutParams = layoutParams
glide.downloadOnly().load(post.preview.images[0].source.url).into(target)
}
}
} else {
thumbnail.visibility = View.GONE
glide.clear(target)
thumbnail.recycle()
}
}
}
Where thumbnail is a SSIV
The problem is I got the measured width asynchronously every time, which caused a delay in setting the height.
I calculated measuredHeight on OnCreateViewHolder and store it for later use.

Set runtime margin to any view using Kotlin

I am a beginner in Kotlin .I am not too much familier with this language. I am making one example and playing with code. I Just want to set runtime margin to any view. I also trying to google it but not getting any proper solution for this task.
Requirement
Set runtime margin to any View.
Description
I have taking one xml file which is contain on Button and I want to set runtime margin to this button.
Code
I also try below thing but it's not work.
class MainActivity : AppCompatActivity() {
//private lateinit var btnClickMe: Button
//var btnClickMe=Button();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//For setting runtime text to any view.
btnClickMe.text = "Chirag"
//For getting runtime text to any view
var str: String = btnClickMe.text as String;
//For setting runtimer drawable
btnClickMe.background=ContextCompat.getDrawable(this,R.drawable.abc_ab_share_pack_mtrl_alpha)//this.getDrawable(R.drawable.abc_ab_share_pack_mtrl_alpha)
/*
//For Setting Runtime Margine to any view.
var param:GridLayout.LayoutParams
param.setMargins(10,10,10,10);
btnClickMe.left=10;
btnClickMe.right=10;
btnClickMe.top=10;
btnClickMe.bottom=10;
*/
// Set OnClick Listener.
btnClickMe.setOnClickListener {
Toast.makeText(this,str,5000).show();
}
}
}
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:orientation="vertical"
tools:context="chirag.iblazing.com.stackoverflowapp.MainActivity"
android:layout_height="match_parent">
<Button
android:id="#+id/btnClickMe"
android:text="Click Me"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
How can I proceed?
You need to get the layoutParams object from button and cast it to ViewGroup.MarginLayoutParams (which is a parent class of LinearLayout.LayoutParams, RelativeLayout.LayoutParams and others and you don't have to check which is btnClickMe's actual parent) and set margins to whatever you want.
Check following code:
val param = btnClickMe.layoutParams as ViewGroup.MarginLayoutParams
param.setMargins(10,10,10,10)
btnClickMe.layoutParams = param // Tested!! - You need this line for the params to be applied.
This is how I would like to do in Kotlin -
fun View.margin(left: Float? = null, top: Float? = null, right: Float? = null, bottom: Float? = null) {
layoutParams<ViewGroup.MarginLayoutParams> {
left?.run { leftMargin = dpToPx(this) }
top?.run { topMargin = dpToPx(this) }
right?.run { rightMargin = dpToPx(this) }
bottom?.run { bottomMargin = dpToPx(this) }
}
}
inline fun <reified T : ViewGroup.LayoutParams> View.layoutParams(block: T.() -> Unit) {
if (layoutParams is T) block(layoutParams as T)
}
fun View.dpToPx(dp: Float): Int = context.dpToPx(dp)
fun Context.dpToPx(dp: Float): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
now we just have to call this on a view like
textView.margin(left = 16F)
Here's a useful Kotlin extension method:
fun View.setMargins(
left: Int = this.marginLeft,
top: Int = this.marginTop,
right: Int = this.marginRight,
bottom: Int = this.marginBottom,
) {
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(left, top, right, bottom)
}
}
Use it like this:
myView.setMargins(
top = someOtherView.height
bottom = anotherView.height
)
EDIT: the solution is similar to the answer from Hitesh, but I'm using the (original) ViewGroup.setMargins in pixels. Of course you can make your own setMarginsDp variant based on these examples, or use Hitesh's dpToPx extension before calling my implementation. Whichever solution you choose depends on your own taste.
Also take note that my solution (re)sets all margins, although this won't be an issue in most cases.
If you want to change specific margin like top or bottom you can use below code with Data binding .
#BindingAdapter("android:layout_marginTop")
#JvmStatic
fun setLayoutMarginTop(view: View, marginTop: Float) {
val layoutParams = view.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = marginTop.toInt()
view.layoutParams = layoutParams
}
and in .xml file you can write like below code
<ImageView
android:id="#+id/imageView3"
android:layout_width="#dimen/_15dp"
android:layout_height="#dimen/_15dp"
android:layout_marginTop="#{homeViewModel.getLanguage() ? #dimen/_14dp : #dimen/_32dp }"
android:contentDescription="#string/health_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/imageView1"
app:layout_constraintEnd_toStartOf="#+id/textView3"
android:src="#{ homeViewModel.remoteStatusVisible ? #drawable/green_rectangle : #drawable/gray_rectangle}"/>
Here is another sample of CardView
myCardView.elevation = 0F
myCardView.radius = 0F
val param = (myCardView.layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(0,0,0,0)
}
myCardView.layoutParams = param

Categories

Resources