Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android view object reuse -- prevent old size from showing up when View reappears

EDIT: One more piece of possibly relevant info: The use case in which I see the problem is tab switching. That is, I create view X on tab A, remove it when leaving tab A, then recycle it into tab B. That's when the problem happens. This is also exactly when I need the performance gain . . .

I am working on the performance of my Android app. I've noticed that I can speed things up by reusing View objects of a class we'll call MyLayout. (It's actually a custom FrameLayout subclass, but that likely doesn't matter. Also, this is NOT ListView related.) That is, when I'm done with a View, rather than letting GC get ahold of it, I put it into a pool. When the same activity wants another MyLayout object, I grab one from the pool, if available. This does indeed speed up the app. But I'm having a hard time clearing the old size information. The result is that when I grab the View back, things are usually fine, but in some cases, the new View briefly appears before it is laid out with the new size information. This happens even though I set new LayoutParams shortly before or after adding the View back into the hierarchy (I've tried both ways; neither helps). So the user sees a brief (maybe 100ms) flash of the old size, before it goes to the correct size.

I'm wondering if/how I can work around this. Below, via C#/Xamarin, are some things I've tried, none of which help:

When recycling:

//Get myLayoutParams, then:
myLayoutParams.Width = 0;
myLayoutParams.Height = 0;
this.SetMeasuredDimension(0, 0);
this.RequestLayout();

Immediately before or after bringing back -- within the same event loop that adds the layout to its new parent:

// a model object has already computed the desired x, y, width, and height
// It's taken into account screen size and the like; the Model's sizes
// are definitely what I want.
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams (model.width, model.height);
layoutParams.LeftMargin = model.x;
layoutParams.TopMargin = model.y; 
this.LayoutParameters = layoutParams;

I've also tried bringing it back like the below, but the problem still remains:

FrameLayout.LayoutParams layoutParams = . . .  // the same LayoutParams as above
parent.AddView(viewThatIsBeingRecycled, layoutParams);

EDIT: Per request, some of the sequences I have tried. All suffer from the same problem. The basic issue is that even though the LayoutParams are correct, the layout itself is not correct as the actual layout has not happened yet.

Recycling time:

attempt A:

this.RemoveFromHierarchy();
// problem is that the width and height are retained

attempt B:

//Get myLayoutParams, then:
myLayoutParams.Width = 0;
myLayoutParams.Height = 0;
this.SetMeasuredDimension(0, 0);
this.RequestLayout();
this.RemoveFromHierarchy();
//problem is that even though layout has been requested, it does not actually happen.  
//Android seems to decide that since the view is no longer in the hierarchy,
//it doesn't need to do the actual layout.  So the width and height
//remain, just as they do in attempt A above.

When adding the view back:

All attempts call one of the following subroutine to sync the LayoutParams to the model:

public static void SyncExistingLayoutParamsToModel(FrameLayout.LayoutParams layoutParams, Model model) {
  layoutParams.TopMargin = model.X;
  layoutParams.LeftMargin = model.Y;
  layoutParams.Width = model.Width;
  layoutParams.Height = model.Height;
}

public static FrameLayout.LayoutParams CreateLayoutParamsFromModel(Model model) {
  FrameLayout.LayoutParams r = new FrameLayout.LayoutParams(model.Width, model.Height);
  r.LeftMargin = x;
  r.TopMargin = y;
  return r;
}

Attempt A:

newParent.AddView(viewThatIsBeingRecycled);
// get layoutParams of the view, then:
SyncExistingLayoutParamsToModel(myLayoutParams, model);

Attempt B: same as A, but in the opposite order:

// get layoutParams of the view, then:
SyncExistingLayoutParamsToModel(myLayoutParams, model);
newParent.AddView(viewThatIsBeingRecycled);

Attempt C: same as A, but with fresh layoutParams:

newParent.AddView(viewThatIsBeingRecycled);
FrameLayout.LayoutParams layoutParams = CreateLayoutParamsFromModel(model);
viewThatIsBeingRecycled.LayoutParams = layoutParams;

Attempt D: same as B, but with fresh layoutParams:

FrameLayout.LayoutParams layoutParams = CreateLayoutParamsFromModel(model);
viewThatIsBeingRecycled.LayoutParams = layoutParams;
newParent.AddView(viewThatIsBeingRecycled);

Attempt E: using the AddView that takes a layoutParams argument:

FrameLayout.LayoutParams layoutParams = CreateLayoutParamsFromModel(model);
newParent.AddView(viewThatIsBeingRecycled, layoutParams);

In all five cases, the problem is that even though the layoutParams are correct, the view is made visible to the user before the layout adjusts itself to the new layoutParams.

like image 767
William Jockusch Avatar asked May 30 '15 14:05

William Jockusch


4 Answers

Try executing these events (setLayoutParams and addView) in a message queue.

Solution 1 )

FrameLayout.LayoutParams layoutParams = CreateLayoutParamsFromModel(model);
viewThatIsBeingRecycled.setLayoutParams(layoutParams);
viewThatIsBeingRecycled.post(new Runnable() {
            @Override
            public void run() {
                newParent.AddView(viewThatIsBeingRecycled);
            }
        });

Solution 2 )

FrameLayout.LayoutParams layoutParams = CreateLayoutParamsFromModel(model);
viewThatIsBeingRecycled.setLayoutParams(layoutParams);
viewThatIsBeingRecycled.setVisibility(View.INVISIBLE);
newParent.AddView(viewThatIsBeingRecycled);
newParent.post(new Runnable() {
            @Override
            public void run() {
               viewThatIsBeingRecycled.setVisibility(View.VISIBLE); 
            }
        });

I am not sure if this works in your case. If setLayoutParams or addView is considered to be a message in internal OS implementation, then it places the next event in a queue so that it will be executed once previous event is executed.

like image 56
Abhishek V Avatar answered Nov 18 '22 11:11

Abhishek V


I hope it will help you:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
   @Override
   public void onGlobalLayout() {
       //set layout params here
   }
 });
like image 32
klimat Avatar answered Nov 18 '22 10:11

klimat


Apply Gravity with the View to which these layout parameters are associated

EDIT

Also in your attempt B, instead of this.requestLayout(); call that on the Parent View that will be getView() of the Fragment or the Activity content View... Telling the child to layout itself out makes the parent dirty,hence it will call requestLayout() for itself to layout out, that is what i think is the reason for the delay- since they run in a successive manner, but if you make it a direct call all it will be universal layout, which will eliminate the delay

Hope it helps

like image 2
Elltz Avatar answered Nov 18 '22 11:11

Elltz


I ran into the exact same problem except on native android, not xamarin.

Having my own test scene, made things a lot easier to debug the problem. I seem to have fixed it by setting right and left of the specific view to 0, just after removing it from its parent and before adding to another one:

((ViewGroup)view.getParent()).removeView(view);

view.setRight(0);
view.setLeft(0);

otherLayout.addView(view);
like image 2
Simas Avatar answered Nov 18 '22 11:11

Simas