I have a custom view that extends LinearLayout and implements onMeasure. I'd like the children to be either as wide as they need to be or filling the available space.
XML files:
Parent:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.AtMostLinearLayout
android:id="#+id/at_most_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
Button example:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:src="#android:drawable/ic_delete" />
</FrameLayout>
Views are added programmatically, for example:
findViewById<AtMostLinearLayout>(R.id.at_most_linear_layout).apply {
repeat(4) {
LayoutInflater.from(context).inflate(R.layout.button, this)
}
}
Finally the Custom view class:
class AtMostLinearLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
init {
orientation = HORIZONTAL
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (childCount < 1) return
val newWidth = min(measuredWidth, maxTotalWidth)
var availableWidth = newWidth
var numberOfLargeChildren = 0
repeat(childCount) {
getChildAt(it).let { child ->
if (child.measuredWidth > availableWidth / childCount) {
availableWidth -= child.measuredWidth
numberOfLargeChildren++
}
}
}
val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
repeat(childCount) {
getChildAt(it).apply {
measure(
MeasureSpec.makeMeasureSpec(max(measuredWidth, minChildWidth), EXACTLY),
UNSPECIFIED
)
}
}
setMeasuredDimension(
makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
}
}
It works fine in LTR:
In RTL however the views are off set for some reason and are drawn outside the ViewGroup:
Where could this offset coming from? It looks like the children's measure calls are being added to the part, or at least half of it...
You could use the Layout Inspector (or "show layout boundaries" on device), in order to determine why it behaves as it does. The calculation of the horizontal offset may have to be flipped; by substracting instead of adding ...in order to account for the change in layout direction, where the absolute offset in pixels may always be understood as LTR.
If the canvas is rtl in the onDraw method, have you tried inverting it?
You could try using View.getLayoutDirection(). Return the layout direction.
onDraw method override and
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
if(isRtrl){
canvas.scale(-1f, 1f, width / 2, height / 2)
}
After reading through the LinearLayout & View measure and layout code some more I figured out why this it's happening.
Whenever LinearLayout#measure is called mTotalLength is calculated, which represents the calculated width of the entire view. As I'm manually remeasuring the children with a different MeasureSpec LinearLayout cannot cache these values. Later in the layout pass the views use the cached mTotalLength to set the child's left i.e. the offset. The left is based on the gravity of the child and thus being affected by the cached value.
See: LinearLayout#onlayout
final int layoutDirection = getLayoutDirection();
switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
case Gravity.RIGHT:
// mTotalLength contains the padding already
childLeft = mPaddingLeft + right - left - mTotalLength;
break;
case Gravity.CENTER_HORIZONTAL:
// mTotalLength contains the padding already
childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
break;
case Gravity.LEFT:
default:
childLeft = mPaddingLeft;
break;
}
I've change the impl to ensure it always sets the gravity to Gravity.LEFT. I should probably manually implement onLayout instead!
class AtMostLinearLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
init {
orientation = HORIZONTAL
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (childCount < 1) return
val newWidth = min(measuredWidth, maxTotalWidth)
var availableWidth = newWidth
var numberOfLargeChildren = 0
repeat(childCount) {
getChildAt(it).let { child ->
if (child.measuredWidth > availableWidth / childCount) {
availableWidth -= child.measuredWidth
numberOfLargeChildren++
}
}
}
val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
repeat(childCount) {
getChildAt(it).let { child ->
child.measure(
makeMeasureSpec(max(child.measuredWidth, minChildWidth), EXACTLY),
UNSPECIFIED
)
}
}
// Effectively always set it to Gravity.LEFT to prevent LinearLayout using its
// internally-cached mTotalLength to set the Child's left.
gravity = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) Gravity.END else Gravity.START
setMeasuredDimension(
makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
}
}
I don't understand why you need a custom ViewGroup for this work. How about set layout_weight when you add child view to LinearLayout.
Just simple by:
val layout = findViewById<LinearLayout>(R.id.linear_layout)
repeat(4) {
val view = LayoutInflater.from(this).inflate(R.layout.button, layout, false)
view.apply {
updateLayoutParams<LinearLayout.LayoutParams> {
width = 0
weight = 1f
}
}
layout.addView(view)
}
Related
I want to animate TextView width when I change visibility of TextView. I don't wanna achieve generic "fade in/out" effect, but I wanna collapse TextView from sides to 0 width.
Here are my functions:
fun fadeInTextViewSize(){
val parentWidth = (buttonText.parent as View).measuredWidth
val widthAnimator = ValueAnimator.ofInt(buttonText.width, parentWidth)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
fun fadeOutTextViewSize(){
val widthAnimator = ValueAnimator.ofInt(buttonText.width, 0)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
Issue is that with this function, my TextView height is for some reason set to MATCH_PARENT.
I assume that you need the TextView to appear as if erased from both ends equally. Here is a technique that will do that:
private lateinit var buttonText: TextView
private var viewWidth = 0
fun fadeOutTextViewSize() {
with(buttonText) {
// Enable scrolling for the view since we will need to scroll horizontally to center text.
setMovementMethod(ScrollingMovementMethod())
setHorizontallyScrolling(true)
// Lock in the starting width and height.
viewWidth = buttonText.width
layoutParams.width = buttonText.width
layoutParams.height = buttonText.height
visibility = View.VISIBLE
}
with(ValueAnimator.ofInt(viewWidth, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.layoutParams.width = newWidth
// Shift text left so it stays centered in the initial bounds.
val scrollX = (viewWidth - newWidth) / 2
buttonText.scrollTo(scrollX, 0)
buttonText.requestLayout()
}
doOnEnd {
// Make the view invisible to seal its disappeared status. This could also be
// "GONE" depending on the desired effect.
buttonText.visibility = View.INVISIBLE
}
start()
}
}
The technique is to lock in the size and height of the TextView to its initial size. From that initial size, the animation shrinks the view's width from the initial size to zero. Since the layout will position the start of the text to the start of the view, the text is scrolled left to maintain its position on the screen as the width shrinks.
The red line is there just to mark the center of the TextView and is not needed.
Causing the view to reappear is mostly the opposite of the code here.
The test layout:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_blue_dark"
tools:context=".MainActivity">
<TextView
android:id="#+id/buttonText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light"
android:text="Hello World!"
android:textSize="48sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/buttonText"
app:layout_constraintEnd_toEndOf="#id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toTopOf="#id/buttonText" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Click Here"
app:layout_constraintEnd_toEndOf="#+id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toBottomOf="#id/buttonText" />
</androidx.constraintlayout.widget.ConstraintLayout>
There is another way to accomplish this which is to use a custom TextView that will clip its canvas to make the view shrink and expand. This method has the following advantages:
It is probably more efficient in that it does not require additional layout of the TextView.
If the TextView is within a ConstraintLayout and there are other views that are constrained to the start and/or end of the TextView, those views will not shift since the boundaries of the TextView remain unchanged.
private lateinit var buttonText: ClippedTextView
fun fadeOutTextViewSize() {
with(ValueAnimator.ofInt(buttonText.width, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.setClippedWidth(newWidth)
buttonText.invalidate()
}
start()
}
}
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mClipWidth = right - left
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}
We can also bring all the logic into the custom TextView as follows:
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
private val mAnimator: ValueAnimator by lazy {
ValueAnimator().apply {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
setClippedWidth(animation.animatedValue as Int)
invalidate()
}
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
setClippedWidth(right - left)
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun expandView() {
doWidthAnimation(mClipWidth, 0)
}
fun shrinkView() {
doWidthAnimation(mClipWidth, width)
}
private fun doWidthAnimation(startWidth: Int, endWidth: Int) {
animation?.cancel()
with(mAnimator) {
setIntValues(startWidth, endWidth)
start()
}
}
private fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}
I am looking for a way to programmatically slowly scroll a RecyclerView so as to bring a certain element targetPosition exactly in the middle of the screen. Note that all my items in the RecylerView have the same height by design. The RecyclerView is vertical. I am programming in Kotlin and AndroidX
I have tried:
smoothScrollToPosition(targetPosition)
It does the scrolling slowly (I can also control the speed by extending the LinearLayoutManager and overriding calculateSpeedPerPixel() - see How can i control the scrolling speed of recyclerView.smoothScrollToPosition(position)), however I can't control the exact location of the list item on the screen after the scrolling. All I know is it will be fully visible on the screen.
I have tried:
scrollToX(0, targetPosition * itemHeight - screenHeight / 2)
It gives me the error message: "W/RecyclerView: RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead"
I have also tried replacing the RecyclerView by a LinearLayoutManager which contains all the items as children, and translate the LinearLayoutManager in the view, but I don't get the children initially outside of the screen to draw.
Is there a solution to this issue?
You can calculate the smoothScrollToPosition's targetPosition based on your actual target position and the number of visible items.
A quick POC on how to do that:
val scrollToPosition = 50
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstPosition = layoutManager.findFirstVisibleItemPosition()
val lastPosition = layoutManager.findLastVisibleItemPosition()
val visibleItems = lastPosition - firstPosition + 1
if (firstPosition < scrollToPosition) {
recyclerView.smoothScrollToPosition(scrollToPosition + (visibleItems / 2))
} else {
recyclerView.smoothScrollToPosition(scrollToPosition - (visibleItems / 2))
}
If you want more precise results that the item should be exactly at the middle of the screen, you can use the heights of the item (since its fixed) and the height of the RecyclerView, then calculate the offset to scroll. And call:
recyclerView.scrollBy(dx, dy)
Or:
recyclerView.smoothScrollBy(dx, dy)
Thank you #Bob. I missed scrollBy(). Following your advice, here is the code that worked for me.
class RecyclerViewFixedItemSize : RecyclerView {
var itemFixedSize : Int = 0
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
}
constructor(context: Context) : super(context) {
}
fun smoothScrollToPositionCentered(position: Int) {
// Find the fixed item size - once for all
if (itemFixedSize == 0) {
val ll = layoutManager as LinearLayoutManager
val fp = ll.findFirstCompletelyVisibleItemPosition()
val fv = ll.getChildAt(fp)
itemFixedSize = fv!!.height
}
// find the recycler view position and screen coordinates of the first item (partially) visible on screen
val ll = layoutManager as LinearLayoutManagerFixedItemSize
val fp = ll.findFirstVisibleItemPosition()
val fv = ll.getChildAt(0)
// find the middle of the recycler view
val dyrv = (top - bottom ) / 2
// compute the number of pixels to scroll to get the view position in the center
val dy = (position - fp) * itemFixedSize + dyrv + fv!!.top + itemFixedSize / 2
smoothScrollBy(0, dy)
}
}
I have layout like this:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="#layout/app_bar_main"
android:orientation="vertical">
<FrameLayout
android:id="#+id/folderContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<FrameLayout
android:id="#+id/playerControlsContainer"
android:layout_width="match_parent"
android:layout_height="#dimen/control_view_height_min"
android:visibility="visible">
<fragment
android:id="#+id/playerControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:name="eu.zderadicka.audioserve.fragments.ControllerFragment"
/>
</FrameLayout>
</LinearLayout>
And I want to achieve this:
by pulling (scrolling) up bottom frame up to expand it's height from control_view_height_min (80dp) to control_view_height_max (200dp)
when fram is expanded pull it back again to control_view_height_min by pulling (scrolling) down
while pulling change height as I pull
after stop pulling animate rest of the way
I've have been looking at "Android Sliding Up Panel", but it looks bit too complex for this case and it always pulls bottom frame to full screen.
So my question is what would be simplest way to do it in standard SDK? What to use and how? Are the some code samples close to my requirements.
Thanks for any tips.
Ok so I had to find it myself. Created subclass of FrameLayout, which can be expanded/collapsed by dragging. Much simpler then Sliding Up Panel and what is more important it works in my setup. If anyone is interested (source in kotlin):
package eu.zderadicka.audioserve.ui
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.widget.FrameLayout
import eu.zderadicka.audioserve.R
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
private const val LOG_TAG = "ExpandableFrameLayout"
class ExpandableFrameLayout #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0)
: FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
var minHeight: Int = 0
var maxHeight: Int = 0
val gestureDetector: GestureDetector
var animator: ValueAnimator? = null
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
var midHeight: Int = 0
fun animate(toValue: Int) {
animator?.cancel()
animator = ValueAnimator.ofInt(height, toValue).apply {
this.addUpdateListener {
layoutParams.height = it.animatedValue as Int
requestLayout()
}
start()
}
}
inner class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
var isDragging = false
var startHeight = 0
override fun onDown(e: MotionEvent?): Boolean {
startHeight = layoutParams.height
isDragging = false
return true
}
// override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// Log.d(LOG_TAG, "Fling with velocity $velocityY")
// isDragging = true
// if (velocityY < 0 && height < maxHeight) {
// animate(maxHeight)
//
// } else if (velocityY > 0 && height > minHeight) {
// animate(minHeight)
// }
//
// return true
// }
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
val dist = e1.rawY - e2.rawY
Log.d(LOG_TAG, "Scroll in Y $dist")
isDragging = true
if (dist > touchSlop && height < maxHeight) {
layoutParams.height = min(startHeight + dist.roundToInt(), maxHeight)
requestLayout()
} else if (dist < -touchSlop && height > minHeight) {
layoutParams.height = max(startHeight + dist.roundToInt(), minHeight)
requestLayout()
}
return true
}
fun dragging() = isDragging
}
val gestureListener = MyGestureListener()
init {
val customAttrs = context.theme.obtainStyledAttributes(attrs, R.styleable.ExpandableFrameLayout, 0, 0)
minHeight = customAttrs.getLayoutDimension(R.styleable.ExpandableFrameLayout_minExpandableHeight, 0)
maxHeight = customAttrs.getLayoutDimension(R.styleable.ExpandableFrameLayout_maxExpandableHeight, 1024)
midHeight = minHeight+ (maxHeight - minHeight) / 2
gestureDetector = GestureDetector(context, this.gestureListener)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
Log.d(LOG_TAG, "Player touch $event")
if (event.actionMasked == MotionEvent.ACTION_UP) {
if (layoutParams.height >= midHeight && layoutParams.height < maxHeight)
animate(maxHeight)
else if (layoutParams.height < midHeight && layoutParams.height > minHeight)
animate(minHeight)
}
return gestureDetector.onTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
//listen also to all child events in upper part
if (event.y <= minHeight) {
onTouchEvent(event)
// need to block up event, which were dragging
if (event.actionMasked == MotionEvent.ACTION_UP) {
return gestureListener.isDragging
}
}
return super.onInterceptTouchEvent(event)
}
// this should be called from containing frame/activity in onPause
fun onPause() {
if (layoutParams.height >= minHeight) {
layoutParams?.height = minHeight
}
}
}
Gotchas:
when changing height or other layout properties call requestLayout
use rawY for scrolling - ( y is related to the view and thus scrolling motion was shaky)
as child views (buttons in my case) are consuming motion events I had to send them also from onIntercentTouchEvent plus block then unwanted ACTION_UP events.
finally it was easier and gave better results to just use onScroll - it gave better experience (but for shorter bottom panel) - combination with onFling gave bit more complex movements.
I wish somebody told me about those things here, it could save me some hours. However it looks like SO is now more about down voting then providing useful hints.
I need to optimize my Android app to look good on phones with a notch, like the Essential Phone.
This phones have different status bar heights than the standard 25dp value, so you can't hardcode that value.
The Android P developer preview released yesterday includes support for the notch and some APIs to query its location and bounds, but for my app I only need to be able to get the status bar height from XML, and not use a fixed value.
Unfortunately, I couldn't find any way to get that value from XML.
Is there any way?
Thank you.
I have already found the answer:
The recommended way is to use the WindowInsets API, like the following:
view.setOnApplyWindowInsetsListener { v, insets ->
view.height = insets.systemWindowInsetTop // status bar height
insets
}
Accessing the status_bar_height, as it's a private resource and thus subject to change in future versions of Android, is highly discouraged.
It is not a solution, but it can explain you how it should be don in correct way and how things are going under the hood:
https://www.youtube.com/watch?v=_mGDMVRO3iE
OLD SOLUTION:
Here is small trick:
#*android:dimen/status_bar_height
android studio will show you an error, but it will work like a charm :)
Method provides the height of status bar
public int getStatusBarHeight() {
int result = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}
class StatusBarSpacer #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
View(context, attrs) {
private var statusHeight: Int = 60
init {
if (context is Activity && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
context.window.decorView.setOnApplyWindowInsetsListener { _, insets ->
statusHeight = insets.systemWindowInsetTop
requestLayout()
insets
}
context.window.decorView.requestApplyInsets()
} else statusHeight = resources.getDimensionPixelSize(
resources.getIdentifier("status_bar_height", "dimen", "android")
)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) =
setMeasuredDimension(0, statusHeight)
}
I have found a solution to work with status bar size that should be fit to any device. It is a way to transform the size from code to XML layouts.
The solution is to create a view that is auto-sized with the status bar size. It is like a guide from constraint layout.
I am sure it should be a better method but I didn't find it. And if you consider something to improve this code please let me now:
KOTLIN
class StatusBarSizeView: View {
companion object {
// status bar saved size
var heightSize: Int = 0
}
constructor(context: Context):
super(context) {
this.init()
}
constructor(context: Context, attrs: AttributeSet?):
super(context, attrs) {
this.init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int):
super(context, attrs, defStyleAttr) {
this.init()
}
private fun init() {
// do nothing if we already have the size
if (heightSize != 0) {
return
}
// listen to get the height
(context as? Activity)?.window?.decorView?.setOnApplyWindowInsetsListener { _, windowInsets ->
// get the size
heightSize = windowInsets.systemWindowInsetTop
// return insets
windowInsets
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// if height is not zero height is ok
if (h != 0 || heightSize == 0) {
return
}
// apply the size
postDelayed(Runnable {
applyHeight(heightSize)
}, 0)
}
private fun applyHeight(height: Int) {
// apply the status bar height to the height of the view
val lp = this.layoutParams
lp.height = height
this.layoutParams = lp
}
}
Then you can use it in XML as a guide:
<com.foo.StatusBarSizeView
android:id="#+id/fooBarSizeView"
android:layout_width="match_parent"
android:layout_height="0dp" />
The "heightSize" variable is public to can use it in the code if some view need it.
Maybe it helps to someone else.
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