FloatingActionButtonをスクロール中に消す

NestedScrollViewを下にスクロールしている間は, FloatingActionButtonを表示させないようにする. 少し変えれば, RecyclerView等の他のScrollViewにも使えそう(未確認). 下に実際の挙動を示した.



目次


方法

  1. 縦スクロールする場合に対してのみ有効にする.
  2. 現在のFloatingActionButtonのアニメーション状態を4つ, スクロールの状態を5つで管理する.
  3. スクロール時のアニメーションを設定する
  4. 停止時のアニメーションを設定する
  5. FloatingActionButtonのbehaviorを設定する

コード全文

参考にしたサイト


方法


  1. 縦スクロールする場合に対してのみ有効にする.


  2. 現在のFloatingActionButtonのアニメーション状態を4つ, スクロールの状態を5つで管理する.

  3. FloatingActionButtonが消えている間は, 押しても反応しないようにする. ここのアニメーションはListenerに, AnimatorListenerAdapterAnimator.AnimatorListenerのインスタンスを代入できればよく, もっと複雑にできるかもしれない. また, アニメーションの時間はそこまで長くしないほうが良い.

    現在のスクロール状態(scrollingState)と, (状態が変わる)1つ前のスクロール状態(preScrollingState)を保持しておく. 実際の状態管理については, 最後のコード全文を参照

  4. スクロール時のアニメーションを設定する
  5. 下にスクロールまたは, フリック(以降, スクロールに含める)したときに, FloatingActionButtonを消す. また, FloatingActionButtonが消失している場合, 上にスクロールしたとき再表示する. 4行目から7行目, 23行目から26行目では, 一番下までスクロールしたFloatingActionButtonが消えないようにしている(下のコードでは, 見やすさのためにスクロール状態の管理を省いている).

  6. 停止時のアニメーションを設定する
  7. FloatingActionButtonが消失している場合, 放置していると再表示する.
    上のコードで, 9行目から13行目までは, 以下のような挙動を抑えるために記述している.


    すなわち, スクロール(フリックは含めない)後, フリックすると, イベントとしては, onNestedPreScroll -> onStopNestedScroll -> onNestedPreFlingScroll (コードに関係しないイベントは除く)と, onStopNestedScrollが途中で呼ばれてしまい, 何もしないとここで, 一回FloatingActionButtonが消える.

    また, 15行目から22行目では, 200(=duration)ms未満の下スクロールを行うと, onStopNestedScrollが呼ばれたときに, stateSTATE.VANISHINGのままで, 該当箇所を抜くと, FloatingActionButtonが消えたままとなる(下のgif参照)ので, これを回避するために, そうなったときに, 400(=delayDuration)ms後, FloatingActionButtonを再表示させている.

    放置しているときに, FloatingActionButtonを再表示させたくない場合は, onStopNestedScrollをオーバーライドしなければよい.

  8. FloatingActionButtonのbehaviorを設定する
  9. MainActivityOnCreateで次のようにすればよい.


コード全文


コード全文を掲載する

VanishFloatingActionButtonBehavior.java

MainActivity.java


参考にしたサイト




NestedScrollViewで最下部にスクロールしたかを判定する

まず, NestedScrollViewのサイズを取得する方法 :

よって, これとNestedScrollView.getScrollY()を比較すればよい:


参考にしたサイト



Toolbarの挙動をコードで変える

例えば, 最初の画面ではToolbarを動かさないようにしたいが,次の画面ではToolbarをcollapseさせたい時の方法

方法


android.support.design.widget.CollapsingToolbarLayoutapp:layout_scrollFlags属性をJavaコード中で変更する.
実際には、以下のように行う.
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;

...

CollapsingToolbarLayout collapsingToolbarLayout = findViewById(R.id.collapsingToolbarLayout);
AppBarLayout.LayoutParams appBarLayoutParams = (AppBarLayout.LayoutParams)collapsingToolbarLayout.getLayoutParams();
appBarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
toolbarLayout.setLayoutParams(appBarLayoutParams);

setScrollFlagsメソッドに代入する定数は以下の通りで, 複数指定したい場合は, 上のコードのようにビット演算子OR|をはさんでやる.

定数 対応するapp:layout_scrollFlagsの値
SCROLL_FLAG_ENTER_ALWAYS enterAlways
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED enterAlwaysCollapsed
SCROLL_FLAG_SCROLL scroll
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED exitUntilCollapsed
SCROLL_FLAG_SNAP snap

上の定数は, android.support.design.widget.AppBarLayout.LayoutParamsのフィールドであるが, ToolBarの挙動については, こちらが詳しい.

実装例


FloatingActionButtonを押すことで, Toolbarの挙動をenterAlways <-> scroll|exitUntilCollapsedと相互に変える. 以下のような感じ.

