View's height is 0 under certain conditions when transitioning between Activities - android

Here is the context: I have a Activity with complex layout to which I want to transition using relatively complex Transitions. The catch is that I need to position one view (scroll_frame) below the image (I can't use XML), so I need to know it's height. It works fine the first time, however after some number of going back and forth between Activities image's height is suddenly zero (caching, race conditions?)
Here is the snippet of onCreate:
super.onCreate(savedInstanceState)
postponeEnterTransition()
setContentView(R.layout.some_id)
// ... non-essential stuff.
Glide.with(this)
.load([some resource id])
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(d: Drawable?, t: Transition<in Drawable>?) {
// Prepare image.
image.setImageDrawable(d)
val margin = Math.max(image.measuredHeight, 0)
// ... non-essential stuff
layoutParams.setMargins(0, margin, 0, 0)
// Critical part.
scroll_frame?.layoutParams = layoutParams
// Start transition.
startPostponedEnterTransition()
}
})

The thing is that views are not given the measured height in onCreate.
Try this:
//Put this in onCreate
imageView.post(new Runnable() {
#Override
public void run() {
//This is called after view measurements are done.
//Run your glide code here.
}
});
Hope this helps.

that's because your view isn't set yet:
use this:
image.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
//don't forget to remove the listener to prevent being called again by future layout events:
image.getViewTreeObserver().removeOnGlobalLayoutListener(this);
val margin = Math.max(image.measuredHeight, 0)
// ... non-essential stuff
layoutParams.setMargins(0, margin, 0, 0)
// Critical part.
scroll_frame?.layoutParams = layoutParams
// Start transition.
startPostponedEnterTransition()
}
}

Related

onGlobalLayout differentiate between various invocations

I have a logo view, which is a full screen fragment containing single ImageView.
I have to perform some operations after the logo image is completely visible.
Following code is used to invoke the special task
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ImageView logoImageMaster = new ImageView(getContext());
//logoImageMaster.setImageResource(resID); //even after removing this, i am getting the callback twice
try {
// get input stream
InputStream ims = getActivity().getAssets().open("product_logo.png");
// load image as Drawable
Drawable d = Drawable.createFromStream(ims, null);
// set image to ImageView
logoImageMaster.setImageDrawable(d);
}
catch(IOException ex) {
}
logoImageMaster.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() { //FIXME get called twice. Check this out, no info to distinguish first from second
// Log.e("PANEL", "onGlobalLayout of Logo IV ---------------------------------");
activityInterface.doSpecialLogic();
}
});
return logoImageMaster;
}
My exact problem is, onGlobalLayout is called twice for this view hierarchy.
I know that onGlobalLayout is invoked in performTraversal of View.java hence this is expected.
For my use case of Single parent with Single child view, I want to distinguish the view attributes such that doSpecialLogic is called once[onGlobalLayout is called twice] , after the logo image is completely made visible.
Please suggest some ideas.
OnGlobalLayoutListener gets called every time the view layout or visibility changes. Maybe you reset the views in your doSpecialLogic call??
edit
as #Guille89 pointed out, the two set calls cause onGlobalLayout to be called two times
Anyhow, if you want to call OnGlobalLayoutListener just once and don't need it for anything else, how about removing it after doSpecialLogic() call??
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
//noinspection deprecation
logoImageMaster.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
logoImageMaster.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
activityInterface.doSpecialLogic();
It seems to be called one time for each set done over the imageView
logoImageMaster.setImageResource(resID);
logoImageMaster.setImageDrawable(d);
You should Try using kotlin plugin in android
This layout listener is usually used to do something after a view is measured, so you typically would need to wait until width and height are greater than 0. And we probably want to do something with the view that called it,in your case
Imageview
So generified the function so that it can be used by any object that extends View and also be able to access to all its specific functions and properties from the function
[kotlin]
inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
[/kotlin]
Note:
make sure that ImageView is described properly in the layout. That is its layout_width and layout_height must not be wrap_content. Moreover, other views must not result in this ImageView has 0 size.

Calling View.getTop() in Robolectric always returns 0 [duplicate]

