Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ImageView on orientation change - Drawable ignores setBounds and returns to original state

EDIT 3:

I am maintaining the original question below for historical reasons. However, I have found that the issue is not isolated to FrameLayout. Hence, updated the title.

Instead of over-crowding this post with more code, I have created a sample project which demonstrates the issue; and uploaded it on Google Project Hosting.

The summary of the issue is this:

A drawable in portrait orientation with a certain bounds set on it, on changing orientation and returning to portrait, does not retain the set bounds. Instead it reverts to its original bounds. This is in spite of forcefully setting the explicit bounds on orientation change. Do note that any bounds that you later set on Click etc are obeyed.

I have also uploaded a version of the app that contains 2 separate activities illustrating the issue.

  1. Plain vanilla activity that just illustrates the issue.
  2. Activity that uses a custom BitmapDrawable on the ImageView - this one prints to log whenever bounds are being changed.

The second version show clearly that even if I set the bounds on the Drawable, these bounds are not being obeyed in onBoundsChange().


Original Question:

I am using a FrameLayout with 2 ImageViews stacked one on top of the other for displaying a "Battery Status" graphic. This is in portrait mode. In Landscape mode, I display a different layout (a chart).

My issue is this - suppose the battery status is displaying - say 30%. Now, I rotate the screen and display the chart. When I come back to the Portrait orientation, the battery graphic goes back to its original position (which is "full").

I have tried all sorts of things to try and figure out what is happening. Debugging shows me that the bounds for the "top" graphic are indeed being set as expected. So this seems to be an invalidation issue. I am presenting the code for 2 classes and 2 layout XMLs (all simplified) which helps in reproducing the issue. Also attaching the placeholder PNGs used for the ImageViews.

Placeholder Battery Frame graphicPlaceholder Battery Content graphic

Can anyone spot the error? To recreate the issue, run the app, and click in "Update" button. The graphic will be "filled" to a certain level. Then, switch to landscape and then back to Portrait. The graphic does not remember its earlier value.

The Activity:

public class RotateActivity extends Activity {

    private View portraitView, landscapeView;
    private LayoutInflater li;
    private Configuration mConfig;
    private ValueIndicator indicator;
    private Button btn;
    private Random random = new java.util.Random();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        li = LayoutInflater.from(this);
        portraitView = li.inflate(R.layout.portrait, null);
        landscapeView = li.inflate(R.layout.landscape, null);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mConfig = newConfig;
        initialize();

    }

    @Override
    protected void onResume() {
        mConfig = getResources().getConfiguration();
        initialize();
        super.onResume();
    }

    private void initialize(){
        if(mConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){
            displayLandscape();
        } else {
            displayPortrait();
        }
    }

    private void displayLandscape() {
        setContentView(landscapeView);
    }

    private void displayPortrait() {
        setContentView(portraitView);
        btn = (Button)portraitView.findViewById(R.id.button1);
        indicator = (ValueIndicator)portraitView.findViewById(R.id.valueIndicator1);
    /*
     * Forcing the graphic to perform redraw to its known state when we return to portrait view.
     */
    indicator.updateIndicatorUi();  



        btn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                updateIndicator(random.nextInt(100));
            }
        });

    }

    private void updateIndicator(int newValue){
        indicator.setPercent(newValue);
    }
}

portrait.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <com.gs.kiran.trial.inval.ValueIndicator
                android:id="@+id/valueIndicator1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="10dp" />

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Update" />

</LinearLayout>

ValueIndicator.java

public class ValueIndicator extends FrameLayout {

    private ImageView ivFrame, ivContent;
    private Drawable drFrame, drContent;
    private Rect mBounds;
    private int currentTop;
    private int mPercent;

    public ValueIndicator(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View root = li.inflate(R.layout.indicator, this, true);

        ivFrame = (ImageView) root.findViewById(R.id.ivFrame);
        ivContent = (ImageView) root.findViewById(R.id.ivContent);

        drFrame = ivFrame.getDrawable();
        drContent = ivContent.getDrawable();

        mBounds = drFrame.getBounds();

        Log.d(Constants.LOG_TAG, "Constructor of ValueIndicator");

    }

    public void setPercent(int newPercent){
       this.mPercent = newPercent;
       updateIndicatorUi();
    }

