Adding a simple ScrollView to Gallery causes a memory leak - android

I've run into what I can only categorize as a memory leak for ScrollView elements when using the Gallery component.
A short background. I've got an existing app that is a photo slideshow app.
It uses the Gallery component, but each element in the adapter is displayed in full-screen.
(full source is available at this link)
The adapter View element consist of an ImageView, and two TextViews for title and description.
As the photos are of a quite high-resolution, the app uses quite a lot of memory but the Gallery has in general manage to recycle them well.
However, when I am now implementing a ScrollView for the description TextView, I almost immediately run into memory problems. This the only change I made
<ScrollView
android:id="#+id/description_scroller"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:fillViewport="true">
<TextView
android:id="#+id/slideshow_description"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/white"
android:layout_below="#id/slideshow_title"
android:singleLine="false"
android:maxLines="4"/>
</ScrollView>
I did a heap dump and could clearly see that it was the Scrollview which was the root of the memory problems.
Here are two screenshots from the heap dump analysis. Note that the ScrollView retains a reference to mParent which includes the large photo I use
PS same problem occurs if I use the TextView's scrolling (android:scrollbars = "vertical" and .setMovementMethod(new ScrollingMovementMethod());
PSS Tried switching off persistent drawing cache, but no different dreaandroid:persistentDrawingCache="none"

Have you tried removing the scroll view whenever it's container view scrolls off the screen? I'm not sure if that works for you but its worth a shot? Alternatively, try calling setScrollContainer(false) on the scroll view when it leaves the screen. That seems to remove the view from the mScrollContainers set.
Also, this question, answered by Dianne Hackborn (android engineer), explicitly states not to use scrollable views inside of a Gallery. Maybe this issue is why?

Just add this -> android:isScrollContainer="false"
<ScrollView
android:id="#+id/description_scroller"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:fillViewport="true"
android:isScrollContainer="false">
There is some source why this is appear:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/view/View.java
the problem is:
setScrollContainer(boolean isScrollContainer)
by default:
boolean setScrollContainer = false;
but in some cases like this
if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
setScrollContainer(true);
}
it can be true, and when it happends
/**
* Change whether this view is one of the set of scrollable containers in
* its window. This will be used to determine whether the window can
* resize or must pan when a soft input area is open -- scrollable
* containers allow the window to use resize mode since the container
* will appropriately shrink.
*/
public void setScrollContainer(boolean isScrollContainer) {
if (isScrollContainer) {
if (mAttachInfo != null && (mPrivateFlags&SCROLL_CONTAINER_ADDED) == 0) {
mAttachInfo.mScrollContainers.add(this);
mPrivateFlags |= SCROLL_CONTAINER_ADDED;
}
mPrivateFlags |= SCROLL_CONTAINER;
} else {
if ((mPrivateFlags&SCROLL_CONTAINER_ADDED) != 0) {
mAttachInfo.mScrollContainers.remove(this);
}
mPrivateFlags &= ~(SCROLL_CONTAINER|SCROLL_CONTAINER_ADDED);
}
}
mAttachInfo.mScrollContainers.add(this) - all view put into ArrayList this lead to leak of memory sometimes

Yes i noticed the problem, sorry for my previous comment, i've tried to empty the Drawables
by setting previous Drawable.setCallBack(null); but didnt work, btw i have nearly the same project, i use ViewFlipper instead of Gallery, so i can control every thing, and i just use 2 Views in it, and switch between them, and no memory leak, and why not you resize the Image before displaying it, so it will reduce memory usage (search SO for resizing Image before reading it)

Try moving "android:layout_below="#id/slideshow_title" in TextView to ScrollView.