I am creating all of the elements in my android project dynamically. I am trying to get the width and height of a button so that I can rotate that button around. I am just trying to learn how to work with the android language. However, it returns 0.
I did some research and I saw that it needs to be done somewhere other than in the onCreate() method. If someone can give me an example of how to do it, that would be great.
Here is my current code:
package com.animation;
import android.app.Activity;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.LinearLayout;
public class AnimateScreen extends Activity {
//Called when the activity is first created.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout ll = new LinearLayout(this);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(30, 20, 30, 0);
Button bt = new Button(this);
bt.setText(String.valueOf(bt.getWidth()));
RotateAnimation ra = new RotateAnimation(0,360,bt.getWidth() / 2,bt.getHeight() / 2);
ra.setDuration(3000L);
ra.setRepeatMode(Animation.RESTART);
ra.setRepeatCount(Animation.INFINITE);
ra.setInterpolator(new LinearInterpolator());
bt.startAnimation(ra);
ll.addView(bt,layoutParams);
setContentView(ll);
}
Any help is appreciated.
The basic problem is, that you have to wait for the drawing phase for the actual measurements (especially with dynamic values like wrap_content or match_parent), but usually this phase hasn't been finished up to onResume(). So you need a workaround for waiting for this phase. There a are different possible solutions to this:
1. Listen to Draw/Layout Events: ViewTreeObserver
A ViewTreeObserver gets fired for different drawing events. Usually the OnGlobalLayoutListener is what you want for getting the measurement, so the code in the listener will be called after the layout phase, so the measurements are ready:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
view.getHeight(); //height is ready
}
});
Note: The listener will be immediately removed because otherwise it will fire on every layout event. If you have to support apps SDK Lvl < 16 use this to unregister the listener:
public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)
2. Add a runnable to the layout queue: View.post()
Not very well known and my favourite solution. Basically just use the View's post method with your own runnable. This basically queues your code after the view's measure, layout, etc. as stated by Romain Guy:
The UI event queue will process events in order. After
setContentView() is invoked, the event queue will contain a message
asking for a relayout, so anything you post to the queue will happen
after the layout pass
Example:
final View view=//smth;
...
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
}
});
The advantage over ViewTreeObserver:
your code is only executed once and you don't have to disable the Observer after execution which can be a hassle
less verbose syntax
References:
https://stackoverflow.com/a/3602144/774398
https://stackoverflow.com/a/3948036/774398
3. Overwrite Views's onLayout Method
This is only practical in certain situation when the logic can be encapsulated in the view itself, otherwise this is a quite verbose and cumbersome syntax.
view = new View(this) {
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
view.getHeight(); //height is ready
}
};
Also mind, that onLayout will be called many times, so be considerate what you do in the method, or disable your code after the first time
4. Check if has been through layout phase
If you have code that is executing multiple times while creating the ui you could use the following support v4 lib method:
View viewYouNeedHeightFrom = ...
...
if(ViewCompat.isLaidOut(viewYouNeedHeightFrom)) {
viewYouNeedHeightFrom.getHeight();
}
Returns true if view has been through at least one layout since it was
last attached to or detached from a window.
Additional: Getting staticly defined measurements
If it suffices to just get the statically defined height/width, you can just do this with:
View.getMeasuredWidth()
View.getMeasuredHeigth()
But mind you, that this might be different to the actual width/height after drawing. The javadoc describes the difference in more detail:
The size of a view is expressed with a width and a height. A view
actually possess two pairs of width and height values.
The first pair is known as measured width and measured height. These
dimensions define how big a view wants to be within its parent (see
Layout for more details.) The measured dimensions can be obtained by
calling getMeasuredWidth() and getMeasuredHeight().
The second pair is simply known as width and height, or sometimes
drawing width and drawing height. These dimensions define the actual
size of the view on screen, at drawing time and after layout. These
values may, but do not have to, be different from the measured width
and height. The width and height can be obtained by calling getWidth()
and getHeight().
We can use
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
You are calling getWidth() too early. The UI has not been sized and laid out on the screen yet.
I doubt you want to be doing what you are doing, anyway -- widgets being animated do not change their clickable areas, and so the button will still respond to clicks in the original orientation regardless of how it has rotated.
That being said, you can use a dimension resource to define the button size, then reference that dimension resource from your layout file and your source code, to avoid this problem.
I used this solution, which I think is better than onWindowFocusChanged(). If you open a DialogFragment, then rotate the phone, onWindowFocusChanged will be called only when the user closes the dialog):
yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
// Here you can get the size :)
}
});
Edit : as removeGlobalOnLayoutListener is deprecated, you should now do :
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
yourView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
else {
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// Here you can get the size :)
}
If you need to get width of some widget before it is displayed on screen, you can use getMeasuredWidth() or getMeasuredHeight().
myImage.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = myImage.getMeasuredWidth();
int height = myImage.getMeasuredHeight();
As Ian states in this Android Developers thread:
Anyhow, the deal is that layout of the
contents of a window happens
after all the elements are constructed and added to their parent
views. It has to be this way, because
until you know what components a View
contains, and what they contain, and
so on, there's no sensible way you can
lay it out.
Bottom line, if you call getWidth()
etc. in a constructor, it will return
zero. The procedure is to create all
your view elements in the constructor,
then wait for your View's
onSizeChanged() method to be called --
that's when you first find out your
real size, so that's when you set up
the sizes of your GUI elements.
Be aware too that onSizeChanged() is
sometimes called with parameters of
zero -- check for this case, and
return immediately (so you don't get a
divide by zero when calculating your
layout, etc.). Some time later it
will be called with the real values.
I would rather use OnPreDrawListener() instead of addOnGlobalLayoutListener(), since it is called a bit earlier than other listeners.
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
if (view.getViewTreeObserver().isAlive())
view.getViewTreeObserver().removeOnPreDrawListener(this);
// put your code here
return true;
}
});
Adjusted the code according to comment of #Pang. onPreDraw method should return true to proceed with the current drawing pass.
AndroidX has multiple extension functions that help you with this kind of work, inside androidx.core.view
You need to use Kotlin for this.
The one that best fits here is doOnLayout:
Performs the given action when this view is laid out. If the view has been laid out and it has not requested a layout, the action will be performed straight away otherwise, the action will be performed after the view is next laid out.
The action will only be invoked once on the next layout and then removed.
In your example:
bt.doOnLayout {
val ra = RotateAnimation(0,360,it.width / 2,it.height / 2)
// more code
}
Dependency: androidx.core:core-ktx:1.0.0
A Kotlin Extension to observe on the global layout and perform a given task when height is ready dynamically.
Usage:
view.height { Log.i("Info", "Here is your height:" + it) }
Implementation:
fun <T : View> T.height(function: (Int) -> Unit) {
if (height == 0)
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(height)
}
})
else function(height)
}
It happens because the view needs more time to be inflated. So instead of calling view.width and view.height on the main thread, you should use view.post { ... } to make sure that your view has already been inflated. In Kotlin:
view.post{width}
view.post{height}
In Java you can also call getWidth() and getHeight() methods in a Runnable and pass the Runnable to view.post() method.
view.post(new Runnable() {
#Override
public void run() {
view.getWidth();
view.getHeight();
}
});
One liner if you are using RxJava & RxBindings. Similar approach without the boilerplate. This also solves the hack to suppress warnings as in the answer by Tim Autin.
RxView.layoutChanges(yourView).take(1)
.subscribe(aVoid -> {
// width and height have been calculated here
});
This is it. No need to be unsubscribe, even if never called.
Maybe this helps someone:
Create an extension function for the View class
filename: ViewExt.kt
fun View.afterLayout(what: () -> Unit) {
if(isLaidOut) {
what.invoke()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
what.invoke()
}
})
}
}
This can then be used on any view with:
view.afterLayout {
do something with view.height
}
Height and width are zero because view has not been created by the time you are requesting it's height and width .
One simplest solution is
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
view.getWidth(); //width is ready
}
});
This method is good as compared to other methods as it is short and crisp.
If you are using Kotlin
customView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
customView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
else {
customView.viewTreeObserver.removeGlobalOnLayoutListener(this)
}
// Here you can get the size :)
viewWidth = customView.width
}
})
Answer with post is incorrect, because the size might not be recalculated.
Another important thing is that the view and all it ancestors must be visible. For that I use a property View.isShown.
Here is my kotlin function, that can be placed somewhere in utils:
fun View.onInitialized(onInit: () -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
onInit()
}
}
})
}
And the usage is:
myView.onInitialized {
Log.d(TAG, "width is: " + myView.width)
}
For Kotlin:
I have faced a production crash due to use view.height/ view.width which lead to NaN while I was using View.post() which sometimes view diemsions returned with 0 value.
So,
Use view.doOnPreDraw { // your action here} which is:
OneShotPreDrawListener so it called only one time.
Implements OnPreDrawListener which make sure view is layouted and measured
well , you can use addOnLayoutChangeListener
you can use it in onCreate in Activity or onCreateView in Fragment
#Edit
dont forget to remove it because in some cases its trigger infinite loop
myView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener{
override fun onLayoutChange(
v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
) {
if (v?.width > 0 && v?.height > 0){
// do something
Log.i(TAG, "view : ${view.width}")
// remove after finish
v?.removeOnLayoutChangeListener(this)
}
}
})
Cleanest way of doing this is using post method of view :
kotlin:
view.post{
var width = view.width
var height = view.height
}
Java:
view.post(new Runnable() {
#Override
public void run() {
int width = view.getWidth();
int height = view.getHeight();
}
});
Gone views returns 0 as height if app in background.
This my code (1oo% works)
fun View.postWithTreeObserver(postJob: (View, Int, Int) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(widthSpec, heightSpec)
postJob(this#postWithTreeObserver, measuredWidth, measuredHeight)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
#Suppress("DEPRECATION")
viewTreeObserver.removeGlobalOnLayoutListener(this)
} else {
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
}
We need to wait for view will be drawn. For this purpose use OnPreDrawListener. Kotlin example:
val preDrawListener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
// code which requires view size parameters
return true
}
}
view.viewTreeObserver.addOnPreDrawListener(preDrawListener)
In my case, I can't get a view's height by post or by addOnGlobalLayoutListener, it's always 0. Because my view is in a fragment, and the fragment is the second tab in MainActivity. when I open MainActivity, I enter the first tab, so the second tab doesn't show on the screen. But onGlobalLayout() or post() function still has a callback.
I get the view's height when the second fragment is visible on the screen. And this time I get the correct height.
Usage:
imageView.size { width, height ->
//your code
}
View extention:
fun <T : View> T.size(function: (Int, Int) -> Unit) {
if (isLaidOut && height != 0 && width != 0) {
function(width, height)
} else {
if (height == 0 || width == 0) {
var onLayoutChangeListener: View.OnLayoutChangeListener? = null
var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
removeOnLayoutChangeListener(onLayoutChangeListener)
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(width, height)
}
}
}
onLayoutChangeListener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val width = v?.width ?: 0
val height = v?.height ?: 0
if (width > 0 && height > 0) {
// remove after finish
viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
v?.removeOnLayoutChangeListener(this)
function(width, height)
}
}
}
viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
addOnLayoutChangeListener(onLayoutChangeListener)
} else {
function(width, height)
}
}
}
public final class ViewUtils {
public interface ViewUtilsListener {
void onDrawCompleted();
}
private ViewUtils() {
}
public static void onDraw(View view, ViewUtilsListener listener) {
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (view.getHeight() != 0 && view.getWidth() != 0) {
if (listener != null) {
listener.onDrawCompleted();
}
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
}
}
you can use like this ;
ViewUtils.onDraw(view, new ViewUtils.ViewUtilsListener() {
#Override
public void onDrawCompleted() {
int width = view.getWidth();
int height = view.getHeight();
}
});
private val getWidth: Int
get() {
if (Build.VERSION.SDK_INT >= 30) {
val windowMetrics =windowManager.currentWindowMetrics
val bounds = windowMetrics.bounds
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = bounds.width().toFloat()
}
val density = resources.displayMetrics.density
val adWidth = (adWidthPixels / density).toInt()
return adWidth
} else {
val display = windowManager.defaultDisplay
val outMetrics = DisplayMetrics()
display.getMetrics(outMetrics)
val density = outMetrics.density
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = outMetrics.widthPixels.toFloat()
}
val adWidth = (adWidthPixels / density).toInt()
return adWidth
}
}
replace (View) with the view you want to measure
This is a little old, but was having trouble with this myself (needing to animate objects in a fragment when it is created). This solution worked for me, I believe it is self explanatory.
class YourFragment: Fragment() {
var width = 0
var height = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val root = inflater.inflate(R.layout.fragment_winner_splash, container, false)
container?.width.let {
if (it != null) {
width = it
}
}
container?.height.let {
if (it != null) {
height = it
}
}
return root
}
If you're worried about overworking the onDraw method, you can always set the dimension as null during construction and then only set the dimension inside of onDraw if it's null.
That way you're not really doing any work inside onDraw
class myView(context:Context,attr:AttributeSet?):View(context,attr){
var height:Float?=null
override fun onDraw(canvas:Canvas){
if (height==null){height=this.height.toFloat()}
}
}

