1

I've search everywhere and can't find any way of doing this at all:

In my project, I have two resources folders for layouts: One is called "layout", and the other is called "layout-land". This is the familiar way of having two separate layouts for each Activity that get used depending on the current orientation. The issue is that I don't have separate "layout-land" layouts for every Activity, and not all Activities even need a different layout for landscape mode.

What I've done is overridden the default onConfigurationChanged to prevent orientation changes from happening (with the appropriate configChanges="orientation" in the manifest). This saved me a lot of headaches stuffing things into Bundles every time the user tilted their screen. However, in some cases, I actually want orientation changes to happen. Whenever there's a corresponding layout in the "layout-land" folder, I want the orientation to change as it normally would. Whenever there isn't, I want onConfigurationChanged to suppress an orientation change event.

I made a parent BaseActivity class that extends Activity and is the base class for all of my activities, and overrode onConfigurationChanged:

@Override
public void onConfigurationChanged(android.content.res.Configuration newConfig)
{
    super.onConfigurationChanged(newConfig);

    if(...there is a layout-land version of the current activity's layout...)
    {
        setRequestedOrientation(newConfig.orientation);
    }
    else
    {
        // Do nothing, since raising the orientation change event
        // to change from a portrait layout to the same portrait layout is silly.
    }
}

The only snag is that I can't find any way of determining if there's a layout-land version of my layout resource. I already know the resource id of the current layout (obtained elsewhere in my BaseActivity class by overriding setContentView), but both the landscape version of the layout and the portrait version share the same id, and I see no easy way for the Resources object to tell me which version of the layout it gives me when I ask it for a specific resource by id. There's got to be a way to do this. What am I missing?

Alex
  • 1,103
  • 1
  • 11
  • 24
  • Overriding `onConfigurationChanged()` just to avoid having to save state is just bad practice and is going to come back to bite you later. – Kevin Coppock Mar 06 '13 at 21:36
  • `onSaveInstanceState`'s entire existence is bad practice. The idea that you should be ready for your application to be disposed of entirely at any given time just because the user rotated the phone is poor design. Additionally, _how_ will avoiding saving state come back to bite me? Keep in mind the only reason I am doing this is to avoid situations where a dialog might be popping up at the exact time the user tilts their screen. User data is saved in my app whenever it is changed (including in `onSaveInstanceState`, if needed.) – Alex Mar 06 '13 at 21:43
  • Let's say your app is running and a user decides to go open a notification. Your app is now in the background. Depending on memory usage, your app may be stopped or destroyed (at the very least, it will be paused) before the user is done. When the user hits "back" to return to your activity, the activity is recreated, and all of your data contained in that activity instance is gone. – Kevin Coppock Mar 06 '13 at 22:14
  • 1
    I have personally tested this case. In the event that my app goes to the background, any dialogs I am showing are still visible when the user returns. There is always the possibility that the app remains in the background long enough to close entirely, but that's also fine. By that time, the user's data is saved. In fact, I save the data all the time, whenever it changes. Data loss is not an issue. Improper state of the app is the issue. If the user tilts the screen when a dialog is displaying, the Activity gets recreated but the dialog is gone. Now my app is in an illegal state. – Alex Mar 06 '13 at 22:18
  • 1
    The bottom line here is that I can always save and restore data. I'm okay with doing that and it's unavoidable. But if I have to rerun logic to determine which dialogs are displayed, restart network calls that were half-completed on other threads, etc., then I would much rather restart the whole Activity fresh (otherwise I would have to have an incredibly complex state machine that I save and restore each time). But, from a user experience point of view, tilting the screen isn't a big deal (like exiting the app altogether is), and shouldn't prompt a major Activity restart at all. – Alex Mar 06 '13 at 22:24
  • Well in your case it sounds like you're *already handling* data saving, so my personal opinion (without knowing much about your code, of course) is that it's not really necessary for you to override configChanges in the first place. If you just need to retain your dialogs, [this answer](http://stackoverflow.com/a/9153687/321697) may be of interest to you -- Android will manage the Dialogs for you if you build them in [`onCreateDialog`](http://developer.android.com/reference/android/app/Activity.html). – Kevin Coppock Mar 06 '13 at 23:21

2 Answers2

1

Alright, so I found a solution, albeit a miserable, hacked-together solution. I don't recommend this hack, because it is made of Satan:

The Resources object that you can obtain from an Activity's getResources() method will not by itself tell you if there's a "layout-land" version of a resource. However, you can obtain the AssetManager object that the Resources object uses internally by calling its getAssets() method.

The AssetManager object has a method called openNonAssetFd, which will attempt to grab a file descriptor for a particular file in your res folder. Unfortunately, most (if not all) of the things you attempt to open with this method will throw a FileNotFoundException, even if the file exists!

So, for example, if you have a file called res/layout-land/settings_menu.xml, then it will throw a FileNotFoundException with the message: "This file can not be opened as a file descriptor; it is probably compressed". This is poor design on the part of the Android folks, since technically, the file is there, and the exception thrown should at least be of a different type.

More importantly, however, for the purposes of detecting the mere existence of a layout-land layout, we can rely on the difference in the return messages on the exception thrown. Herein lies the hack:

If the file exists, the exception message will be "This file can not be opened as a file descriptor; it is probably compressed".

If the file doesn't exist, the exception message will be identical to the parameter you passed in to the openNonAssetFd method (i.e., it will be the filename of the nonexistent resource).

Now, with this, we can make a method like so:

protected boolean landscapeLayoutExists(int layoutId)
{
// Since we're always passing in an id of a layout, this will always correspond
// to a layout file in any of the layout folders (either layout, or both layout and layout-land, in my case).
String rawName = this.getResources().getResourceEntryName(layoutId);
String testPath = "res/layout-land/" + rawName + ".xml";

try
{
    this.getResources().getAssets().openNonAssetFd(testPath);
}
catch(Exception ex)
{
    if(ex.getMessage().equals(testPath))
        return false;
    else
        return true;
}   

// Somehow, we managed to grab the file descriptor successfully.  
// In my experience, this cannot happen with layouts, but if it ever does,
// then we definitely know the file exists.
return true;
}

By using this method, I can now determine if there's a version of the current activity's layout in the "layout-land" folder, and act accordingly.

However, I am unsatisfied with this approach, although it does work (for now, at least on the Galaxy S3 I tested this on). If anyone has a better one please let me know.

Alex
  • 1,103
  • 1
  • 11
  • 24
1

In your layout in the layout_land folder add an id = layout_name_of_this_file for your outermost FrameLayout. For example

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_name_of_this_file"
    .....

and in your code

getResources().getIdentifier("layout_name_of_this_file", "id", "com.example....")) = resource id of the button (a pretty long number)

and

getResources().getIdentifier("layout_name_of_the_layout_file_exist_in_layout_but_not_in_layout_land", "id", "com.example....")) = 0  

because there is no FrameLayout with this id.

Hoan Nguyen
  • 18,033
  • 3
  • 50
  • 54
  • Well.. first, that's not going to work, since `getIdentifier` simply returns the `id` integer that gets compiled into the `R` object. If you created that `button_fake_activity_name` id, then it will exist regardless of if you're currently in a landscape layout (Resources exist per-application, not per-Activity). However, I could just as easily only override `onConfigurationChanged` in the Activities where I didn't want to change orientations. The issue is I wanted a simple solution that I could put into the `BaseActivity` class instead of in each activity. (I have 50+ Activities...) – Alex Mar 06 '13 at 23:46
  • No what I am saying is if there is no layout_land for the name activity then there is no button_fake_activity_name and the above code will return 0. I tested in my test app. – Hoan Nguyen Mar 06 '13 at 23:48
  • I mean you only put the fake button in your layout_land and not in the regular layout. Thus no layout_land no button. – Hoan Nguyen Mar 06 '13 at 23:51
  • That's an interesting way of doing it, but still wouldn't help too much in my particular case (I could just as easily remove `configChanges="orientation"` from specific activities). I do think it's an important solution to the general problem, however. – Alex Mar 08 '13 at 22:53