Ended up with implementing a workaround that uses a TextSwitcher that is automatically changed to the remaining substring every x seconds.
Here is the relevant xml definition from the layout
<TextSwitcher
android:id="#+id/slideshow_description"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="#+id/slideshow_description_anim1"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textColor="#color/white"
android:singleLine="false"/>
<TextView
android:id="#+id/slideshow_description_anim2"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textColor="#color/white"
android:singleLine="false"/>
</TextSwitcher>
Here I add the transition animation to the TextSwitcher (in the adapter's getView method)
final TextSwitcher slideshowDescription = (TextSwitcher)slideshowView.findViewById(R.id.slideshow_description);
Animation outAnim = AnimationUtils.loadAnimation(context,
R.anim.slide_out_down);
Animation inAnim = AnimationUtils.loadAnimation(context,
R.anim.slide_in_up);
slideshowDescription.setInAnimation(inAnim);
slideshowDescription.setOutAnimation(outAnim);
Here is how I swap to the part of the description
private void updateScrollingDescription(SlideshowPhoto currentSlideshowPhoto, TextSwitcher switcherDescription){
String description = currentSlideshowPhoto.getDescription();
TextView descriptionView = ((TextView)switcherDescription.getCurrentView());
//note currentDescription may contain more text that is shown (but is always a substring
String currentDescription = descriptionView.getText().toString();
if(currentDescription == null || description==null){
return;
}
int indexEndCurrentDescription= descriptionView.getLayout().getLineEnd(1);
//if we are not displaying all characters, let swap to the not displayed substring
if(indexEndCurrentDescription>0 && indexEndCurrentDescription<currentDescription.length()){
String newDescription = currentDescription.substring(indexEndCurrentDescription);
switcherDescription.setText(newDescription);
}else if(indexEndCurrentDescription>=currentDescription.length() && indexEndCurrentDescription<description.length()){
//if we are displaying the last of the text, but the text has multiple sections. Display the first one again
switcherDescription.setText(description);
}else {
//do nothing (ie. leave the text)
}
}
And finally, here is where I setup the Timer which causes it to update every 3.5 seconds
public void setUpScrollingOfDescription(){
final CustomGallery gallery = (CustomGallery) findViewById(R.id.gallery);
//use the same timer. Cancel if running
if(timerDescriptionScrolling!=null){
timerDescriptionScrolling.cancel();
}
timerDescriptionScrolling = new Timer("TextScrolling");
final Activity activity = this;
long msBetweenSwaps=3500;
//schedule this to
timerDescriptionScrolling.scheduleAtFixedRate(
new TimerTask() {
int i=0;
public void run() {
activity.runOnUiThread(new Runnable() {
public void run() {
SlideshowPhoto currentSlideshowPhoto = (SlideshowPhoto)imageAdapter.getItem(gallery.getSelectedItemPosition());
View currentRootView = gallery.getSelectedView();
TextSwitcher switcherDescription = (TextSwitcher)currentRootView.findViewById(R.id.slideshow_description);
updateScrollingDescription(currentSlideshowPhoto,switcherDescription);
//this is the max times we will swap (to make sure we don't create an infinite timer by mistake
if(i>30){
timerDescriptionScrolling.cancel();
}
i++;
}
});
}
}, msBetweenSwaps, msBetweenSwaps);
}
Finally I can put this problem to a rest :)

Related

Spinner dropdown items: how is width determined?

