u-ryo's blog

various information for coding...

Negate Method Reference

| Comments

Javaのstreamで、filter(s -> !s.isEmpty())を method referenceに出来ないかなー、と思ったんですが、 java.util.function.Predicateを使って、

  1. Java11だとfilter(Predicate.not(String::isEmpty))
  2. 現状ではfilter(((Predicate<String>) String::isEmpty).negate())と長くなる
  3. 下記のように自分で定義してfilter(not(String::isEmpty))
1
2
3
public static <T> Predicate<T> not(Predicate<T> t) {
    return t.negate();
}

cf. stack overflow: How to negate a method reference predicate

現状では長くなるので、やめました。

Android Petite Tips

| Comments

お仕事で極悪Androidアプリを改修していて、 今日得た知見をば。

getTextSize/setTextSize

あるActivityの画面で、 本文とボタンのtext sizeを揃えようとして、 TextView#getTextSizeしてからsetTextSizeしたら、 大きくなるんですよね。何でだろう、調べると、 TextView#getTextSizesetTextSizeのデフォルト単位が違う のだそう。びっくりです。

1
renewalButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, caution.getTextSize());

というように、単位を明示する必要があるそう。

サイズ自動調整TextView

TextViewで、指示通りに改行位置を固定しようと思って。 指示中の、禁則処理に失敗しているところも含めて忠実に再現しようと。 そのためにはtext sizeを随意にせねばならず。 【Android】横幅に合わせてテキストサイズを調整するTextViewそのままで上手く行きました。 あーでもonLayout()の最初の引数changedtrueの時だけ resize()すればよかとです。

今はAutosizing TextViewsというのがあるそう。 ただ、API 26からなのでまだなかなか使えないでしょうか。

onLayout後の値の取得

上記のようにtext sizeを変えてから、 その結果のtext sizeに合わせて他のViewのtext sizeを 決定しようとすると、 onLayout()が呼ばれ終わってからでないと 目的の値が取得出来ないんですね。 そこで、 How to know when an activity finishes a layout pass? にあるように、 myView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {...}); とすれば良いです。

補足

RobolectricでUnit Test書いてたら、 このclass、testが終わらないんです。 何でかなー、とbreakpointで追ってみると、 延々とonGlobalLayout()が呼ばれ続けてるんですね。 えーっと思って。 RxJavaHooks.setOnIOScheduler(s -> Schedulers.immediate()); しても、 ShadowApplication.runBackgroundTasks(); しても効き目はなく。 androidでheightやwidthが0になって取得できない時 を見ると、用が済んだらすぐremoveするんですね。そっか。 というわけで、Android SDKのversionによって分けて、 removeOnGlobalLayoutListener(this)removeGlobalOnLayoutListener(this)でremoveするように したんですけど、今度はthisが効かない。 なるほど、lambdaだとthisは外側のclass instanceになるんですね。 じゃぁっていうんでlambda自体をListener instanceとして名前付けて、 lambdaの中でthisじゃなくてその名前で参照しようとしたんですが、 might not have been initializedとか言われ、 nullで初期化すると今度はeffectively finalじゃないと言われて、 うー、とか思って仕方なく諦めて、 初めてlambdaを解いてinner classの記述に戻しました。

Rx Androidにmaxはない?

探したんですけど見つからなかったので自分で集計しました。

1
2
3
4
5
6
float textWidth = rx.Observable
        .from(getText().toString().split("\n"))
        .map(paint::measureText)
        .reduce(Math::max)
        .toBlocking()
        .single();

TextViewで白枠

ある段落を白枠で囲って欲しいと言われました。 調べると、[android]xmlで枠を指定するというのがあり、 それ用のdrawable XMLを作ってやってandroid:background="@drawable/..."で、 それを指定すれば、望み通りのものが得られました。 背景色は、これも書いてありますが#00ffffffで透明になります。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- background -->
    <solid android:color="#00ffffff" />
    <!-- rounded corners -->
    <stroke android:width="1dp"
        android:color="@color/white" />
    <corners android:radius="1dp" />
</shape>

Robolectricで次のActivityへの遷移の確認

shadowOf(activity).peekNextStartedActivity()Intentを取得、 getComponent().getClassName()が目的のclass nameかをassert。

