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.
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 ImageView
s 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.
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:
mPercent
which always stores the last known percent value.setPercent()
code to update the member variable mPercent
; and then call the updateIndicateUi()
method.updateIndicatorUi()
(which is now a public
method) now uses the state (i.e, mPercent
) to do its job.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.
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:
ImageView
itself (using setLayoutParams
) instead of the bounds of its drawable.View
and override onDraw(Canvas)
and then draw the red rectangle using drawRect
. If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With