I have a Spinner in my app, with customized dropdown views. This is what the layout of the dropdown items look like:
<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="wrap_content"
android:focusable="false"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/leadingButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_weight="0" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_weight="1">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/dropdown_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/dropdown_text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/dropdown_text" />
</RelativeLayout>
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/trailingButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_weight="0" />
</LinearLayout>
Android Studio warns me that my FrameLayout is useless. But when I take out the FrameLayout the dropdown views become narrow, and don't align with the spinner itself anymore. I have had the same problem when I tried to rewrite the drop-down items with a ConstraintLayout: the dropdown list became narrow, about half of the Spinner's size, and could not display all text, even though the ConstraintLayout had android:layout_width="match_parent".
A sketch to illustrate what I mean:
Why does this happen? How can I predict what the width of the dropdown menu will be based on the layout?
I find this dropdown view sizing quite magical
Did you look at the source code of the Spinner class? I just did. Here's what I found (API 27 Sources):
The spinner uses a ListView internally (first LOL), backed by DropdownPopup (private class):
private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
Before looking at it, look at ListPopupWindow because has a lot of info about the problems it has to deal with. It's a big class but among these things, you can see:
private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownHorizontalOffset;
private int mDropDownVerticalOffset;
It appears the DropDown is -by default- WRAPPING the content based upon the base class, however, the DropDownPopup that drives (and contains the adapter with all the items in the spinner) also has a void computeContentWidth() { method.
This method is called from the show() method, so before showing the popup, this computation happens every time.
I think here's part of the answer you're looking for:
void computeContentWidth() {
final Drawable background = getBackground();
int hOffset = 0;
if (background != null) {
background.getPadding(mTempRect);
hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
} else {
mTempRect.left = mTempRect.right = 0;
}
final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
final int spinnerPaddingRight = Spinner.this.getPaddingRight();
final int spinnerWidth = Spinner.this.getWidth();
if (mDropDownWidth == WRAP_CONTENT) {
int contentWidth = measureContentWidth(
(SpinnerAdapter) mAdapter, getBackground());
final int contentWidthLimit = mContext.getResources()
.getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
if (contentWidth > contentWidthLimit) {
contentWidth = contentWidthLimit;
}
setContentWidth(Math.max(
contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
} else if (mDropDownWidth == MATCH_PARENT) {
setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
} else {
setContentWidth(mDropDownWidth);
}
if (isLayoutRtl()) {
hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
} else {
hOffset += spinnerPaddingLeft;
}
setHorizontalOffset(hOffset);
}
You may want to DEBUG and set breakpoints here to observe what these values are and what they mean.
The other piece there is the setContentWidth() method. This method is from the ListPopupWindow, and looks like:
/**
* Sets the width of the popup window by the size of its content. The final width may be
* larger to accommodate styled window dressing.
*
* #param width Desired width of content in pixels.
*/
public void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
mDropDownWidth = mTempRect.left + mTempRect.right + width;
} else {
setWidth(width);
}
}
And setWidth (also in that class) all it does is:
/**
* Sets the width of the popup window in pixels. Can also be {#link #MATCH_PARENT}
* or {#link #WRAP_CONTENT}.
*
* #param width Width of the popup window.
*/
public void setWidth(int width) {
mDropDownWidth = width;
}
This mDropDownWidth seems used all over the place, but also made me found this other method in ListPopupWindow...
/**
* Sets the width of the popup window by the size of its content. The final width may be
* larger to accommodate styled window dressing.
*
* #param width Desired width of content in pixels.
*/
public void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
mDropDownWidth = mTempRect.left + mTempRect.right + width;
} else {
setWidth(width);
}
}
So there you have it, more logic needed including the "window dressing" (?)
I agree the Spinner is a badly designed class (or rather, with outdated design) and even more so with the name (at this Google I/O in 2019, they actually explained in one of the sessions why the name "Spinner" hint: it comes from the 1st android prototypes). By looking at all this code, it would take a few hours to figure out what the spinner is trying to do and how it works, but the trip won't be pleasant.
Good luck.
I will reiterate my advice to use ConstraintLayout which you said you were familiar with; at the very least, discard weights.
By looking at how this works (ListView!!!) the weight calculation warrants a 2nd measure/layout pass, which is not only extremely inefficient and not needed, but also may be causing issues with the internal data adapter this DropDown thing manages so the "list" is displayed.
Ultimately, another class is also involved, this is all presented in a PopupView. PopupViews are what you see when you open a Menu item for example, and are very hard to customize sometimes, depending what you want to do.
Why Google chose this approach at the time, I don't know, but it certainly warrants an update and Material Design hasn't brought much to the table in this regard yet, as it will always be incomplete or in alpha state a year behind anything else.
It is telling you the FrameLayout is useless because it has a single child view ( the Relative Layout).
Your Framelayout has width defined as so:
<FrameLayout
android:layout_width="0dp"
android:layout_weight="1"
Your relative layout has its width defined as:
<RelativeLayout
android:layout_width="match_parent"
So just removing the FrameLayout means that a different "rule" is in place for the width.
To truly replace the FrameLayout with the RelativeLayout it should look like this:
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"

Parse RelativeLayout, Set Content, And Add It To A LinearLayout

