2018年9月15日土曜日

AlertDialog の Button の有効/無効を切り替える

AlertDialog の PositiveButton, NegativeButton, NeutralButton では、listener での実装によらずタップしたときに必ずダイアログが閉じます。

例えば AlertDialog でテキストを入力するようにして、未入力のときはボタンを押せないようにしたいとします。

AlertDialog の getButton() で Button インスタンスが取れるのでこれを利用します。
取得するボタンは BUTTON_POSITIVE, BUTTON_NEGATIVE, BUTTON_NEUTRAL で指定します。

あとは EditText に TextWatcher を追加して、テキストの変更時にボタンの isEnabled を変更します。

初回時 EditText が空ならボタンを disabled にしておかないといけません。
AlertDialog の getButton() は show() の前に呼ぶと NPE になるので注意が必要です。 val editText = EditText(this).apply { inputType = InputType.TYPE_CLASS_TEXT layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { val margin = (16 * resources.displayMetrics.density).toInt() marginStart = margin marginEnd = margin } } val frameLayout = FrameLayout(this).apply { addView(editText) } val dialog = AlertDialog.Builder(this) .setTitle("Title") .setMessage("Message") .setView(frameLayout) .setPositiveButton(android.R.string.ok, null) .create() editText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = !s.isNullOrBlank() } }) dialog.show() dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false



Android Activity Transitions の xml 定義で exclude を指定する

コードでのやりかたは「Android Activity Transitions の対象から、Navigation Bar と Status Bar を外す(Activity Transitions を実装する その2)」に書きました。

xml で transition を定義する場合は以下のように targets タグと target タグを使います。 <?xml version="1.0" encoding="utf-8"?> <slide xmlns:android="http://schemas.android.com/apk/res/android" android:slideEdge="end"> <targets> <target android:excludeId="@android:id/navigationBarBackground" /> <target android:excludeId="@android:id/statusBarBackground" /> </targets> </slide> target タグには excludeId の他にも以下の属性を指定できます。
  • android:targetClass
  • android:targetId
  • android:excludeId
  • android:excludeClass
  • android:targetName
  • android:excludeName


2018年9月14日金曜日

android:windowCloseOnTouchOutside を指定するとどうなるのか

android:windowCloseOnTouchOutside は API Level 11 で追加されたテーマ用の属性で、true を指定すると、Dialog 系の theme を指定した Activity でダイアログ(というか window)以外の領域をタップしたときにダイアログが閉じます(というか Activity が finish() します)。

この属性は Window に関するもので、Window では以下のフィールドとメソッドが関連します。

Window public abstract class Window { ... private boolean mCloseOnTouchOutside = false; private boolean mSetCloseOnTouchOutside = false; ... /** @hide */ public void setCloseOnTouchOutside(boolean close) { mCloseOnTouchOutside = close; mSetCloseOnTouchOutside = true; } /** @hide */ public void setCloseOnTouchOutsideIfNotSet(boolean close) { if (!mSetCloseOnTouchOutside) { mCloseOnTouchOutside = close; mSetCloseOnTouchOutside = true; } } ... /** @hide */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { final boolean isOutside = event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) || event.getAction() == MotionEvent.ACTION_OUTSIDE; if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) { return true; } return false; } private boolean isOutOfBounds(Context context, MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop(); final View decorView = getDecorView(); return (x < -slop) || (y < -slop) || (x > (decorView.getWidth()+slop)) || (y > (decorView.getHeight()+slop)); } } Window の shouldCloseOnTouch() では、mCloseOnTouchOutside が true、かつ peekDecorView が null ではない、かつ MotionEvent の action が MotionEvent.ACTION_OUTSIDE、もしくは MotionEvent.ACTION_DOWN でタップ位置が DecorView の外側の場合、true が返ります。

Window の setCloseOnTouchOutside() は hide になっていて通常のアプリからは呼べません。 ではコードでは指定できないのかというと、Activity の setFinishOnTouchOutside() から指定できます。

Activity public void setFinishOnTouchOutside(boolean finish) { mWindow.setCloseOnTouchOutside(finish); } Activity の onTouchEvent() で Window の shouldCloseOnTouch() を呼んでおり、これにより DecorView の外側をタップすると Activity が finish() します。

Activity public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }

ちなみに android:windowCloseOnTouchOutside 属性の設定値は、PhoneWindow から Window.setCloseOnTouchOutsideIfNotSet() を呼ぶことで適用されています。



2018年9月7日金曜日

Android Activity Transitions の対象をグループ化する

ActivityTransition に Slide を指定すると、デフォルトでは View ごとに別々にアニメーションします。

例えばボタンを縦に並べた Activity に Slide で enter すると、スライド中はボタンの間隔が広がって、徐々に詰まっていきます。

左 → 右 : スライド中


ボタン同士の配置そのままに Slide させるには、ボタンの親の ViewGroup の setTransitionGroup() で true をセットします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { startActivity( Intent(this, MainActivity2::class.java), ActivityOptions.makeSceneTransitionAnimation(this).toBundle() ) } } } class MainActivity2 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS) window.enterTransition = Slide() window.exitTransition = Slide() setContentView(R.layout.activity_main2) // これを追加 constraintLayout.isTransitionGroup = true } }

左 → 右 : スライド中



解説

ViewGroup の captureTransitioningViews() で isTransitionGroup() が false の場合、各子 View の captureTransitioningViews() を呼びます。 View の captureTransitioningViews() では VISIBLE な場合自身を transition するリストに追加します。

つまり、ViewGroup の setTransitionGroup() で true をセットすると、その ViewGroup が transition するリストに追加され、子 View はされなくなります。 public abstract class ViewGroup ... { ... /** @hide */ @Override public void captureTransitioningViews(List<View> transitioningViews) { if (getVisibility() != View.VISIBLE) { return; } if (isTransitionGroup()) { transitioningViews.add(this); } else { int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); child.captureTransitioningViews(transitioningViews); } } } ... } public class View ...{ ... public void captureTransitioningViews(List<View> transitioningViews) { if (getVisibility() == View.VISIBLE) { transitioningViews.add(this); } } ... }