【Android】端末の方向やディスプレイサイズから色々なものを動的に変更する

ようやく忙しさが(多少)やわらぎました。

相変わらず時間を見てはのんびりと自作のTwitterクライアントのためのコードを書いており、ちょっとしたネタもたまってきたので吐き出していきます。

端末の方向を動的に取得 / 固定する

Androidの端末の回転に関しては、数多あるAndroid SDKのBad Partsの中でも5本の指に余裕で入るものだと思います。

いくらなんでも開発者の負担が大きすぎます。フレームワークでもうちょっと吸収してくれよと思わざるを得ません。

このままこの話を続けると、この世のものとは思えないほど汚い言葉が出てきてしまいかねないので、さっさと説明しましょう。

まずは端末の方向を取得しましょう。

やり方は簡単です。API Levelが8未満ならDisplay#getOrientationメソッド、それ以上ならDisplay#getRotationメソッドを使います。

どちらもintが返ってくるんですが、比較するのはSurfaceクラスの定数群です。

Activity#setRequestedOrientationメソッドActivityInfoクラスの「SCREEN_ORIENTATION」で始まる定数を渡すことで画面の方向を固定できます。

とりあえずコードを出してしまいましょう。と言ってもこれを参考に改良しただけですが…。

public final class OrientationUtil {
    
    public static final int O_NOTHING = 0;
    public static final int O_PORTRAIT = 1;
    public static final int O_LANDSCAPE = 2;
    public static final int O_REVERSE_PORTRAIT = 3;
    public static final int O_REVERSE_LANDSCAPE = 4;
    