Android: method called after creation of activity interface

I would like to perform some task after the creation of the graphic interface of the activity. I need to know the exact height and width of some views and change the layoutParams of some of those views based on the width and height. In the onResume method the views have all the parameters still equal to 0...
As for now I'm using a delayed task that runs after some time from the onCreate but this isn't a good solution at all...
What is the last method called in the activity creation? And are the views' width and height available in such method?
Call this inside of the onCreate()
final View rootView = getWindow().getDecorView().getRootView();
rootView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//by now all views will be displayed with correct values
}
});
onResume() is last, but perhaps better is onViewCreated(). Its advantage is that it is not invoked every time you regain focus. But try getting properties of your view inside of post() over layout element which you need. For example:
textView.post(new Runnable() {
#Override
public void run() {
// do something with textView
}
});
The last method that runs when activity starts is onResume().
You can find it at Activity lifecycle.
If that is not good enough for you, run delayed task from this onResume() and you'll be fine.
In the last line of the onResume() method get all the data you want. It should show you all that you need.
Use a Coroutine:
In onCreate() ...
CoroutineScope(SupervisorJob()).launch {
getImageAttributes()
}
... then use a while loop to wait until image is in View ...
private fun getImageAttributes() {
while (imageView.height == 0) { /* */ }
val h = imageView.height
val w = imageView.width
val b = imageView.clipBounds
val x = imageView.x
val y = imageView.y
val tx = imageView.translationX
val ty = imageView.translationY
}
You should add a timeout or make the Job cancellable if there is a chance that the imageView.height is going to be 0.