Ok... here's my situation.
I have a carousel of images in a HorizontalScrollView - which contains a LinearLayout - in my Activity, like so:
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/slider"
android:scrollbars="none" >
<LinearLayout
android:id="#+id/carousel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
/>
</HorizontalScrollView>
I have a TypedArray, loop through it, and on each run, set these images programatically, add a ClickListener and a Tag, and add this ImageView to the LinearLayout (set in my Activity Layout), like so:
// Get the array
final TypedArray carouselArray = getResources().obtainTypedArray(R.array.carousel_array);
// Populate the Carousel with item
for (int i = 0 ; i < carouselArray.length() ; ++i) {
// Image Item
ImageView outerImage;
// Set the image view resource
if(i == 0) {
outerImage.setImageResource(R.drawable.toy_filter_clear);
}
else {
outerImage.setImageResource(carouselArray.getResourceId(i, -1));
}
// Set Touch Listener
outerImage.setOnTouchListener(this);
final String prepend = "CAROUSEL_";
final String index = String.valueOf(i);
final String tag = prepend.concat(index);
outerImage.setTag(tag);
/// Add image view to the Carousel container
mCarouselContainer.addView(outerImage);
}
But now, I just found out that I have to programatically add a second image to sit inside/on top of the first image at particular coordinates (damn you UI ppl!). I need these to be considered the same image/view essentially, so need to pack them together inside of a layout, I am assuming. So I have made a layout file, like so:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/carousel_item"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="#+id/carousel_outer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="#drawable/toy_filter_normal"
/>
<ImageView
android:id="#+id/carousel_inner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="#+id/carousel_outer"
android:layout_alignRight="#+id/carousel_outer"
android:layout_marginBottom="10dp"
android:layout_marginRight="5dp"
android:src="#drawable/thumb_nofilter"
/>
</RelativeLayout>
This has the proper positioning, and the default images set on it. So what I want to be able to do is to reach into the Layout file, grab the ImageViews by their ID, overwrite the image if necessary, and then add that RelativeLayout to my LinearLayout at the end of my loop... sounds easy enough, right ?
My first attempt was to do it like this :
RelativeLayout item = (RelativeLayout) findViewById(R.id.carousel_item);
ImageView outerImage = (ImageView) item.findViewById(R.id.carousel_outer);
ImageView innerImage = (ImageView) item.findViewById(R.id.carousel_inner);
... but that gives me a NullPointer on the ImageView...So then I tried to inflate the RelativeLayout first, like this:
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.carousel_item_layout, null);
ImageView outerImage = (ImageView) view.findViewById(R.id.carousel_outer);
ImageView innerImage = (ImageView) view.findViewById(R.id.carousel_inner);
This gets rid of the NPE's, and (apparently) let's the images be set properly like so:
if(i == 0) {
outerImage.setImageResource(R.drawable.toy_filter_clear);
innerImage.setImageResource(0);
}
else {
outerImage.setImageResource(R.drawable.toy_filter_normal);
innerImage.setImageResource(carouselArray.getResourceId(i, -1));
}
but when I try to add the outerImage ImageView back to the LinearLayout, I get an NPE there:
mCarouselContainer.addView(outerImage);
More to the point, I don't want to add ONLY the one ImageView to the LinearLayout/HorizontalScrollView - I want to somehow pack the resulting images back into the RelativeLayout and add the whole thing back into my LinearLayout... but, it is worth mentioning, that this also gives me an NPE.
What is a guy to do ? Any thoughts appreciated...
Ok... Wow, thanks SO Code Monkey!
I managed to fix this with a one line fix, by adding the inflated View to the LinearLayout instead of the ImageView or the RelativeLayout (which wasn't doing anything), like so:
mCarouselContainer.addView(view);
Don't know why I hadn't tried that before, but I was unclear on whether as it's children were being updated if it would reflect the parent, so to speak... now I know it was.
I'm gonna keep the question up, as I think it's helpful... ?

How can i update the view's position who is below the view which i am animating?

I am animating a view firstView and secondView is below it.On firstView i have used object animator's property 'y' to move along in y direction but when i animate i want the secondView also move along with it because view it is below the firstView.How can i do that?
I have tried view.requestLayout() and view.invalidate() but it does not help.
Here is my layout -:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="#+id/firstView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
/>
<View
android:id="#+id/secondView"
android:layout_below="#+id/firstView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
Animations are only visually moving the view. Logically, the view is still where it was when the animation started, so the secondView will never move up because firsView technically never moved.
There are two ways to handle this depending on how you want the views to act after the animation is complete. If there are no clickable views (buttons, images, etc) within the animated view, you can apply your translation animation to firstView, and a scale animation to the second view. This will give you the effect you are looking for.
However, if there are clickable views inside, the clickable areas will not be affected by the animation and give some weird user experience with detached click areas. To handle this, you will want to create your own animation loop (on a different thread), and update the margin of firstView on each iteration. This will cause the physical location of both views to change along with their contents. If this is what you need, I can go into more detail of how to achieve this with a smooth animation.
Approach #2 in more detail
To use the second approach, you will need to create your own animation thread off of the main thread. We do not want to block the main UI thread for the duration of the animation. I'm going to assume you are wanting to move the entire view off of the screen in this example.
final View firstView = findViewById(R.id.firstView);
new Thread() {
public void run() {
long startTime = System.currentTimeMillis();
int duration = 500; // Make this whatever you want, in ms
int finalY = -firstView.getHeight();
double progress = 0;
while (progress < 1) {
progress = Math.min((System.currentTimeMillis() - startTime) / (double)duration, 1);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)firstView.getLayoutParams();
params.topMargin = (int)(progress * (double)finalY);
firstView.setLayoutParams(params);
firstView.postInvalidate();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}.start();

Android: ImageView, LinearLayout and TextView heights returned negative, 0 and 0, why?

I am trying to scale the image in my linear layout to fill the available space, but I don't understand the values I'm getting for the widths of the layout. Here's the relevant part of my main.xml layout file:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/LeftButtonsLayout"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="10"
android:orientation="vertical" >
<TextView
android:id="#+id/Jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/jump"
android:textStyle="bold"
android:padding="5dip"
/>
<ImageView
android:id="#+id/JumpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="#drawable/jump"
android:contentDescription="#string/jump"
android:padding="5dip"
/>
<LinearLayout
Here's the onCreate() method of my activity, which has a debug print:
public void onCreate (Bundle savedInstanceState) {
setContentView(R.layout.main);
LinearLayout leftButtonsLayout = (LinearLayout) findViewById(R.id.LeftButtonsLayout);
imageView = (ImageView) findViewById(R.id.ResetButton);
Log.d("DEBUG", CLASS_NAME + "scaleLeftButtonsLayoutContents: \n" +
"linear layout height: " + leftButtonsLayout.getHeight() + "\n" +
"text height: " + ((TextView)findViewById(R.id.Jump)).getHeight() + "\n" +
"image height: " + imageView.getLayoutParams().height);
}
1) If I place the setContentVew() call after the Log.d() debug print, I get a Null Pointer Exception. Why? Is memory not allocated for the LinearLayout before it's used on the view?
2) The prints I see are:
linear layout height: 0
text height: 0
image height: -2
What am I doing wrong here? I expected to see sane values here, since I can see the imageView on the device screen.
3) I was planning to scale the image using:
imageView.getLayoutParams().height = newHeight. Is that right to do? Will doing this automatically update the imageView on the screen, or will I have to do a setContentView() again?
Thanks in advance for your help.
UPDATE
Thanks for your answers everyone. I've overridden the onWindowFocusChanged() method of my activity, but when I check the size of the nested ImageView below, it's reported as -2. Resizing it works, but I'm curious why it's -2 when it should've had a sane value. My code's as follows:
#Override
public void onWindowFocusChanged (boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus)
scaleLeftButtonsLayoutContents();
}
private void scaleLeftButtonsLayoutContents () {
LinearLayout leftButtonsLayout = (LinearLayout)findViewById(R.id.LeftButtonsLayout);
imageView = (ImageView) findViewById(R.id.JumpButton);
Log.d("TAG", CLASS_NAME + "JumpButton.height " + imageView.getLayoutParams().height);
imageView.getLayoutParams().height = verticalSpaceAvailable;
imageView.getLayoutParams().width = verticalSpaceAvailable;
leftButtonsLayout.requestLayout();
}
This produces the print:
JumpButton.height -2
The resize produces a sane image, but why is the initial height -2?
To answer your points,
1) It is because you haven't initailaized your Button or ImageView. Since you call your Log before doing this, obviously the Button and ImageView are null and hence you get the exception.
2)And initializing doesn't mean that your view are completely drawn to provide you with width and height. So you have to provide the time to get itself drawn. But unfortuanately we don't know the exact time it takes to get drawn. So Android provides this method,
#Override
public void onWindowFocusChanged(boolean hasFocus)
{
// which gets called when your view is drawn.
}
Just now answered a similar question here.
So what you have to do is, add your Log inside this method in your Activity and then check the resulting width and height.
3) To answer your third question, you definitely should not call setContentView() once again, which might throw you some other exception. But when considering scaling you might make use of some bitmaps to do this.
Here are some answers for you:
1) If you place the setContentView after calling view.getHeight() you will get null pointer because that view is not set on the Activity content therefore you can't get a reference to it before setting it to the content of the Activity
2) You see that because the view doesn't had time to layout.. if you want to see the height/width of a view it's better to use a ViewTreeObserver listener like this:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
/* don't forget to remove the listener after you use it once */
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
Log.d("MY VIEW WIDTH","width:"+view.getWidth());
}
});
3) After you set the layout params of a view don't forget to call view.requestLayout() to make sure that your view will refresh. You don't have to call setContentView() again.
EDIT: Also the width of your LinearLayout has to be at least wrap_content if not fill_parent or a value greater than 0 if you want to see the child views of the Linear Layout..
You cannot do like this. Because the linearlayout is the main container of your activity. You cannot provide android:layout_weight="10" and android:layout_width="0dip" to the main layout. create one Linear layout outside this android:id="#+id/LeftButtonsLayout" and give the layout height and width to fill_parent or match_parent. This will work in your case.
And one more thing, You cannot allow to call elements of layout before the setContentView.