    public static final int getCurrentOrientationLock(Context context) {
        return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString("orientationLockValue", "0"));
    }
    
    public static final boolean setLockOrientation(Activity activity, int orientation) {
        int currentOrientation = getCurrentOrientation(activity.getApplicationContext());
        
        switch(orientation) {
        case O_NOTHING:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
            break;
        case O_PORTRAIT:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            break;
        case O_LANDSCAPE:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            break;
        case O_REVERSE_PORTRAIT:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
            break;
        case O_REVERSE_LANDSCAPE:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
            break;
        default:
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
        }
        
        return orientation != O_NOTHING && currentOrientation != orientation;
    }
    
    public static final boolean setLockOrientation(Activity activity) {
        return setLockOrientation(activity, getCurrentOrientationLock(activity.getApplicationContext());
    }
    
    public static final int getCurrentOrientation(Context context) {
        switch(((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation()) {
        case Surface.ROTATION_90:
            return O_LANDSCAPE;
        case Surface.ROTATION_270:
            return O_REVERSE_LANDSCAPE;
        case Surface.ROTATION_180:
            return O_REVERSE_PORTRAIT;
        default:
            return O_PORTRAIT;
        }
    }
    
    public static final boolean isPortrate(Context context) {
        int orientation = getCurrentOrientation(context);
        return orientation == O_PORTRAIT || orientation == O_REVERSE_PORTRAIT;
    }
    
}

現在の方向を取得しているのがgetCurrentOrientation、方向を固定しているのがsetLockOrientationです。

Preferenceでユーザに設定させることを想定しているので色々なメソッドを適当に入れています。

ちなみにActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAITActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAITAPI Levelが9以上じゃないと使えません。ややこしいですねー。

とは言え、所詮はintの定数なので、ドキュメントに書いてある値を渡せば普通に動いてくれます。互換性を気にする方は上手くBuild.VERSION.SDK_INTで振り分けてあげましょう。

どうでもいいと言えばどうでもいい話なんですが、setLockOrientationでbooleanを返しているのは「画面固定によって画面の回転が実行されてしまったか?」を知りたかったからです。

各ActivityのsetContentViewの前にこの方向固定のためのメソッドを呼んでいるんですが、うっかり回転が発生してしまうと二重でonCreateが流れてしまい爆発四散する事故が多発しました。そんなわけでsetLockOrientationでtrueが返されてしまったら一旦returnし、二度目のonCreateでちゃんと初期化してもらう、と言う涙ぐましい努力をしています。

後はisPortrateメソッドなどからsetContentViewで渡すリソースIDを判断すれば「端末の方向が縦の時と横の時でレイアウトを変える」なんてことも可能です。

端末のサイズを取得する

じゃあ次は端末のサイズを取得していきましょう。

端末のサイズと言っても色々とあります。ステータスバーやナビゲーションバーも含めた文字通り端末のサイズなのか、それらやタイトルバー(アクションバー)を除いた、表示領域としてアプリ開発者が使えるViewのサイズなのか。

何となくのイメージがわかない人はこれを読むとわかりやすいと思います。

上記の記事にも書かれていますが、ステータスバーやナビゲーションバーのサイズは端末依存になってしまうことや、それらのサイズを簡単に取得できないことからも、その辺まで考慮してレイアウトを決定するべきではない、と思います。

また、そもそもナビゲーションバーは4.0からだとか、タブレットのUIだとここに書いてないシステムバーと言うものがあるだとか、ナビゲーションバーも物理キーが存在する端末だと存在そのものがないケースがあるとか、相当色々なことを考慮する必要が出てきます。そのコストに見合うだけのデザインを考えたのならやってみてもいいんじゃないでしょうか。

表示領域のサイズを取得する。

以前ActionBarを無理矢理下に持っていった時の応用みたいなもんです。Window#getDecorView()で取得できるViewの中にはsetContentViewで指定したリソースを突っ込むViewが用意されています。(参考

そのViewにはandroid.R.id.contentと言うリソースIDが振られているので、こんな風にすることができます。

View contentView = getWindow().getDecorView().findViewById(android.R.id.content);
int width = contentView.getWidth();
int height = contentView.getHeight();

ただし、onCreateの時点ではWidth / Heightともに0が返ってきます。まだ初期化中なので仕方ないですね。

Activity#onAttachedToWindowあたりで取得するのが無難でしょう。Fragmentでやる場合はonActivityCreatedあたりでいいと思います。DecorViewを取得するためにはgetActivity().getWindow()しなきゃいけませんしね。

この値を使用することで、「あるViewのサイズを端末のサイズに合わせて変更する」なんてことも当然可能です。

私はこんな風にwidthとheightをパーセンテージ指定するようなメソッドを作りました。

public static void fitViewToDisplay(Window window, View v, int width, int height) {
    View contentView = window.getDecorView().findViewById(android.R.id.content);
    int w = contentView.getWidth();
    int h = contentView.getHeight();
    
    int newWidth = (int) (width != ViewGroup.LayoutParams.MATCH_PARENT
                                && width != ViewGroup.LayoutParams.WRAP_CONTENT
                 ? w * (width * 0.01)
                 : width);

    int newHeight = (int) (height != ViewGroup.LayoutParams.MATCH_PARENT
                                    && height != ViewGroup.LayoutParams.WRAP_CONTENT
                  ? h * (height * 0.01)
                  : height);

    ViewGroup.LayoutParams p = v.getLayoutParams();
    p.width = newWidth;
    p.height = newHeight;
    v.setLayoutParams(p);
}

端末のサイズから判定を行う

もし「onCreate内で端末のサイズから判定して使用するリソースIDを変えたい」と言う場合はWindowManagerクラスgetDefaultDisplayメソッドを使うのが無難です。

DisplayクラスgetWidth / getHeightメソッドでおおよその端末のサイズはわかります。おおよそと言っても、端末のサイズからナビゲーションバーのサイズを抜いた値なのでその程度の判断であれば十分でしょう。widthだけ知りたいのであればこれが一番楽だと思います。

注意する点があるとすれば、API Levelが13以降であればgetSizeメソッドの方を使わなくてはならないこと、また、widthとheightは端末の方向に依存することでしょうか。(後者はこれに限った話ではないですが…。)

端末のwidthが480px以上だったらタブレットとする、なんて判定をするにはこんな感じです。

[2014/08/01修正]Pointのプロパティ名が滅茶苦茶な間違い方をしていたので修正しました、

public static boolean isTablet(Context context) {
    Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    
    Point p = new Point();
    
    if(Build.VERSION.SDK_INT < 13) {
        p.x = display.getWidth();
        p.y = display.getHeight();
    } else {
        // なんと参照渡しである。
        display.getSize(p);
    }
    
    return OrientationUtil.isPortrait(context) ? p.x > 480 : p.y > 480;
    
}

ステータスバーやナビゲーションバーのサイズを取得する

先に言っておきますが、この辺のサイズの取得は「こんなやり方で頑張ってとってる人がいるよ」程度の紹介にとどめておきます。

ステータスバーはこんな感じです。DecorViewにはステータスバーも含まれているので、そこから上手いこと計算して取得しているようです。

ナビゲーションバーはこんな感じです。内部リソースのIDから取得しています。

ステータスバーも内部リソースから取得する方法が書かれています。が、両者とも「OSの標準で決められたサイズ」を取得するだけなので、実際に同じ値かどうかはわかりません。

まとめ

と言うわけで、端末依存に関するあれこれのお話でした。

参考