styles.xml

ToolBarをつかうので, アプリケーションテーマとしてNoActionBarを採用している.
また, 11行目から13行目, 15行目から18行目は, ToolBarのタイトル文字がデフォルトでは黒色なので, 見やすい白色にするために追加している(後に, activity_main.xmlのCollaspingToolbarLayoutapp:collapsedTitleTextAppearance, app:expandedTitleTextAppearanceで指定してやる).

activity_main.xml

styles.xmlで述べたが, 17行目と19行目でToolBarのタイトルのテキストスタイルを変更している
54行目に出てくる, FloatingActionButtonに使うdrawableリソースはここには載せないが, 適当な画像を使えばよい.

MainActivity.java

ハイライトしたところで, Toolbarの挙動を変更している.
基本的に, statusで現在の状態を管理している. 詳しい説明は, コード中のコメントを参照.

実装例の最初に挙げたgifを見て気づいた方もいるかもしれないが, このコードの問題として, AppBarLayouheightwrap_contentか, あまり大きくない値を指定するとタイトルが表示されないことが挙げられる(Toolbarapp:titleを設定してないことは関係ない!). この問題は, CollapsingToolbarLayoutを使わないようにすれば回避できる:

しかし, この方法では, Toolbarの挙動をJavaコード中では変えられないっぽく, 自作のカスタムViewを作る等面倒なことをしないといけないみたい(? 現在模索中)

参考にしたサイト




VisibilityAwareImageButton.setVisibility can only be called from within the same library group

sdkのバージョンを28 beta1に変えてみると、android.support.design.widget.FloatingActionButton.setVisiblityメソッドを使っていたときに次のようなエラーが出た.
VisibilityAwareImageButton.setVisibility can only be called from within the same library group (groupId=com.android.support)

build.gradle(Module: app)でこのエラーに関すると思われるものは以下:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        ...
        targetSdkVersion 28
        ...
    }
    ...
}

dependencies {
    implementation 'com.android.support:design:28.0.0-beta01'
    ...
}

雑に言えば、FloatingActionButtonvisibilityを直接変えてはいけないようである. setVisiblityの代わりにshowメソッドかhideメソッドを使えば、エラーを回避できる.
floatingActionButton.show();
floatingActionButton.hide();

ただし、この場合、FloationActionButtonvisibilityinvisibleにすることは工夫をしないとできないようである.

参考にしたサイト



Perl 6で現在時刻を取得する、ついでにDateTimeの使い方も


DateTimeクラスのnowメソッドを使う.
my $time = DateTime.now;


処理時間を計測する


次のようにすると、任意の処理にかかる時間を計測できる(単位は秒、結果の型はDurationになる. 詳しくは、infix:<->へ).

my $start = DateTime.now;
...何かの処理...
my $end = DateTime.now;
say $start - $end;

時間を計測するだけなら、DateTimeは書かなくてもよい(正確には、termのnowを使っている. この場合は、両方ともDateTimeを書かない.):

my $start = now;
...何かの処理...
my $end = now;
say $start - $end;


DateTimeの使い方


DateTimeのメソッドと関連する関数を可能な限りまとめてみる.

目次


now メソッド
clone メソッド
hh-mm-ss、hour、minute、second メソッド
whole-second メソッド
timezone、offset メソッド
offset-in-minutes、offset-in-hours メソッド
Instance、Date、DateTime メソッド
posix メソッド
later、earlier メソッド
truncated-to メソッド
utc メソッド
in-timezone メソッド
local メソッド
infix:<-> 関数
infix:<+> 関数
Dateish ロール


now メソッド


定義 :
method now(:$timezone = $*TZ, :&formatter --> DateTime:D)

指定したタイムゾーン、指定した形式の現在時刻を返す.
タイムゾーンは、GMT(グリニッジ標準時)からのずれを秒数により指定する.デフォルトでは、$*TZ変数の値になる.

 現在時刻を表示する :
say DateTime.now;
実行結果 :
2018-05-26T17:09:47.148866+09:00


clone メソッド


定義 :
method clone(:$year, :$month, :$day, :$hour, :$minute, :$second, :$timezone, :&formatter)

既存のDateTimeオブジェクトで所定の属性を変更した新しいオブジェクトを返す.

 年を2015年に、月を1月に変える:
say DateTime.new("2018-04-30T17:37:12+09:00").clone(year => 2015, month => 1);
実行結果:
2015-01-30T17:37:12+09:00


hh-mm-ss、hour、minute、second メソッド


定義 :
method hh-mm-ss(DateTime:D: --> Str:D)
method hour(DateTime:D: --> Int:D)
method minute(DateTime:D: --> Int:D)
method second(DateTime:D:)

