My app has a screen with an expandablelistview. Within the expandablelistview are collapsible rows that contain listviews. These listviews contain a dynamic number of items and with dynamic heights on each item. As listviews are meant to be set to a hard-coded height and then to have scrolling within them, and I don't want to have scrolling within scrolling, I've had to get around that by calculating the actual height of the listview's items then set the height of the listview to that height so that all of its contents will fit within the exandablelistview row.
Here is my extension method that calculates the listview's height:
public static void SetHeightBasedOnChildren(this ListView listView) {
var listAdapter = listView.Adapter;
var totalHeight = listView.PaddingTop + listView.PaddingBottom;
if (listView.DividerHeight > 0)
totalHeight += (listView.DividerHeight * (listAdapter.Count - 1));
var desiredWidth = DeviceHelper.GetDimensions ().X;// Add parameter if listview doesn't always span entire width of screen
var listViewWidth = MeasureSpec.MakeMeasureSpec (desiredWidth, MeasureSpecMode.AtMost);
View listItem;
for (int i = 0; i < listAdapter.Count; i++) {
listItem = listAdapter.GetView (i, null, listView);
if (listItem is ViewGroup && listItem.LayoutParameters == null)
listItem.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
listItem.Measure (listViewWidth, (int)MeasureSpecMode.Unspecified);
totalHeight += listItem.MeasuredHeight;
}
var layoutParams = listView.LayoutParameters;
layoutParams.Height = totalHeight;
listView.LayoutParameters = layoutParams;
}
The issue with this is that when scrolling down the screen and reaching each listview, as it calculates the dynamic height of the listview's items and sets its height to that, it causes the scrolling to get really choppy.
As I mentioned that listviews are meant to be a hard-coded height, I tried swapping out our listviews for linearlayouts with vertical orientation, which are meant to have an adjustable height that fit their content so don't need any fancy height calculations on the fly. Unfortunately this approach didn't work, as linearlayouts don't cache their rows like listviews do, so the screen was completely unusable from trying to manage hundreds -> thousands of rows without any sort of caching. Note: the linearlayouts performed fine on the emulator but not on devices.
I'm wondering if anyone has ideas of how to get smooth scrolling in this situation. Some ideas I had...
Keep using listviews but set a hard-coded height on each item in the listview. Then I can calculate the height of the listview much faster, as I know the height based on the total number of rows. I would still have to set the height on each listview though, which makes it still not smooth but better than before. The downside is that I'd either need to make all rows tall enough to fit longer content (waste of vertical space), or truncate content that doesn't fit.
Look into if anyone has developed a hybrid widget, that is a linearlayout with vertical orientation that utilizes row caching.
Figure out how to force the expandablelistview to load the listview sections immediately on screen load rather than waiting until they're just about to come into view. The downside is we're holding a lot more in memory so this might render the screen unusable.
Thoughts??
EDIT: I should probably mention that the listviews are also within viewpagers, as the user can swipe between lists from different years (think tax records or mortgage records for a property). I bring this up so someone won't say, just don't have listviews at all, just only use the expandablelistview.
Related
I have to make two RecyclerViews, that can dynamically adjust its heights to each other. Assume i have a screen = 1/3 of its height is first RV and next 2/3 of its height us second RV. This is normal state for two populated lists. But if first RV doesn't have enough elements to fill 1/3 of screen - second RV becomes higher and if seconds RV doesn't have enough elements to fill 2/3 of screen height - first list become taller.
Is this theoretically possible? If yes, can you give me some clues to start digging?
Try this.
private int itemHeight = "height of item layout which u are inflating in the recycler view"
ViewGroup.LayoutParams params = recyclerView.getLayoutParams();
params.height = itemHeight * numberOfItemInList;
recyclerView.requestLayout();
I have to display posts from a json feed inside a RecyclerView and I have a layout for how a single row looks inside the RecyclerView as follows
I have not yet implemented the bottom part in my actual layout which contains the red and green boxes from the figure above and my layout looks like this
I am also implementing Swipe to delete with undo which as you know requires a FrameLayout as the root , and 2 nodes under it, one showing the normal area and one showing the layout which is revealed on swipe. In my case, when I swipe the item, this is what you will see.
Now the problem is, there are 13 views per row and I don't like the odds of that, I will be displaying a maximum of 100 items in the RecyclerView at a given time and as you see it would lead to a large number of Views.
I have certain approaches in mind to make a custom View to reduce the number of Views in the row. What would be the best way according to you to optimise this View or should I say, reduce the number of Views per row in the RecyclerView. I get all the data from JSON and the central text area needs to be expandable in nature with a Read More or Read Less depending on its state.
Approach 1
Slight simplification
In this approach, I will combine the person's profile picture at the top left, the TextView with the name and updated time into a single Custom View, in other words, it will extend from View, have its own canvas for drawing the Bitmap, the Strings but I'll have to manually code RTL and LTR and other items using a StaticLayout in Android. I need the central text area to be expandable in nature so I will stick with one of the Expandable Views everyone keeps mentioning on stackoverflow.
Approach 2
Highly modular Custom UI component.
The entire diagram composed of the user's image, text with name, time, central text and image can be made into a single CustomView, I am not sure how I can make this expandable yet because a StaticLayout once initialised in Android cannot be modified. What do you think? Is this the correct approach to go? I will end up having only 4 children per row if I make the entire thing a single View. Is that a valid use case for custom Views?
Well, I found the answer myself. There are two types of post items I am dealing with, ones that contain an ImageView to display a 16:9 post image and the ones that don't. My RecyclerView was lagging heavily in the scroll operation till now since I had a single layout file which had an ImageView with its width set to match_parent and height set to wrap_content. When I was scrolling, the posts with Images were using Glide to load images and were calling requestLayout() since the size of those items were changing. This was resulting in too many requestLayout() calls causing the heavy lag while scrolling.
How did I fix this?
I made an Adapter which was sectioned having 2 types of rows.
#Override
public int getItemViewType(int position) {
if (mResults != null) {
return mResults.get(position).getPicture() != null ? IMAGE : NO_IMAGE;
}
return NO_IMAGE;
}
Based on whether the result at the current position contains an Image in the post or not, I had my onCreateViewHolder method modified to set a predefined size for the ImageView incase my item had a post image.
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == IMAGE) {
view = mLayoutInflater.inflate(R.layout.row_post_image, parent, false);
RowImageHolder holder = new RowImageHolder(view);
//To set the width and height of the ImageView of our image in the post, we get its LayoutParams and adjust its width and height to maintain 16:9 ratio
ViewGroup.LayoutParams params = holder.mPostPicture.getLayoutParams();
params.width = mPostImageWidth;
params.height = mPostImageHeight;
holder.mPostPicture.setLayoutParams(params);
return holder;
} else {
view = mLayoutInflater.inflate(R.layout.row_post_image, parent, false);
RowNoImageHolder holder = new RowNoImageHolder(view);
return holder;
}
}
And thats all which needs to be done. The onBindViewHolder sets an image if the type of the item supports an image. The benefit is that space is reserved in advance for those items that have images and the performance of the RecyclerView on scroll improves tremendously. I achieved this without using any custom views so far :)
I'm simulating a tile-based map in Android using android-support-v7-gridlayout.
I implement it smoothly and fast during the debugging times until I test it on a huge data scale. The actual data would be about ~700x400 (row-column) and I just tested it in 400x100 but the application has just crashed and throws an OutOfMemoryException. I then reduced the data until it actually runs on 300x100. It's not lagging or I don't have any CPU performance issue, the only issue is inadequate memory.
This is how I add ImageViews in the grid layout:
public boolean genMap(GridLayout gl) {
gl.removeAllViews();
gl.setRowCount( mapFile.getRowCount() );
gl.setColumnCount( mapFile.getColCount() );
try {
for (int row = 0; row < mapFile.getRowCount(); ++row) {
for (int col = 0; col < mapFile.getColCount(); ++col) {
com.gridlayout.GridLayout.Spec rowspan = GridLayout.spec(row, 1);
com.gridlayout.GridLayout.Spec colspan = GridLayout.spec(col, 1);
GridLayout.LayoutParams lp = new GridLayout.LayoutParams(rowspan, colspan);
lp.width = Cell.SIZE;
lp.height = Cell.SIZE;
Cell cell = genNewCell( ctx.getApplicationContext(), row, col );
gl.addView( cell, lp );
}
}
return true;
} catch (Exception e) {
return false;
}
}
Where Cell is a subclass of ImageView
Perhaps, I also think of lazyload pattern where the only visible view will be loaded out but, I'd like to know if GridLayout already implements it.
Update 1
GridView seems is not what I'm looking for. It cannot scroll diagonally plus it can't have a scrollbar both horizontal and vertical at the same time. What I want to achieve is something like GoogleMaps' ViewGroup layout wherein you can scroll in a 2 dimensional way and each cells memory allocation/deallocation are managed automatically. I think GridLayout is my last hope as I cannot see any ViewGroup w/c implements what I wanted to.
So my question is, how can I able to recycle those cells that aren't visible yet in the screen while keeping layout as they are present?
GridLayout does not do any dynamic memory management and simply creates everything regardless if it is actually on the screen or not. You need to use something like the GridView. With a GridView, you could theoretically support an infinite amount of items. Now because you want to do horizontal scrolling you will need a custom GridView implementation. Something like https://github.com/jess-anders/two-way-gridview.
If you take away nothing from my post, DO NOT DO THIS WITH A GRID LAYOUT. You could test all day but every phones different and some have more or less memory.
EDIT 1:
This also might be easier with the release of the new RecyclerView in android L, but I haven’t look into it that much.
In my opinion you should use Webview for this. Has scroll, zoom, memory issues already addressed.
from an answer to one of my other questions I found an Google Demo of a ListView subclass that allows item reorder.
The demo works great, but I am having some trouble to understand how the it works:
When an item is dragged above/below the bounds of the ListView, the ListView starts scrolling up/down to reveal new items. The necessary calculation uses different parameters of the underling ScrollView:
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
heightis the height of the ListView itself
offsetis the scroll position = how many units/pixels have been scrolled up/down
rangeis the height of the complete content.
extent - well, what is this?
ListView inherits computeVerticalScrollExtent() from View and the docu says:
Compute the vertical offset of the vertical scrollbar's thumb within
the horizontal range. This value is used to compute the position of
the thumb within the scrollbar's track.
If one looks at the code computeVerticalScrollExtent() is not implemented by one of the sublasses but only directly by View: It simply returns the height of the view.
This makes sense: If the ListView/ScrollView has a height of 500, the part of the scroll content that is visible at a time is also 500. Is this the meaning of the ScrollExtent? Why is ScrollExtent necessary? Why not simply use getHeight() directly?
I think I am missing something and I am happy about any hint!
compute*ScrollOffset - Defines the distance between the start of the scrollable area and the top of the current view window inside the scrollable area. So for example, if your list has 10 items and you've scrolled down so the 3rd item is at the top-most visible item, then the offset is 3 (or 3*itemHeight, see below).
compute*ScrollExtent - Defines the size of the current view window inside the scrollable area. So for example, if your list has 10 items and you can currently see 5 of those items, then the extent is 5 (or 5*itemHeight, see below).
compute*ScrollRange - Defines the size of the current scrollable area. So for example, if your list has 10 items then the range is 10 (or 10*itemHeight, see below).
Note that all these methods can return values in different units depending on their implementation, so for the examples above, I am using the indices, but in some cases these methods will return pixel values equivalent to the width or height of the items.
In particular, the LinearLayoutManager of the RecyclerView will return indices if the 'smooth scrollbar' feature is disabled, otherwise it will return pixel values. See ScrollbarHelper in the support library for more information.
Additional reading: https://groups.google.com/forum/#!topic/android-developers/Hxe-bjtQVvk
It's kinda late, but hope it's ok.
1) The method actually is implemented by the subclasses. For example, this is what it looks like for AbsListView (ListView's superclass).
2) View's height can be different from its vertical scroll's height - just imagine a View with weird top/bottom padding .
These two points pretty much make other questions irrelevant :)
This is a sample code which might help you to understand as to how to get scrollBar top and bottom using computeVerticalScrollExtent:
scrollbarTop = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*this.computeVerticalScrollOffset();
scrollbarBottom = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*(this.computeVerticalScrollExtent()+this.computeVerticalScrollOffset());
According to the article from here:
I found this explanation correct.
ListView with 30 items has scrollRange equals to 3000, that is due to scrollRange = numberOfItems * 100, thus scrollExtent = numberOfVisibleItems * 100 and scrollOffset = numberOfScrolledItems * 100. You can find evidance of these words in the source code of AbsListView
I used Horizontal scroll view in which I add 'Child View'. But on HZ scroll view I want to arrange child some think like, whole screen will show 3.5 child at a time or 4.5 , 5.5 depends on screen size. The half of child indicate there is more child on Scroller.
For that I used different dimensions for child view depends on density. But still their is some device which show whole child at the end of screen.
So how I can mange this on scroll view. Please guide me in right direction.
For that I used different dimensions for child view depends on
density. But still their is some device which show whole child at the
end of screen.
That will not work well in a lot of devices.
Assuming that those orange child views are all of the same width(although not that important) and you want to show half of one only when the HorizontallScrollView is first laid out, you could simply post a Runnable(in the onCreate method) on a view to set their widths to a proper dimension so you make the proper appearance:
public void onCreate(Bundle saved) {
// ... work
horizontalScrollView.post(new Runnable() {
#Override
public void run() {
DisplayMetrics dm = getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;
// based on this width, the space between the children and their
// widths calculate new widths so you get a half clipped
// children on the right side.
}
});
}
But, I would avoid this and simply set a much more powerful indicator on the HorizontalScrollView(overriding its dispatchDraw method and drawing something there(maybe also hiding it a bit later with a Runnable)) if you really want to make sure that the user sees the extra content.