2

I have a custom Layout (extends FrameLayout) with a few items in it. Each item is a custom View that can load the corresponding object (Let's say a user's profile, display picture, etc.). These views are contained inside a few LinearLayouts (rows) so that I can line them up properly in the rows without problems. The rows are fixed (e.g. Items 0, 1, & 2 are always in the first row and always present, 3 & 4 are in the 2nd and so on). I use a FrameLayout because these rows need to overlap a bit ... think interlaced hexagons.

When the layout is loaded, all the rows are laid out on top of each other since I don't yet know the sizes.

protected void onMeasure (final int widthMeasureSpec, final int heightMeasureSpec)

Inside the onMeasure method, I use the provided width & height MeasureSpecs to calculate how much room I have. Then based on that, I calculate the maximum width & height each item can have and store these in 3 variables (width, height, and radius). I also calculate given the padding and spacing between the items, what the width and the height of the layout itself needs to be. Now I take these values, and create new MeasureSpecs with the mode set to MeasureSpec.EXACTLY and pass them to the super layout.

super.onMeasure (MeasureSpec.makeMeasureSpec (width, MeasureSpec.EXACTLY),
                     MeasureSpec.makeMeasureSpec (height, MeasureSpec.EXACTLY));

Note: Initially I used setMeasuredDimension (width, height) to force the size of the layout, but it suffered from the issue that I'm about to describe. Changing to the MeasureSpec.EXACTLY method fixed it.

protected void onLayout (final boolean changed, final int l, final int t, final int r, final int b)

Inside the OnLayout method, I use the values that I calculated in onMeasure to layout the items. If changed is false, I bail because nothing has changed. Otherwise I loop through the rows and set their top margins:

final int rowTop = rowTop(i); // i = index of row
final LayoutParams layoutParams = (LayoutParams) row.getLayoutParams ();
layoutParams.topMargin = rowTop;
layoutParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
row.setLayoutParams (layoutParams);

This allows the rows to overlap a bit (the amount is calculated based on the sizes, padding, etc). Once I've set all the rows, I loop through the items and set their height and width (as calculated in onMeasure):

final ViewGroup.LayoutParams layoutParams = child.getLayoutParams ();
layoutParams.width = cellWidth;
layoutParams.height = cellHeight;
child.setLayoutParams (layoutParams);

This works just fine on many many devices. When I rotate the device, it recalculates all these values (prints them to log so I can confirm) and redraws everything just fine. This includes Nexus 4, Cyanogenmod-7, Cyanogenmod-11, LG G3 Stock ROM, and a Galaxy S5. I can load other Fragments and return to the Fragment that contains this layout and everything works as expected.

However things don't go as smoothly on an S2, S3, or an S4 for some reason. Unfortunately I can't include screenshots (Company IP), but I'm hoping that describing the symptoms may help.

Below, where I say I've verified this I mean that I used paper and pencil and drew the items to scale and it looked right.

Rotation On the problematic devices when I rotate, the onMeasure values are calculated correctly. I've confirmed this via the log, paper, and a pencil. The onLayout values are also calculated correctly (verified). What I see drawn on the screen, though, is not what the calculated values tell me. The values seem to lag one rotation behind. When the device goes landscape, I see the same drawing as portrait (this should not happen unless the device is a square). When I rotate back to portrait, I now see the items drawn with landscape sizes.

Reload When I load another fragment (think navigation drawer) and reload the fragment with this custom layout, the rows never expand to multiple rows (they stay fully overlapped as they are initially).

I have log output at the beginning on onConfigurationChanged, onMeasure, onSizeChanged, and onLayout, and they all print when they should and print out the values that I expect to be calculated:

08-28 18:14:41.498  19593-19593/<REDACTED>: config
08-28 18:14:41.518  19593-19593/<REDACTED>: measure
08-28 18:14:41.518  19593-19593/<REDACTED>: Forcing size to 1903x2374
08-28 18:14:41.518  19593-19593/<REDACTED>: measure
08-28 18:14:41.518  19593-19593/<REDACTED>: Forcing size to 1903x2374
08-28 18:14:41.538  19593-19593/<REDACTED>: size
08-28 18:14:41.538  19593-19593/<REDACTED>: layout
08-28 18:14:41.548  19593-19593/<REDACTED>: Top of row 0: 0
08-28 18:14:41.548  19593-19593/<REDACTED>: Top of row 1: 524
08-28 18:14:41.548  19593-19593/<REDACTED>: Top of row 2: 1062
08-28 18:14:41.548  19593-19593/<REDACTED>: Top of row 3: 1601

Has anyone ran into drawing issues on these devices? Does my drawing problem remind you of something you've had to deal with? If so, how did you get around it?

copolii
  • 14,208
  • 10
  • 51
  • 80
  • You say that you verified that your values calculated in the measure+layout pass are correct. *Therefore* the problem is in the drawing. Tell us a bit about how you do that – Gil Vegliach Sep 05 '14 at 15:27
  • I don't do the drawing. I just say where to draw. In the `onLayout`, I set the positions and dimensions of all the elements. This works on all my test devices, except for the 3 mentioned. I have an S3 running Cyanogenmod that doesn't behave this way. – copolii Sep 05 '14 at 18:25
  • Yeah sorry, I was a bit lazy and from your wording it looked like a drawing problem. Anyway, there is a bad smell in your layout pass. Take a look at my answer – Gil Vegliach Sep 05 '14 at 20:10

1 Answers1

0
  • You should use View.layout() on each children during onLayout()

During the layout you really want to position your children, so you calculate their relative coordinates and call layout() on them, so they can proceed to set their respective mLeft, mRight, etc. and have a position on screen. Their heights and widths have just been calculated in the measure pass.

Setting the LayoutParams during the layout pass looks a bit like a hack and is a bad smell. You are at a lower level here, you shouldn't do that.

Your case: Take for example when you set the margins on a row. My idea is that on 'non-problematic' devices the layout pass is run twice. There you probably call super.onLayout(). The first time the children are not positioned correctly but the (ignored) margins are calculated correctly. You don't see this because before drawing the layout pass is triggered again (is it inside a RelativeLayout?) and therefore FrameLayout.onLayout() sets the right child positions based on the margins calculated on the first layout pass. On some devices the layout pass runs only once and everything breaks.

If you want to implement the layout pass for a ViewGroup, you probably don't want to call super.onLayout(). For implementation examples, refer to the code of common layouts, like FrameLayout. You can read about the measure and layout passes in the View docs.

Gil Vegliach
  • 3,542
  • 2
  • 25
  • 37
  • Looks like you may have nailed it ... I'll have to test on the 3 devices when I'm back in the office (WFH today). I've re-implemented the `onLayout` method to 1. not call the super & 2. calculate the cell bounds and pass them to the cell via view.layout (l,t,r,b). – copolii Sep 06 '14 at 02:19