それぞれ、時:分:秒、時間、分、秒を返す.

 時:分:秒だけを表示する:
say DateTime.new("2018-04-30T17:37:12+09:00").hh-mm-ss;
実行結果:
17:37:12


whole-second メソッド


定義 :
method whole-second(DateTime:D:)

秒を返すが、その結果はIntに丸め込む(切り下げ).

 秒数を表示する:
say DateTime.new("2018-04-30T17:37:12.999+09:00").whole-second;
実行結果:
12 #文字が少なすぎて表示されない


timezone、offset メソッド


定義 :
method timezone(DateTime:D: --> Int:D)
method offset(DateTime:D: --> Int:D)

UTC(協定世界時)からのずれを秒数で取得する.

 タイムゾーンを表示する:
say DateTime.new("2018-04-30T17:37:12-09:00").timezone;
実行結果:
-32400


offset-in-minutes、offset-in-hours メソッド


定義 :
method offset-in-minutes(DateTime:D: --> Real:D)
method offset-in-hours(DateTime:D: --> Real:D)

UTCからのずれをそれぞれ分、時間で表示する.


Str メソッド


定義 :
method Str(DateTime:D: --> Str:D)

formatter属性によって指定された表現で、時刻を返す.デフォルトでは、 ISO 8601による表記で返す.

 時刻を表示する:
say DateTime.new("2018-04-30T17:37:12.001+09:00").Str;
実行結果:
2018-04-30T17:37:12.001000+09:00


Instance、Date、DateTime メソッド


定義 :
method Instant(DateTime:D: --> Instant:D)
multi method Date(DateTime:U --> Date:U)
multi method Date(DateTime:D --> Date:D)
method DateTime(--> DateTime)

それぞれ、自身のInstanceオブジェクト、Dateオブジェクト、DateTimeオブジェクトを返す.


posix メソッド


POSIX/UNIXに基づいた時刻、すなわち、1970年1月1日 UTC からの経過秒数を返す.

 POSIX/UNIXによる時刻を表示する:
say DateTime.new("2018-04-30T17:37:12.001+09:00").posix;
実行結果:
1525077432


later、earlier メソッド


定義 :
method later(DateTime:D: *%unit)
method earlier(DateTime:D: *%unit)

指定した時間だけ遅らせた(later)、または、早めた(earlier)時刻を返す.
引数として有効なキーは以下の通り:
second,seconds,minute,minutes,hour,hours,day,week,weeks,month,months,year,years.
second,seconds以外を使ったとき以外の値は、Intに変換される.
負の値も指定できる.

例1 1日後の時刻を表示する:
say DateTime.new("2018-04-30T17:37:12.001+09:00").later(:1day);
次でもよい.
say DateTime.new("2018-04-30T17:37:12.001+09:00").later(:day);
実行結果:
2018-05-01T17:37:12.001000+09:00

例2 2年前の時刻を表示する:
say DateTime.new("2018-04-30T17:37:12.001+09:00").earlier(:2years);
次でもよい.
say DateTime.new("2018-04-30T17:37:12.001+09:00").earlier(:2year);
実行結果:
2016-04-30T17:37:12.001000+09:00


truncated-to メソッド


定義 :
method truncated-to(DateTime:D: Cool $unit)

指定場所未満の時刻を切り捨てた時刻を返す(例を見るとわかる).
有効な引数は以下の通り:
'second','minute','hour','day','month','year'
second,seconds

 日以降を切り捨てる:
say DateTime.new("2018-04-30T17:37:12.001+09:00").truncated-to('day');
実行結果:
2018-04-30T00:00:00+09:00


utc メソッド


定義 :
method utc(DateTime:D: --> DateTime:D)

UTCでのオブジェクトの時刻を返す.

 UTCでの時刻を表示する:
say DateTime.new("2018-04-30T17:37:12.001+09:00").utc;
実行結果:
2018-04-30T08:37:12.001000Z


in-timezone メソッド


定義 :
method in-timezone(DateTime:D: $timezone = 0 --> DateTime:D)

指定したtimezoneにおけるオブジェクトの時刻を返す.引数には、UTCからの秒数でのずれを指定する.
$timezone = 0とすれば(つまり、デフォルト)、utcメソッドと同じ値が返される.

 ロサンゼルス(太平洋夏時間:UTC-7)における時刻を表示する :
say DateTime.new("2018-04-30T17:37:12.001+09:00").in-timezone(-7 * 60 * 60);
実行結果:
2018-04-30T01:37:12.001000-07:00


local メソッド


定義 :
method local(DateTime:D: --> DateTime:D)

in-timezoneと同じだが、ローカルタイムゾーン($*TZ)で指定する.$*TZは秒数で指定する.

 ロサンゼルス(太平洋夏時間:UTC-7)における時刻を表示する :