1
2
3
Intent intent = shadowOf(activity).peekNextStartedActivity();
assertThat(Objects.requireNonNull(intent.getComponent()).getClassName(),
        is(MainActivity.class.getName()));

Local PushでNotification

Local PushでNotificationをして欲しい、と言われました。 調べてみると、要するに、 AlarmManagerPendingIntentをsetして、 それがset時の引数のUnix Time(millisec)になると、 これもset時引数のBroadcastReceiverの子classの onReceive(context, intent)が呼ばれるので、 そこでNotificationManager.notify()をする、と。

AndroidのNotificationについては、 sample applicationを作って色々と試してみました。

  1. uninstall/端末再起動すれば登録済みのalarmは解除される
  2. 多重登録してもPendingIntent.FLAG_UPDATE_CURRENTなら最後のNotificationに上書きされる
  3. 過去の時日のalarmを登録するとすぐNotifyされてしまう
  4. 機種によっては挙動が違う(Huaweiでは、アプリが起動していない時/Sleep時にAlarmを発動させるには「保護されたアプリ」でないとならない、等)
  5. 長いtextは全文出ないで端折られる。出したいなら、.setStyle(new NotificationCompat.BigTextStyle().bigText("..."))する。但し.setBigContentTitle(intent.getStringExtra("..."))も同時に加えるとダメっぽい。

How to Get the Result From DialogFragment

| Comments

DialogFragmentをnewしてdialogを表示させ、 そこでのbutton tapによって、元のActivity上で処理をさせたい時、 どうやってfeedbackしたら良いのかなと。 呼び出し元はFragmentではなくActivityなので、 Fragmentで呼び出し元に結果を伝えるReceive result from DialogFragment等にあるようにsetTargetFragment()を使えないんですよね。

結局、DialogFragment側にsetCallback(Callback callback)と Functional InterfaceとしてCallbackを定義して、 button押したらcallback.call();とし、 呼び出し元のActivity側でnew SomeDialogFragment().setCallback(() -> someMethod())としてやりたいことをsomeMethod()に込めました。 ちょっと面倒ですけどこのようにcallback駆使するしか無いのかなぁと。 onActivityResult()getTargetFragment()が使えないのと 処理をActivity側に書きたいというのがあったので。 いや、onActivityResult()の中身はActivity側ですか。 setTargetFragment()の代わりに何かActivityのreferenceを DialogFragment側に持たせればよかった?のかな? いやいや、そもそもFragmentからgetActivity()で取得できる? からこんなことしなくてよかった? あれ?? いやーでもDialogFragment側は引数の情報を持っておらず、 Activity側しか引数持ってないんですよね。 今回のぼくの場合では、 引数を引き回すか、再度SQLで取得するかしてonActivityResult()でkickするか、 callbackを作るか、ということだったでしょうか。

Caution on ListView

| Comments

今時ListViewなんてあんまり使わないと思いますが、 ListViewのviewの使い回しでbugがあったのでメモです。 ぼくがやったんじゃないです。

public View getView(int position, View convertView, ViewGroup parent)convertViewを使い回すわけですが、 nullの時とnot nullの時の扱いが微妙に違うんですね。 具体的には、 nullの時だけsetOnItemSelectedListenerを設定して、 そのOnItemSelectedListenerが表示される初回だけ onItemSelectedが発動することを利用してobjectの 設定の一部をしているから、 not nullの時にはその経路を通らず、 objectの一部がnullのままで次に進むとNullPointerExceptionに なるという。 だからこのbugの発現条件は、 「初回表示時のリストの数が画面を2つ以上超える」時、 というわかりにくい、後からでは見つけにくいものになっています。 OnItemSelectedListenerで設定しているのも悪いし、 convertViewnullの時とnullでない時の処理に きちんと心を砕かなかったから、 こういうことになるんですね。

まーでもこんなlevelのbugはまだいぃ方ですけどねー このsoftwareについては。 色々アホなのはホントやる気無くします。

参考: Android GridViewのパフォーマンスを上げよう(1/2)←そうですよね普通気を付けますよね、ListView は Graphical Layout で作ったまま使ってはいけない←そうなんですよ最初、頭の1つだけnullで呼ばれた後画面に見える分だけnot nullで呼ばれ、その後改めてnullで呼ばれるので、何でかなーと思ったものです。ただもぅ今はRecyclerViewだからこんなtipsはもう不要かなーと。