Home » Android » android – ListView of WebViews – reusing items of different height fails; asks for all views before displaying any

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

Posted by: admin June 15, 2020 Leave a comment

Questions:

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;
        }
How to&Answers:

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

Answer:

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

Answer:

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);
        }
    }
};

Answer:

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.