[Android 9] ナビゲーションバーの画面回転ボタンを非表示にする

f:id:mnoqlo:20190421004619p:plain

Android 9 Pieで、画面右下に表示される画面回転ボタンを表示させないようにして無効化する方法を紹介します。
いずれの方法もroot化は不要です。

adbコマンドで設定する方法

1. PCでadbコマンドを使える状態にする

PCでadbコマンドを使える状態にします。
こちらのやり方は長くなるので各自調べてね。

2. Android側で『開発者向けオプション』を表示

Android側で開発者向けオプションを表示します(既に表示している場合は手順3へ)。
『設定』アプリから [端末情報] を開き、 [ビルド番号] を7回連続でタップしてください。
『これで開発者になりました!』の通知とともに [開発者向けオプション] が解放されます。

f:id:mnoqlo:20190507081415p:plainf:id:mnoqlo:20190507074046p:plain

3. Android側でUSBデバッグをオン

『設定』アプリから、 [システム] > [詳細設定] > [開発者向けオプション] を表示し、 [USBデバッグ] をオンにします。

f:id:mnoqlo:20190507074056p:plainf:id:mnoqlo:20190507074101p:plain

4. adbコマンドを実行

PCとAndroid端末をUSB接続します。
PCでコマンドライン(Windowsなら『コマンド プロンプト』等)を起動し、以下のコマンドを実行します。

adb shell settings put secure show_rotation_suggestions 0

f:id:mnoqlo:20190507074701p:plain

以上で、回転ボタンが表示されなくなるはずです。

Tips. 設定を元に戻す方法

設定を元に戻す(画面回転ボタンを表示する)場合は、上記の手順4のコマンドの代わりに以下のコマンドを実行します。

adb shell settings put secure show_rotation_suggestions 1

画面制御アプリを使う方法

1. 『Rotation Control』をインストール

Google Play ストアより『Rotation Control』をインストールします。
play.google.com

2. Rotation Controlを起動、開始

Rotation Controlを起動し、回転制御開始をオンにします。
それ以外の設定を任意で行います。

f:id:mnoqlo:20190420222344p:plainf:id:mnoqlo:20190420222347p:plain

3. 通知領域から設定

手順2で回転制御開始をオンにすると、通知領域にウィジェットが表示されます。
ウィジェットの左側にある盾のようなアイコンを選択します。
また、他のアイコンより画面制御モードを設定します。

f:id:mnoqlo:20190420230913p:plainf:id:mnoqlo:20190420230937p:plain

以上で、回転ボタンが表示されなくなるはずです。

Tips. ウィジェットのアイコンの説明

ウィジェットのアイコンに対応する画面制御モードの説明は、アプリ起動画面の『説明の表示』より見ることができます。

f:id:mnoqlo:20190420222347p:plainf:id:mnoqlo:20190420233655p:plain

Tips. 『Rotation Controlが他のアプリの上に表示されています』と通知が出る場合

手順3で盾のようなアイコンを選択したタイミングで、『Rotation Controlが他のアプリの上に表示されています』と削除できない通知が表示される場合があります。
この通知が不要な場合、その通知を長押しし、『通知を表示しない』を選択してください。

f:id:mnoqlo:20190420232704p:plainf:id:mnoqlo:20190420232706p:plain

[Twitter] PNG画像の投稿方法が変わりました

Twitterにて2019年2月11日(タイムゾーン不明)にPNG画像の投稿に関する仕様変更がありました。

変更後の仕様

上から順に判定されるイメージです。

  1. 3MB以上はJPEGで投稿される
  2. PNG8はPNGで投稿される
  3. 長辺900px以下はPNGで投稿される
  4. 上記以外は一旦品質85%のJPEGに変換され、変換前と変換後でファイルサイズの小さい方が投稿される

(ファイルサイズがちょうど3MBな画像の扱いはよくわかんないです)

図解

フローチャートにしてみました。
f:id:mnoqlo:20190214045306p:plain:h700

Q&A

つまりどういうことだってばよ

画質に関して言えば、どうやって投稿するにしても一長一短あるので、特にこだわらない方は気にする必要は無いです。
こだわる人はがんばって理解して!

PNG8ってなに?

簡単に言うと、色数の少ないPNG画像のことです。
JPEGやPNG24が約1677万色表現できるのに対してPNG8はわずか256色です。
写真にはまず向いていません。
また、イラストも種類によっては向かない場合があるので、普段投稿するイラストで一度比較テストをしてみるといいかもしれません。

仕様変更前のように1px透過させる必要はあるの?