my $*TZ = -7 * 60 * 60; #myは省略可
say DateTime.new("2018-04-30T17:37:12.001+09:00").local;
実行結果:
2018-04-30T01:37:12.001000-07:00


infix:<-> 関数


定義 :
multi sub infix:<-> (DateTime:D, Duration:D --> DateTime:D)
multi sub infix:<-> (DateTime:D, DateTime:D --> Duration:D)

DateTimeオブジェクトと、他のDateTimeオブジェクトまたは、Durationオブジェクトとの差を返す.返り値の型は右オペランドによる.すなわち、
DateTime-Duration=DateTime
DateTime-DateTime=Duration
となる.

 30日前の時刻を表示する :
say DateTime.new("2018-04-30T17:37:12.001+09:00") - Duration.new(30 * 24 * 60 * 60);
実行結果:
2018-03-31T17:37:12.001000+09:00


infix:<+> 関数


定義 :
multi sub infix:<+> (DateTime:D, Duration:D --> DateTime:D)
multi sub infix:<+> (Duration:D, DateTime:D --> DateTime:D)

DateTimeオブジェクトを、Durationオブジェクト分だけ進める(順序は問わない).

 2日後の時刻を表示する :
say DateTime.new("2018-04-30T17:37:12.001+09:00") + Duration.new(2 * 24 * 60 * 60);
実行結果:
2018-05-02T17:37:12.001000+09:00


Dateish ロール


DateTimeは、Dateishを実装(Does)しているため、Dateishのメソッドも使える.詳細は、こちらを参照.


参考にしたサイト



以下のドキュメンテーションを和(意)訳、改変した.

Parcelabeと実行時エラー

なかなか直せなかったバグが、Parcelable関連を見直すことで直ったので、その時に気づいた注意点を残しておく.

Parcelableを実装するときの注意


例えば、Stringの配列をreadする際は、readStringArrayではなく、createStringArrayを使う :
public class MyData implements Parcelable {
    
    private String mStringArray[];
    
    public String[] getStringArray() {
        return mStringArray;
    }
    
    public void setStringArray(String stringArray[]) {
        this.mStringArray = stringArray;
    }
    
    private MyData(Parcel in) {
        mStringArray = in.createStringArray();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeStringArray(mStringArray);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<MyData> CREATOR = new Creator<MyData>() {
        @Override
        public MyData createFromParcel(Parcel in) {
            return new MyData(in);
        }

        @Override
        public MyData[] newArray(int size) {
            return new MyData[size];
        }
    };
}

もし、readStringArray使った場合、画面回転程度の読み込みではエラーは出ないが、「アクティビティ破棄する(アクティビティを保持しない)」設定をオンにした状態で再起動する(ホームボタン→アプリ起動)と、私の環境では次のエラーが出た.
Caused by: java.lang.NullPointerException: Attempt to get length of null array
...
at MainActivity.onCreate(MainActivity.java:xxx)


Parcelableでデータを受渡しするときの注意


特に、onRestoreInstanceStateonSavedInstanceStateでアクティビティの復元を行う際、putParcelableArraygetParcelableArrayなどで、Parcelableの配列を使わないほうが良い.
...
private MyData myData[];
...
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    ...
    myData = (MyData[])savedInstanceState.getParcelableArray(Key1);
    ...
}

上のようにキャストしないとコンパイルエラーが発生するが、アクティビティが破棄された後(「アクティビティを破棄する」設定をオンにしてアプリケーションを終了する)では、Bundle内で型の情報が保持されないようで、次のようなエラーが出た.
Caused by: java.lang.ClassCastException: android.os.Parcelable[] cannot be cast to com.foo.example.MyData[]
        at com.foo.example.MainActivity.onViewStateRestored(MainActivity.java:xxx)

代わりに、putParcelableArrayListgetParcelableArrayListを使うことで、このエラーを回避できた.
...
private ArrayList<MyData> myData;
...
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    ...
    myData = savedInstanceState.getParcelableArrayList(Key1);
    ...
}


参考にしたサイト



RecyclerView in NestedScrollViewでスクロールが角つく

javaコード中で、次を追加すると回避できた.
recyclerView.setNestedScrollingEnabled(false);

しかし、そもそもNestedScrollViewとRecyclerViewをネストにすることが問題っぽい.
例えば、画面をスクロールしてもRecyclerView.onScrolledは呼ばれないみたいなので、ネストするというデザインを変えたほうがいいかもしれない.

参考にしたサイト



FloatingActionButtonをスクロール中に消す

NestedScrollView を下にスクロールしている間は, FloatingActionButton を表示させないようにする. 少し変えれば, RecyclerView 等の他のScrollViewにも使えそう(未確認). 下に実際の挙動を示した. 目次 ...