Get content view size in onCreate

I'm looking for a good way to measure the dimensions of the actual content area for an activity in Android.
Getting display always works. Simply go like this:
Display display = getWindowManager().getDefaultDisplay();
And you can get the pixel count for the entire screen. Of course this does not take into consideration the ActionBar, status bar, or any other views which will reduce the available size of the activity itself.
Once the activity is running, you can do this:
View content = getWindow().findViewById(Window.ID_ANDROID_CONTENT);
To get the activity content only. But doing this in onCreate() will result in a view with width and height of 0, 0.
Is there a way to get these dimensions during onCreate? I imagine there ought to be a way to get the measurements of any status bars and just subtract that from the total display size, but I'm unable to find a way to do that. I think this would be the only way, because the content window method will always return a view with no width/height before it is drawn.
Thanks!
You can use a layout or pre-draw listener for this, depending on your goals. For example, in onCreate():
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//Remove it here unless you want to get this callback for EVERY
//layout pass, which can get you into infinite loops if you ever
//modify the layout from within this method.
content.getViewTreeObserver().removeGlobalOnLayoutListener(this);
//Now you can get the width and height from content
}
});
Update
as of API 16 removeGlobalOnLayoutListener is deprecated.
Change to:
content.getViewTreeObserver().removeOnGlobalLayoutListener(this)
(copied from my answer to a related question)
I use the following technique - post a runnable from onCreate() that will be executed when the view has been created:
contentView = findViewById(android.R.id.content);
contentView.post(new Runnable()
{
public void run()
{
contentHeight = contentView.getHeight();
}
});
This code will run on the main UI thread, after onCreate() has finished.
Answer with post is incorrect, because the size might not be recalculated.
Another important thing is that the view and all it ancestors must be visible. For that I use a property View.isShown.
Here is my kotlin function, that can be placed somewhere in utils:
fun View.onInitialized(onInit: () -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
onInit()
}
}
})
}
And the usage is:
myView.onInitialized {
Log.d(TAG, "width is: " + myView.width)
}
This appears to be a duplicate question. There is an elegant answer within this SO question:
getWidth() and getHeight() of View returns 0
Which (shamelessly copied) is to override onWindowFocusChanged(), which seems to fire just after onCreate(), and where the sizes are rendered:
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
If you want lo load a Bitmap into a ImageView within OnCreate(), you can use this example to do it:
public static void setImageLater(#NonNull final ImageView imageView,final Bitmap bitmap){
final ViewTreeObserver observer = imageView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
imageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
imageView.setImageBitmap(bitmap);
}
});
}
You can do any work wich needs sizes in onResume(using flag like alreadyDone not to repeat it every time an Activity goes foreground). In onCreate views are not displayed, so it's normal that sizes are zeros.