必要なくなりました。

半透明部分のあるPNG画像を投稿したい

画像1枚のファイルサイズを3MB未満にしたうえで、長辺を900px以下にして投稿すると確実です。

透明部分のあるPNG画像を投稿したい

画像1枚のファイルサイズを3MB未満にしたうえで、長辺を900px以下にするかPNG8にして投稿すると確実です。

85%JPEG変換後よりサイズが小さいことってある?

ほぼないです。単色が画像の大部分を占めるような場合はあり得ます。

『サーバー内部のエラー』とメッセージが出て投稿できない

このエラーは仕様変更前と変更後ともに確認しましたが、具体的な条件は不明です。
少なくとも画像サイズ(いわゆる解像度)を小さくすることによって対応できます。

上記の通りに投稿してもPNGにならない

AndroidiOSTwitter公式アプリではすべてのPNGJPEGで投稿されます。
その場合、ブラウザにてTwitterのサイトから投稿するか、他のPNG画像を投稿できるクライアントから投稿してください。

PNG画像を投稿できるクライアントは?

逆に、AndroidiOSTwitter公式アプリではすべてのPNGJPEGで投稿されます。

ソースは?

以下は、Twitter社の方による仕様変更についての説明です。

Upcoming changes to PNG image support
https://twittercommunity.com/t/upcoming-changes-to-png-image-support/118695


質問がある

コメントください。

この記事に間違いがある

コメントください。

コーク派?ペプシ派?

ドクターペッパー派。

[Android] PreferenceFragmentCompatで設定画面を作る

AppCompatActivity + PreferenceFragmentCompatで作る設定画面のサンプルコードです。

完成図

今回扱う内容は

  • Fragmentの遷移
  • 例としてListPreferenceの定義と適用

です。

コード

詳しいことはコード内のコメントに書きましたので参照ください。

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Buttonクリックで設定画面へ
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, SettingsActivity.class));
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        // レイアウトルートの背景をテーマ設定の値によって変更
        RelativeLayout root = findViewById(R.id.root);
        SharedPreferences defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        switch (defaultSharedPreferences.getString("preference_theme", getString(R.string.default_value_preference_theme))) {
            case "light":
                root.setBackgroundColor(Color.parseColor("#FFFFFF"));
                break;
            case "dark":
                root.setBackgroundColor(Color.parseColor("#000000"));
                break;
        }
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hatenablog.mnoqlo.sampleapplication.MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:gravity="center"
        android:text="設定画面を開く" />

</RelativeLayout>

SettingsActivity.java

public class SettingsActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);

        // Toolbarの設定
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        // savedInstanceStateがnullでない場合は前回のFragmentが自動で復元されるのでnullの場合のみ処理
        if (savedInstanceState == null) {
            // トップ画面のFragmentを表示
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.fragment_container, SettingsFragment.newInstance("preference_root"))
                    .commit();
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            // ActionBarの矢印がクリックされたとき、Backボタンと同等の処理をする
            // 前のFragmentに戻るのではなくActivity自体を終了させたい場合は代わりに finish();
            onBackPressed();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    // PreferenceScreenがクリックされた時に呼び出されます
    @Override
    public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
        // Fragmentの切り替えと、addToBackStackで戻るボタンを押した時に前のFragmentに戻るようにする
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.fragment_container, SettingsFragment.newInstance(pref.getKey()))
                .addToBackStack(null)
                .commit();
        return true;
    }

}

activity_settings.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hatenablog.mnoqlo.sampleapplication.SettingsActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:theme="@style/ToolbarTheme" />

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/toolbar" />

</RelativeLayout>

SettingsFragment.java

public class SettingsFragment extends PreferenceFragmentCompat {

    public static SettingsFragment newInstance(String rootKey) {
        SettingsFragment fragment = new SettingsFragment();
        Bundle bundle = new Bundle();
        // 第1引数をPreferenceFragmentCompat.ARG_PREFERENCE_ROOTとすることでonCreatePreferencesの第2引数がここでputしたrootKeyになります
        bundle.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, rootKey);
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.preferences, rootKey);

        // クリックされたPreferenceScreen毎にPreferenceのカスタマイズなど
        switch (rootKey) {
            case "preference_appearance":
                onCreateAppearancePreferences();
                break;
            case "preference_others":
                break;
        }
    }

    private void onCreateAppearancePreferences() {
        // テーマ設定の現在の値をSummaryに表示
        ListPreference themePreference = (ListPreference) findPreference("preference_theme");
        themePreference.setSummary(themePreference.getEntry());
        themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
            @Override
            public boolean onPreferenceChange(Preference preference, Object newValue) {
                int indexOfValue = themePreference.findIndexOfValue(String.valueOf(newValue));
                themePreference.setSummary(indexOfValue >= 0 ? themePreference.getEntries()[indexOfValue] : null);
                return true;
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
        // ActionBarのタイトルに現在表示中のPreferenceScreenのタイトルをセット
        String rootKey = getArguments().getString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT);
        getActivity().setTitle(findPreference(rootKey).getTitle());
    }

}

preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:title="設定"
    android:key="preference_root">

    <PreferenceScreen
        android:title="外観設定"
        android:key="preference_appearance">

        <ListPreference
            android:title="テーマ"
            android:key="preference_theme"
            android:entries="@array/entries_preference_theme"
            android:entryValues="@array/entry_values_preference_theme"
            android:defaultValue="@string/default_value_preference_theme" />

    </PreferenceScreen>

    <PreferenceScreen
        android:title="その他の設定"
        android:key="preference_others">

        <PreferenceScreen />

    </PreferenceScreen>

</PreferenceScreen>

arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string-array name="entries_preference_theme">
        <item>ライト</item>
        <item>ダーク</item>
    </string-array>
    <string-array name="entry_values_preference_theme">
        <item>light</item>
        <item>dark</item>
    </string-array>

</resources>

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Sample Application</string>

    <string name="default_value_preference_theme">light</string>
</resources>

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="ToolbarTheme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
        <item name="android:background">@color/colorPrimary</item>
    </style>
</resources>

build.gradle

/*
省略
*/
dependencies {
    implementation 'androidx.preference:preference:最新バージョン'
    // 試してはいませんが、AndroidXではなくSupport Libraryの場合は implementation 'com.android.support:preference-v14:最新バージョン'
}

androidxの方のバージョンはたぶんここ
Support Libraryの方のバージョンはたぶんここ


間違い等ありましたら、コメントしてくださると助かります🙏🙏

[Android] RecyclerViewのスクロール直後にクリックが動作しない

事象

CoordinatorLayout + AppBarLayout + RecyclerViewのようなレイアウトにおいて、RecyclerViewを端までスクロールさせた際、直後数秒間1回目のタップが動作しない不具合が発生しました。

原因

コンポーネント自体の不具合のようです。
AOSP issue 66996774

解決策

方法1

Support Libraryのバージョン 27.0.1 で修正されたとのアナウンスがありました(https://developer.android.com/topic/libraries/support-library/revisions#bug-fixes_8)
が、アップデートしても改善されないとの声が多くあります。
一応build.gradleよりアップデートを試してみて、改善されない場合は以下の方法をお試しください。

方法2

以下のリンク先のようにAppBarLayout.Behaviorを修正する方法です。基本的にはこちらで対応してください。
Fixed AppBarLayout.Behavior for https://issuetracker.google.com/66996774 · GitHub

方法3

ほとんど方法2と変わりませんが、以下のようにAppBarLayoutを拡張してもいいかもしれません(推奨はしません)。

FixedAppBarLayout.java

public class FixedAppBarLayout extends AppBarLayout {

    public FixedAppBarLayout(Context context) {
        super(context, null);
    }

    public FixedAppBarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setLayoutParams(ViewGroup.LayoutParams params) {
        if (params instanceof CoordinatorLayout.LayoutParams) {
            ((CoordinatorLayout.LayoutParams) params).setBehavior(new FixedAppBarLayoutBehavior());
        }
        super.setLayoutParams(params);
    }

    // 以下、方法2と同じ
    public static class FixedAppBarLayoutBehavior extends AppBarLayout.Behavior {

        public FixedAppBarLayoutBehavior() {
            super();
        }

        public FixedAppBarLayoutBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target,
                                   int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed, type);
            stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
        }

        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                                      View target, int dx, int dy, int[] consumed, int type) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
            stopNestedScrollIfNeeded(dy, child, target, type);
        }

        private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) {
            if (type == ViewCompat.TYPE_NON_TOUCH) {
                final int currOffset = getTopAndBottomOffset();
                if ((dy < 0 && currOffset == 0)
                        || (dy > 0 && currOffset == -child.getTotalScrollRange())) {
                    ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH);
                }
            }
        }

    }

}

layout.xml

<パッケージ.FixedAppBarLayout
    android:layout_height="..."
    android:layout_width="...">

[Twitter] 6種類のアイコンサイズについて

