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 になるので注意が必要です。
  1. val editText = EditText(this).apply {  
  2.     inputType = InputType.TYPE_CLASS_TEXT  
  3.     layoutParams = FrameLayout.LayoutParams(  
  4.         FrameLayout.LayoutParams.MATCH_PARENT,  
  5.         FrameLayout.LayoutParams.WRAP_CONTENT  
  6.     ).apply {  
  7.         val margin = (16 * resources.displayMetrics.density).toInt()  
  8.         marginStart = margin  
  9.         marginEnd = margin  
  10.     }  
  11. }  
  12.   
  13. val frameLayout = FrameLayout(this).apply {  
  14.     addView(editText)  
  15. }  
  16.   
  17. val dialog = AlertDialog.Builder(this)  
  18.     .setTitle("Title")  
  19.     .setMessage("Message")  
  20.     .setView(frameLayout)  
  21.     .setPositiveButton(android.R.string.ok, null)  
  22.     .create()  
  23.   
  24. editText.addTextChangedListener(object : TextWatcher {  
  25.     override fun afterTextChanged(s: Editable?) {  
  26.     }  
  27.   
  28.     override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {  
  29.     }  
  30.   
  31.     override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {  
  32.         dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = !s.isNullOrBlank()  
  33.     }  
  34. })  
  35.   
  36. dialog.show()  
  37. 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 タグを使います。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <slide xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:slideEdge="end">  
  4.     <targets>  
  5.         <target android:excludeId="@android:id/navigationBarBackground" />  
  6.         <target android:excludeId="@android:id/statusBarBackground" />  
  7.     </targets>  
  8. </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
  1. public abstract class Window {  
  2.     ...  
  3.   
  4.     private boolean mCloseOnTouchOutside = false;  
  5.     private boolean mSetCloseOnTouchOutside = false;  
  6.   
  7.     ...  
  8.   
  9.     /** @hide */  
  10.     public void setCloseOnTouchOutside(boolean close) {  
  11.         mCloseOnTouchOutside = close;  
  12.         mSetCloseOnTouchOutside = true;  
  13.     }  
  14.   
  15.     /** @hide */  
  16.     public void setCloseOnTouchOutsideIfNotSet(boolean close) {  
  17.         if (!mSetCloseOnTouchOutside) {  
  18.             mCloseOnTouchOutside = close;  
  19.             mSetCloseOnTouchOutside = true;  
  20.         }  
  21.     }  
  22.   
  23.     ...  
  24.   
  25.     /** @hide */  
  26.     public boolean shouldCloseOnTouch(Context context, MotionEvent event) {  
  27.         final boolean isOutside =  
  28.                 event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)  
  29.                 || event.getAction() == MotionEvent.ACTION_OUTSIDE;  
  30.         if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {  
  31.             return true;  
  32.         }  
  33.         return false;  
  34.     }  
  35.   
  36.     private boolean isOutOfBounds(Context context, MotionEvent event) {  
  37.         final int x = (int) event.getX();  
  38.         final int y = (int) event.getY();  
  39.         final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();  
  40.         final View decorView = getDecorView();  
  41.         return (x < -slop) || (y < -slop)  
  42.                 || (x > (decorView.getWidth()+slop))  
  43.                 || (y > (decorView.getHeight()+slop));  
  44.     }  
  45.   
  46. }  
Window の shouldCloseOnTouch() では、mCloseOnTouchOutside が true、かつ peekDecorView が null ではない、かつ MotionEvent の action が MotionEvent.ACTION_OUTSIDE、もしくは MotionEvent.ACTION_DOWN でタップ位置が DecorView の外側の場合、true が返ります。

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

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

Activity
  1. public boolean onTouchEvent(MotionEvent event) {  
  2.     if (mWindow.shouldCloseOnTouch(this, event)) {  
  3.         finish();  
  4.         return true;  
  5.     }  
  6.   
  7.     return false;  
  8. }  


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



2018年9月7日金曜日

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

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

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

左 → 右 : スライド中


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


左 → 右 : スライド中



解説

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

つまり、ViewGroup の setTransitionGroup() で true をセットすると、その ViewGroup が transition するリストに追加され、子 View はされなくなります。
  1. public abstract class ViewGroup ... {  
  2.   
  3.     ...  
  4.   
  5.     /** @hide */  
  6.     @Override  
  7.     public void captureTransitioningViews(List<View> transitioningViews) {  
  8.         if (getVisibility() != View.VISIBLE) {  
  9.             return;  
  10.         }  
  11.         if (isTransitionGroup()) {  
  12.             transitioningViews.add(this);  
  13.         } else {  
  14.             int count = getChildCount();  
  15.             for (int i = 0; i < count; i++) {  
  16.                 View child = getChildAt(i);  
  17.                 child.captureTransitioningViews(transitioningViews);  
  18.             }  
  19.         }  
  20.     }  
  21.   
  22.     ...  
  23. }  
  24.   
  25. public class View ...{  
  26.   
  27.    ...  
  28.   
  29.     public void captureTransitioningViews(List<View> transitioningViews) {  
  30.         if (getVisibility() == View.VISIBLE) {  
  31.             transitioningViews.add(this);  
  32.         }  
  33.     }  
  34.   
  35.     ...  
  36. }