How to programmatically scroll an HorizontalScrollView

I have an HorizontalScrollView which contains a RelativeLayout. This layout is empty in the XML, and is populated from java in the onCreate.
I would like this scroll view to be initially somewhere in the middle of the RelativeLayout, which is way larger than the screen.
I tried mHorizScrollView.scrollTo(offsetX, 0); which doesn't work.
I don't know what's wrong with this.
I could post the code, but it is not really relevant. What matters is that everything is done programatically (has to :s), and that the initial position of the HorizontalScrollView has to be set programmatically.
Thanks for reading. Please tell me if you need more details or if this is not clear enough.
I found that if you extend the HorizontalScrollView and override the onLayout, you can cleanly scroll, after the super call to onLayout:
MyScrollView extends HorizontalScrollView {
protected void onLayout (boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
this.scrollTo(wherever, 0);
}
}
EDIT:
For scrolling to start or end you can use:
this.fullScroll(HorizontalScrollView.FOCUS_RIGHT/FOCUS_LEFT);
instead of
this.scrollTo(wherever, 0);
public void autoSmoothScroll() {
final HorizontalScrollView hsv = (HorizontalScrollView) view.findViewById(R.id.horiscroll);
hsv.postDelayed(new Runnable() {
#Override
public void run() {
//hsv.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
hsv.smoothScrollBy(500, 0);
}
},100);
}
To test whether it's a timing issue (which I think it is), instead of calling scrollTo() in onStart, call postDelayed() with a Runnable that calls scrollTo, with a delay of 30 or so.
A better approach would be using the ViewTreeObserver to observe layouts.
View interestedInView;
onCreate(){
//Code
//Observe for a layout change
ViewTreeObserver viewTreeObserver = interestedInView.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//REMOVE THE VIEWTREE OBSERVER
//interestedInView is ready for size and position queries because it has been laid out
}
});
}
}
The problem is that the view doesn't have a size yet. The Clean way to do this is to implement the onSizeChanged of the ScrollView to send a message to a Handler in your activity to in order to notify the activity that the view has a size and that scrolling is possible.
Use view.post and write scrollTo(x,y) inside run method. This the way to invalidate view in post method as per android API document.
The scrollTo function should work. What is the layout_width of the HSV, and the view inside of it?
The onSizeChanged function should work. What you fail to understand is that the onSizeChanged should send a message to a Handler of your activity and not any callback function, because a handler will execute code in the UI thread. You cannot do UI operations outside it. Have a look at handlers.
Use Kotlin extension function.
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
Usage:
mHorizontalScrollView.afterMeasured {
var scrollTo = 0
val count = (layout.svList.getChildAt(0) as LinearLayout)
.childCount
if(index < count){
val child = (layout.svList.getChildAt(0) as LinearLayout)
.getChildAt(index)
scrollTo = child.width
}
Log.d(TAG, "scrollToComment: $scrollTo")
mHorizontalScrollView.scrollTo(scrollTo, 0)
}

Categories

Resources