    public void updateIndicatorUi(){
        Rect newBounds = new Rect(mBounds);
        newBounds.top = mBounds.bottom - (int)(this.mPercent * mBounds.height() / 100);
        currentTop = newBounds.top;
        Log.d(Constants.LOG_TAG, "currentTop = "+currentTop);
        drContent.setBounds(newBounds);
        //invalidateDrawable(drContent);
        invalidate();
    }
}

indicator.xml (The XML for the FrameLayout used in the custom View)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frameLayout1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >

    <ImageView
        android:id="@+id/ivFrame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/frame" />

    <ImageView
        android:id="@+id/ivContent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/content" />

</FrameLayout>

landscape.xml (dummy placeholder - sufficient to recreate the issue)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Landscape" />

</LinearLayout>

AndroidManifest.xml snippet:

<activity
            android:label="@string/app_name"
            android:name=".RotateActivity" 
            android:configChanges="orientation|keyboardHidden">
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

EDIT

I also tried calling setPercent() and pass in a saved value in displayPortraitView() - basically forcing an update to a known state whenever we get back to portrait mode. Still no luck. Do note that the logs tell me that the bounds of the drawables are correct. I can't figure out why the invalidation doesn't happen.


EDIT 2:

  1. In ValueIndicator.java, I introduced a member variable mPercent which always stores the last known percent value.
  2. Updated the setPercent() code to update the member variable mPercent; and then call the updateIndicateUi() method.
  3. updateIndicatorUi() (which is now a public method) now uses the state (i.e, mPercent) to do its job.
  4. Whenever we are back to portrait mode, I call updateIndicatorUi(). This forces the battery graphic to update itself.

I also updated the code to reflect these changes.

The idea is to force a redraw and an invalidate whenever we return from landscape to portrait mode. Again - I do see the bounds of the battery "content" drawable being set as desired, but the UI refuses to keep pace.

like image 414
curioustechizen Avatar asked Nov 05 '22 06:11

curioustechizen


1 Answers

I've examined your code on Google Code Hosting (appreciate your effort to document the code so thoroughfully), and I found that the bounds set on the Drawable is indeed changed again when you go back to the portrait from landscape orientation.

The bounds on the Drawable is changed not by your code, but by ImageView's layout method. When you place a new layout (setContentView), all layoutting code is run, including ImageView's. ImageView changes the bound of the drawable that it contains, that's why you get the bounds of the drawable changed to the original one.

The stack trace leading to the bound change is:

Thread [<1> main] (Suspended (entry into method onBoundsChange in BitmapDrawable))  
    BitmapDrawable.onBoundsChange(Rect) line: 293   
    BitmapDrawable(Drawable).setBounds(int, int, int, int) line: 131    
    ImageView.configureBounds() line: 769   
    ImageView.setFrame(int, int, int, int) line: 742    
    ImageView(View).layout(int, int, int, int) line: 7186   
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7192    
    FrameLayout.onLayout(boolean, int, int, int, int) line: 338 
    FrameLayout(View).layout(int, int, int, int) line: 7192 
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7192    
    PhoneWindow$DecorView(FrameLayout).onLayout(boolean, int, int, int, int) line: 338  
    PhoneWindow$DecorView(View).layout(int, int, int, int) line: 7192   
    ViewRoot.performTraversals() line: 1145 
    ViewRoot.handleMessage(Message) line: 1865  
    ViewRoot(Handler).dispatchMessage(Message) line: 99 
    Looper.loop() line: 130 
    ActivityThread.main(String[]) line: 3835    
    Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
    Method.invoke(Object, Object...) line: 507  
    ZygoteInit$MethodAndArgsCaller.run() line: 847  
    ZygoteInit.main(String[]) line: 605 
    NativeStart.main(String[]) line: not available [native method]  

When reading your code I found it overkill to change bounds and storing the bounds etc. just to draw a meter. May I suggest one of the following:

  1. Change the size of the ImageView itself (using setLayoutParams) instead of the bounds of its drawable.
  2. Instead of using ImageView, create a class extending View and override onDraw(Canvas) and then draw the red rectangle using drawRect.
like image 96
Randy Sugianto 'Yuku' Avatar answered Nov 09 '22 06:11

Randy Sugianto 'Yuku'