Twitterのアイコン(プロフィール画像)には6種類のサイズがあります。

サイズ 解像度 URL例
mini 24x24 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX_mini.jpg
https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg_mini
normal 48x48 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX_normal.jpg
https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg_normal
bigger 73x73 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX_bigger.jpg
https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg_bigger
200x200 200x200 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX_200x200.jpg
https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg_200x200
400x400 400x400 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX_400x400.jpg
https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg_400x400
original 512x512 https://pbs.twimg.com/profile_images/0000000000000000000/XXXXXXXX.jpg

解像度は元画像の解像度によって異なる場合があるので注意です。
それと、以前originalサイズで画像の取得自体できないユーザーがいたので、自動化する場合は例外処理をしましょう。

[Twitter API] OAuthのCallback URLホワイトリスト化に対応する

概要

2018/06/12よりTwitter OAuthのCallback URL関係で変更がありました。
この変更に伴い、以下のようなエラーが発生するようになりました。

code: 415
Callback URL not approved for this client application. Approved callback URLs can be adjusted in your application settings.

変更内容はCallback URLs — Twitter Developersに書いてあるんですが、変更後は

みたいです。
また、次のような場合には注意してください。

Callback URLにクエリ文字列を使用したい場合

例えばCallback URLに『https://yourdomain.com?source=twitter』と設定したい場合は、次のようにします。

  1. developer.twitter.com/en/appsのCallback URL設定に『https://yourdomain.com』と登録
  2. POST oauth/request_tokenのcallback_url(oauth_callback)パラメータを『https://yourdomain.com?source=twitter』と指定

Callback URLにlocalhostを使用したい場合

developer.twitter.com/en/appsのCallback URL設定はホスト名が『localhost』だと登録できません。
代わりに、次のいずれかの方法で登録します。

いずれの方法でもポート番号を指定することができるようです(例: http://127.0.0.1:2000)

Callback URLにモバイルアプリへのリンクを設定したい場合

例えばCallback URLに『example://authorize』と設定したい場合は、次のようにします。

  1. developer.twitter.com/en/appsのCallback URL設定に『example://』と登録
  2. POST oauth/request_tokenのcallback_url(oauth_callback)パラメータを『example://authorize』と指定

Twitter Kitを使用している場合

Twitter Kitを使用している場合、冒頭のエラーまたは以下のエラーが発生する場合があります。

Failed to get request token

その場合、developer.twitter.com/en/appsのCallback URL設定を以下のように登録します。

twittersdk://

twitterkit-{アプリのCONSUMERKEY}://

Callback URL設定数が上限に達した場合

developer.twitter.com/en/appsで登録できるCallback URLの数には10個という上限があります。
その上限に達した場合は、Callback URLを単一のアドレスにまとめてPOST oauth/request_tokenでクエリ文字列を使用してください、とのことですたぶん。
自信が無いので引用しておきます。

Need more than 10 callback URLs?
There is a hard limit of 10 callback URLs in the Twitter apps dashboard. Please make sure to combine your callback URLs into a single address and use query strings in your oauth/request_token request.

Callback URLs — Twitter Developers

[Twitter4J] Twitter APIのJSONを扱う

java

// JSONを保存するよう設定する
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.setJSONStoreEnabled(true);
TwitterFactory twitterFactory = new TwitterFactory(configurationBuilder.build());
Twitter twitter = twitterFactory.getInstance();

// 例としてGET statuses/show/:idを行っています
Status status = twitter.showStatus(997324372867076096L);

// 保存されているJSONを取得
String statusJSON = TwitterObjectFactory.getRawJSON(status);

// こうすることでJSONからオブジェクトを生成することもできます
Status status1 = TwitterObjectFactory.createStatus(statusJSON);


上記コードではJSONを保存する設定をConfigurationBuilderクラスから行っていますが、
下記のようにtwitter4j.propertiesファイルから行うこともできます。

twitter4j.properties

jsonStoreEnabled=true

java

// 例としてGET followers/listを行っています
PagableResponseList<User> followersList = TwitterFactory.getSingleton().getFollowersList("mnoqlo", -1);

// 保存されているJSONを取得
String userJSON = TwitterObjectFactory.getRawJSON(followersList.get(0));

// JSONからオブジェクトを生成
User user = TwitterObjectFactory.createUser(userJSON);

jsonStoreEnabledはメモリを食うためデフォルトでfalseになっているとのことなので、
JSONが必要な時に動的に設定できる前者のやり方がいいかもしれません(JSONを取得したいことが多い場合などは別)。