ListView of WebViews - reusing items of different height fails; asks for all views before displaying any

I have an activity that displays a ListView. Each item in the ListView is a LinearLayout consisting of one WebView. There are potentially hundreds of items in the list and each is a different height.
First problem is that when reusing a recycled view in getView(), the new view is always the height of the original view, even though I've set the layout_height for both the LinearLayout and the WebView to wrap_content.
Second problem is that getView() seems to be getting called for every item in the list even though only the first five or six fit on the screen. I haven't seen this when using other list item types. For example, in another place I a list of custom views that are all the same height and I only see getView() being called for the number of views that initially fit on the screen.
So... I need to figure out how to force recycled WebViews to render their new contents so their height can be calculated instead of just using the previous height. And I'd like to know why the system is asking me for ALL my items in this case.
Here's the requisite code snippets:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ListView
android:id="#+id/topPane"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:dividerHeight="1.0px"
android:divider="#FFFFFF"
android:smoothScrollbar="false"
/>
</LinearLayout>
Rows are built from:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<WebView
android:id="#+id/rowWebView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="0.0px"
android:scrollbars="none"
/>
</LinearLayout>
This is getView() in my adapter. HTML snippets come from an array of Strings for now.
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
String item = (String) getItem(position);
if (convertView == null)
{
convertView = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.rowview, parent, false);
}
WebView wv = (WebView) convertView.findViewById(R.id.rowWebView);
wv.loadDataWithBaseURL(null, item, "text/html", "utf-8", "about:blank");
return convertView;
}
I think the problem is that with the code you wrote the system is measuring first the container height (row linear_layout), and later the content (WebView). But second measure doesn't affect the first one until you call invalidate or some method that makes the container to recalculate his size.
About why your getView method is called many times check your getViewTypeCount() method of your adapter.
#Override
public int getViewTypeCount()
{
return NumberOfTypeOfViewsInYourList;
}
Documentation states that:
"this method returns the number of
types of Views that will be created by
getView(int, View, ViewGroup). Each
type represents a set of views that
can be converted in getView(int, View,
ViewGroup). If the adapter always
returns the same type of View for all
items, this method should return 1.
This method will only be called when
when the adapter is set on the the
AdapterView."
I had a problem with this method, giving a big number (like 5000) made my app crash, it looks like it's used internally for some ListView calculations.
Hope it helps somehow.
BTW, there is a very good talk about the world of ListViews
I had the same problem with my ExpandableListView. From other posts it seems to be an ongoing issue with multiple webviews in one list. The work around i found was to create and array of wewViews the first time getView is called, then subsequently return the webView at the correct index. Its a bit more memory intensive but it does'nt cause the view to resize.
SparseArray<WebView> wvGroup = new SparseArray<WebView>();
#Override
public View getView(int position, View convertView, ViewGroup parent) {
WebView wv = null;
if (wvGroup.size() < 1) {
for (int i = 0; i < sectionItems.size(); i++) {
WebView webViewItem = new WebView(context);
String htmlData = "<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" + sectionItems.get(i).child_content;
webViewItem.loadDataWithBaseURL("file:///android_assets/", htmlData, "text/html", "UTF-8", null);
wvGroup.append(i, webViewItem);
}
}
wv = wvGroup.get(groupPosition);
wv.setPadding(30, 10, 10, 10);
return wv
I've tried the solution of sirFunkenstine which works great. Only the client, for which I'm building this app, didn't liked the performance. And I had to admit that it was not that smooth.
It seemed that adding and removing the preloaded WebViews where the problem. The UI got frozen for a split second every time the app added/removed the new WebView for the next row.
Solution
So I thought it would be better to only adjust the content and height of the WebView instead of adding/removing. This way we only have the amount of WebViews which are currently visible in memory. And we can use the reuse functionality of the ListView/Adapter.
So I've added a single WebView behind the ListView which constantly calculates the WebView height of the next items. You can get the height with the WebView method getContentHeight. You'll have to do this in the onPageFinished method so you have the final height. This raised another issue, cause the method returns zero when the content is loaded with loadDataWithBaseURL or loadData methods. It seems that the value is set eventually, but not yet at the time that the onPageFinished is called. To overcome this you can add a thread which constantly checks if the getContentHeight is not zero anymore. If it is not zero then the value is set and you can load the next one.
This whole solution is a bit hacky, but it gave me a nice and smooth ListView including WebViews with different heights.
Some example code:
1: Queue the positions of the rows which you want to preload:
private SparseArray<Integer> mWebViewHeights = new SparseArray<Integer>();
private LinkedBlockingQueue mQueue;
{
mQueue = new LinkedBlockingQueue();
try {
mQueue.put(position);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2: Start the Runnable to constantly load new items from the queue and check/add the height to an array:
Handler h;
Runnable rowHeightCalculator = new Runnable() {
h = new Handler();
rowHeightCalculator.run();
}
3: Load new HTML content and check the heights:
Handler h;
Runnable rowHeightCalculator = new Runnable() {
int mCurrentIndex = -1;
boolean mLoading = false;
// Read the webview height this way, cause oncomplete only returns 0 for local HTML data
#Override
public void run() {
if(Thread.interrupted())
return;
if (mCurrentIndex == -1 && mQueue.size() > 0) {
try {
mCurrentIndex = (Integer)mQueue.take();
String html = mItems.get(mCurrentIndex).getPart().getText();
mDummyWebView.clearView();
mDummyWebView.loadDataWithBaseURL("file:///android_asset/reader/", "THE HTML HERE", "text/html", "UTF-8", null);
mLoading = true;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(mDummyWebView.getContentHeight() != 0 && mCurrentIndex >= 0) {
int contentHeight = mDummyWebView.getContentHeight();
mWebViewHeights.append(mCurrentIndex, contentHeight);
mCurrentIndex = -1;
mLoading = false;
}
// Reload view if we loaded 20 items
if ((mQueue.size() == 0 && (mItemsCount - mQueue.size()) < 20) || (mItemsCount - mQueue.size()) == 20) {
notifyDataSetChanged();
}
if (mQueue.size() > 0 || mLoading) {
h.postDelayed(this, 1);
}
}
};
My solution (even though I put the bounty up) for my problem was to put the ImageView into a FrameLayout, it than intelligently grew.
Invalidating the view or forcing layout calculations did nothing for me, but the framelayout container for the imageview seemed to solve everything. I assume this behaviour is in err, but the workaround was simple enough.
Maybe the same will apply to the OP's webview.

Categories

Resources