1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 @file:Suppress("DEPRECATION")
17 
18 package com.example.android.supportv4.view
19 
20 import android.animation.Animator
21 import android.animation.AnimatorListenerAdapter
22 import android.animation.ValueAnimator
23 import android.annotation.SuppressLint
24 import android.app.Activity
25 import android.graphics.Canvas
26 import android.graphics.Color
27 import android.graphics.Paint
28 import android.os.Bundle
29 import android.os.SystemClock
30 import android.util.Log
31 import android.view.MotionEvent
32 import android.view.View
33 import android.view.ViewConfiguration
34 import android.view.ViewGroup
35 import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
36 import android.view.animation.LinearInterpolator
37 import android.widget.AdapterView
38 import android.widget.ArrayAdapter
39 import android.widget.Button
40 import android.widget.CheckBox
41 import android.widget.Spinner
42 import android.widget.TextView
43 import android.widget.ToggleButton
44 import androidx.annotation.RequiresApi
45 import androidx.core.graphics.Insets
46 import androidx.core.view.ViewCompat
47 import androidx.core.view.WindowCompat
48 import androidx.core.view.WindowInsetsAnimationCompat
49 import androidx.core.view.WindowInsetsAnimationControlListenerCompat
50 import androidx.core.view.WindowInsetsAnimationControllerCompat
51 import androidx.core.view.WindowInsetsCompat
52 import androidx.core.view.WindowInsetsCompat.Type.ime
53 import androidx.core.view.WindowInsetsCompat.Type.navigationBars
54 import androidx.core.view.WindowInsetsCompat.Type.statusBars
55 import androidx.core.view.WindowInsetsCompat.Type.systemBars
56 import androidx.core.view.WindowInsetsControllerCompat
57 import com.example.android.supportv4.R
58 import java.util.ArrayList
59 import kotlin.concurrent.thread
60 import kotlin.math.abs
61 import kotlin.math.max
62 import kotlin.math.min
63 
64 @SuppressLint("InlinedApi")
65 @RequiresApi(21)
66 class WindowInsetsControllerPlayground : Activity() {
67 
68     private val TAG: String = "WindowInsets_Playground"
69 
70     val mTransitions = ArrayList<Transition>()
71     var currentType: Int? = null
72 
73     private lateinit var mRoot: View
74     private lateinit var editRow: ViewGroup
75     private lateinit var visibility: TextView
76     private lateinit var buttonsRow: ViewGroup
77     private lateinit var buttonsRow2: ViewGroup
78     private lateinit var fitSystemWindow: CheckBox
79     private lateinit var isDecorView: CheckBox
80     internal lateinit var info: TextView
81     lateinit var graph: View
82 
83     val values = mutableListOf(0f)
84 
85     @SuppressLint("SetTextI18n")
86     override fun onCreate(savedInstanceState: Bundle?) {
87         super.onCreate(savedInstanceState)
88         setContentView(R.layout.activity_insets_controller)
89         setActionBar(findViewById(R.id.toolbar))
90 
91         mRoot = findViewById(R.id.root)
92         editRow = findViewById(R.id.editRow)
93         visibility = findViewById(R.id.visibility)
94         buttonsRow = findViewById(R.id.buttonRow)
95         buttonsRow2 = findViewById(R.id.buttonRow2)
96         info = findViewById(R.id.info)
97         fitSystemWindow = findViewById(R.id.decorFitsSystemWindows)
98         isDecorView = findViewById(R.id.isDecorView)
99         addPlot()
100 
101         WindowCompat.setDecorFitsSystemWindows(window, fitSystemWindow.isChecked)
102         WindowCompat.getInsetsController(window, window.decorView).systemBarsBehavior =
103             WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
104 
105         Log.e(
106             TAG,
107             "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS: " +
108                 (window.attributes.flags and FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS != 0)
109         )
110 
111         fitSystemWindow.apply {
112             isChecked = false
113             setOnCheckedChangeListener { _, isChecked ->
114                 WindowCompat.setDecorFitsSystemWindows(window, isChecked)
115                 if (isChecked) {
116                     mRoot.setPadding(0, 0, 0, 0)
117                 }
118             }
119         }
120 
121         mTransitions.add(Transition(findViewById(R.id.scrollView)))
122         mTransitions.add(Transition(editRow))
123 
124         setupTypeSpinner()
125         setupHideShowButtons()
126         setupAppearanceButtons()
127         setupBehaviorSpinner()
128         setupLayoutButton()
129 
130         setupIMEAnimation()
131         setupActionButton()
132 
133         isDecorView.setOnCheckedChangeListener { _, _ -> setupIMEAnimation() }
134     }
135 
136     private fun addPlot() {
137         val stroke = 20
138         val p2 = Paint()
139         p2.color = Color.RED
140         p2.strokeWidth = 1f
141         p2.style = Paint.Style.FILL
142 
143         graph =
144             object : View(this) {
145                 override fun onDraw(canvas: Canvas) {
146                     super.onDraw(canvas)
147                     val mx = (values.maxOrNull() ?: 0f) + 1
148                     val mn = values.minOrNull() ?: 0f
149                     val ct = values.size.toFloat()
150 
151                     val h = height - stroke * 2
152                     val w = width - stroke * 2
153                     values.forEachIndexed { i, f ->
154                         val x = (i / ct) * w + stroke
155                         val y = ((f - mn) / (mx - mn)) * h + stroke
156                         canvas.drawCircle(x, y, stroke.toFloat(), p2)
157                     }
158                 }
159             }
160         graph.minimumWidth = 300
161         graph.minimumHeight = 100
162         graph.setBackgroundColor(Color.GRAY)
163         findViewById<ViewGroup>(R.id.graph_container)
164             .addView(graph, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200))
165     }
166 
167     private fun setupAppearanceButtons() {
168         mapOf<String, (Boolean) -> Unit>(
169                 "LIGHT_NAV" to
170                     { isLight ->
171                         WindowCompat.getInsetsController(window, mRoot)
172                             .isAppearanceLightNavigationBars = isLight
173                     },
174                 "LIGHT_STAT" to
175                     { isLight ->
176                         WindowCompat.getInsetsController(window, mRoot)
177                             .isAppearanceLightStatusBars = isLight
178                     },
179             )
180             .forEach { (name, callback) ->
181                 buttonsRow.addView(
182                     ToggleButton(this).apply {
183                         text = name
184                         textOn = text
185                         textOff = text
186                         setOnCheckedChangeListener { _, isChecked -> callback(isChecked) }
187                         isChecked = true
188                         callback(true)
189                     }
190                 )
191             }
192     }
193 
194     private var visibilityThreadRunning = true
195 
196     @SuppressLint("SetTextI18n")
197     override fun onResume() {
198         super.onResume()
199         thread {
200             visibilityThreadRunning = true
201             while (visibilityThreadRunning) {
202                 visibility.post {
203                     visibility.text =
204                         currentType?.let {
205                             ViewCompat.getRootWindowInsets(mRoot)?.isVisible(it).toString()
206                         } + " " + window.attributes.flags + " " + SystemClock.elapsedRealtime()
207                 }
208                 Thread.sleep(500)
209             }
210         }
211     }
212 
213     override fun onPause() {
214         super.onPause()
215         visibilityThreadRunning = false
216     }
217 
218     private fun setupActionButton() {
219         findViewById<View>(R.id.floating_action_button).setOnClickListener { v: View? ->
220             WindowCompat.getInsetsController(window, v!!)
221                 .controlWindowInsetsAnimation(
222                     ime(),
223                     -1,
224                     LinearInterpolator(),
225                     null /* cancellationSignal */,
226                     object : WindowInsetsAnimationControlListenerCompat {
227                         override fun onReady(
228                             controller: WindowInsetsAnimationControllerCompat,
229                             types: Int
230                         ) {
231                             val anim = ValueAnimator.ofFloat(0f, 1f)
232                             anim.duration = 1500
233                             anim.addUpdateListener { animation: ValueAnimator ->
234                                 controller.setInsetsAndAlpha(
235                                     controller.shownStateInsets,
236                                     animation.animatedValue as Float,
237                                     anim.animatedFraction
238                                 )
239                             }
240                             anim.addListener(
241                                 object : AnimatorListenerAdapter() {
242                                     override fun onAnimationEnd(animation: Animator) {
243                                         super.onAnimationEnd(animation)
244                                         controller.finish(true)
245                                     }
246                                 }
247                             )
248                             anim.start()
249                         }
250 
251                         override fun onCancelled(
252                             controller: WindowInsetsAnimationControllerCompat?
253                         ) {}
254 
255                         override fun onFinished(
256                             controller: WindowInsetsAnimationControllerCompat
257                         ) {}
258                     }
259                 )
260         }
261     }
262 
263     private fun setupIMEAnimation() {
264         mRoot.setOnTouchListener(createOnTouchListener())
265         if (isDecorView.isChecked) {
266             ViewCompat.setWindowInsetsAnimationCallback(mRoot, null)
267             ViewCompat.setWindowInsetsAnimationCallback(window.decorView, createAnimationCallback())
268             // Why it doesn't work on the root view?
269         } else {
270             ViewCompat.setWindowInsetsAnimationCallback(window.decorView, null)
271             ViewCompat.setWindowInsetsAnimationCallback(mRoot, createAnimationCallback())
272         }
273     }
274 
275     private fun createAnimationCallback(): WindowInsetsAnimationCompat.Callback {
276         return object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
277             override fun onPrepare(animation: WindowInsetsAnimationCompat) {
278                 mTransitions.forEach { it.onPrepare(animation) }
279             }
280 
281             override fun onProgress(
282                 insets: WindowInsetsCompat,
283                 runningAnimations: List<WindowInsetsAnimationCompat>
284             ): WindowInsetsCompat {
285                 val systemInsets = insets.getInsets(systemBars())
286                 mRoot.setPadding(
287                     systemInsets.left,
288                     systemInsets.top,
289                     systemInsets.right,
290                     systemInsets.bottom
291                 )
292                 mTransitions.forEach { it.onProgress(insets) }
293                 return insets
294             }
295 
296             override fun onStart(
297                 animation: WindowInsetsAnimationCompat,
298                 bounds: WindowInsetsAnimationCompat.BoundsCompat
299             ): WindowInsetsAnimationCompat.BoundsCompat {
300                 mTransitions.forEach { obj -> obj.onStart() }
301                 return bounds
302             }
303 
304             override fun onEnd(animation: WindowInsetsAnimationCompat) {
305                 mTransitions.forEach { it.onFinish(animation) }
306             }
307         }
308     }
309 
310     private fun setupHideShowButtons() {
311         findViewById<Button>(R.id.btn_show).apply {
312             setOnClickListener { view ->
313                 currentType?.let { type ->
314                     WindowCompat.getInsetsController(window, view).show(type)
315                 }
316             }
317         }
318         findViewById<Button>(R.id.btn_hide).apply {
319             setOnClickListener { view ->
320                 currentType?.let { type ->
321                     WindowCompat.getInsetsController(window, view).hide(type)
322                 }
323             }
324         }
325     }
326 
327     private fun setupLayoutButton() {
328         arrayOf(
329                 "STABLE" to View.SYSTEM_UI_FLAG_LAYOUT_STABLE,
330                 "STAT" to View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,
331                 "NAV" to View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
332             )
333             .forEach { (name, flag) ->
334                 buttonsRow2.addView(
335                     ToggleButton(this).apply {
336                         text = name
337                         textOn = text
338                         textOff = text
339                         setOnCheckedChangeListener { _, isChecked ->
340                             val systemUiVisibility = window.decorView.systemUiVisibility
341                             window.decorView.systemUiVisibility =
342                                 if (isChecked) systemUiVisibility or flag
343                                 else systemUiVisibility and flag.inv()
344                         }
345                         isChecked = false
346                     }
347                 )
348             }
349         window.decorView.systemUiVisibility =
350             window.decorView.systemUiVisibility and
351                 (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
352                         View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
353                         View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
354                     .inv()
355     }
356 
357     private fun createOnTouchListener(): View.OnTouchListener {
358         return object : View.OnTouchListener {
359             private val mViewConfiguration =
360                 ViewConfiguration.get(this@WindowInsetsControllerPlayground)
361             var mAnimationController: WindowInsetsAnimationControllerCompat? = null
362             var mCurrentRequest: WindowInsetsAnimationControlListenerCompat? = null
363             var mRequestedController = false
364             var mDown = 0f
365             var mCurrent = 0f
366             var mDownInsets = Insets.NONE
367             var mShownAtDown = false
368 
369             @SuppressLint("ClickableViewAccessibility")
370             override fun onTouch(v: View, event: MotionEvent): Boolean {
371                 mCurrent = event.y
372                 when (event.action) {
373                     MotionEvent.ACTION_DOWN -> {
374                         mDown = event.y
375                         val rootWindowInsets = ViewCompat.getRootWindowInsets(v)!!
376                         mDownInsets = rootWindowInsets.getInsets(ime())
377                         mShownAtDown = rootWindowInsets.isVisible(ime())
378                         mRequestedController = false
379                         mCurrentRequest = null
380                     }
381                     MotionEvent.ACTION_MOVE -> {
382                         if (mAnimationController != null) {
383                             updateInset()
384                         } else if (
385                             abs(mDown - event.y) > mViewConfiguration.scaledTouchSlop &&
386                                 !mRequestedController
387                         ) {
388                             mRequestedController = true
389                             val listener =
390                                 object : WindowInsetsAnimationControlListenerCompat {
391                                     override fun onReady(
392                                         controller: WindowInsetsAnimationControllerCompat,
393                                         types: Int
394                                     ) {
395                                         if (mCurrentRequest === this) {
396                                             mAnimationController = controller
397                                             updateInset()
398                                         } else {
399                                             controller.finish(mShownAtDown)
400                                         }
401                                     }
402 
403                                     override fun onFinished(
404                                         controller: WindowInsetsAnimationControllerCompat
405                                     ) {
406                                         mAnimationController = null
407                                     }
408 
409                                     override fun onCancelled(
410                                         controller: WindowInsetsAnimationControllerCompat?
411                                     ) {
412                                         mAnimationController = null
413                                     }
414                                 }
415                             mCurrentRequest = listener
416 
417                             WindowCompat.getInsetsController(window, v)
418                                 .controlWindowInsetsAnimation(
419                                     ime(),
420                                     1000,
421                                     LinearInterpolator(),
422                                     null /* cancellationSignal */,
423                                     listener
424                                 )
425                         }
426                     }
427                     MotionEvent.ACTION_UP,
428                     MotionEvent.ACTION_CANCEL -> {
429                         if (mAnimationController != null) {
430                             val isCancel = event.action == MotionEvent.ACTION_CANCEL
431                             mAnimationController!!.finish(
432                                 if (isCancel) mShownAtDown else !mShownAtDown
433                             )
434                             mAnimationController = null
435                         }
436                         mRequestedController = false
437                         mCurrentRequest = null
438                     }
439                 }
440                 return true
441             }
442 
443             fun updateInset() {
444                 var inset = (mDownInsets.bottom + (mDown - mCurrent)).toInt()
445                 val hidden = mAnimationController!!.hiddenStateInsets.bottom
446                 val shown = mAnimationController!!.shownStateInsets.bottom
447                 val start = if (mShownAtDown) shown else hidden
448                 val end = if (mShownAtDown) hidden else shown
449                 inset = max(inset, hidden)
450                 inset = min(inset, shown)
451                 mAnimationController!!.setInsetsAndAlpha(
452                     Insets.of(0, 0, 0, inset),
453                     1f,
454                     (inset - start) / (end - start).toFloat()
455                 )
456             }
457         }
458     }
459 
460     private fun setupTypeSpinner() {
461         val types =
462             mapOf(
463                 "System" to systemBars(),
464                 "IME" to ime(),
465                 "Navigation" to navigationBars(),
466                 "Status" to statusBars(),
467                 "All" to (systemBars() or ime())
468             )
469         findViewById<Spinner>(R.id.spn_insets_type).apply {
470             adapter =
471                 ArrayAdapter(
472                     context,
473                     android.R.layout.simple_spinner_dropdown_item,
474                     types.keys.toTypedArray()
475                 )
476             onItemSelectedListener =
477                 object : AdapterView.OnItemSelectedListener {
478                     override fun onNothingSelected(parent: AdapterView<*>?) {}
479 
480                     override fun onItemSelected(
481                         parent: AdapterView<*>?,
482                         view: View?,
483                         position: Int,
484                         id: Long
485                     ) {
486                         if (parent != null) {
487                             currentType = types[parent.selectedItem]
488                         }
489                     }
490                 }
491         }
492     }
493 
494     private fun setupBehaviorSpinner() {
495         val types =
496             mapOf(
497                 "DEFAULT" to WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
498                 "TRANSIENT" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
499                 "BY TOUCH (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
500                 "BY SWIPE (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
501             )
502         findViewById<Spinner>(R.id.spn_behavior).apply {
503             adapter =
504                 ArrayAdapter(
505                     context,
506                     android.R.layout.simple_spinner_dropdown_item,
507                     types.keys.toTypedArray()
508                 )
509             onItemSelectedListener =
510                 object : AdapterView.OnItemSelectedListener {
511                     override fun onNothingSelected(parent: AdapterView<*>?) {}
512 
513                     override fun onItemSelected(
514                         parent: AdapterView<*>?,
515                         view: View?,
516                         position: Int,
517                         id: Long
518                     ) {
519                         if (parent != null && view != null) {
520                             WindowCompat.getInsetsController(window, view).systemBarsBehavior =
521                                 types[selectedItem]!!
522                         }
523                     }
524                 }
525             setSelection(0)
526         }
527     }
528 
529     inner class Transition(private val view: View) {
530         private var mEndBottom = 0
531         private var mStartBottom = 0
532         private var mInsetsAnimation: WindowInsetsAnimationCompat? = null
533         private val debug = view.id == R.id.editRow
534 
535         @SuppressLint("SetTextI18n")
536         fun onPrepare(animation: WindowInsetsAnimationCompat) {
537             if (animation.typeMask and ime() != 0) {
538                 mInsetsAnimation = animation
539             }
540             mStartBottom = view.bottom
541             if (debug) {
542                 values.clear()
543                 info.text = "Prepare: start=$mStartBottom, end=$mEndBottom"
544             }
545         }
546 
547         fun onProgress(insets: WindowInsetsCompat) {
548             view.y = (mStartBottom + insets.getInsets(ime() or systemBars()).bottom).toFloat()
549             if (debug) {
550                 Log.d(TAG, view.y.toString())
551                 values.add(view.y)
552                 graph.invalidate()
553             }
554         }
555 
556         @SuppressLint("SetTextI18n")
557         fun onStart() {
558             mEndBottom = view.bottom
559             if (debug) {
560                 info.text = "${info.text}\nStart: start=$mStartBottom, end=$mEndBottom"
561             }
562         }
563 
564         fun onFinish(animation: WindowInsetsAnimationCompat) {
565             if (mInsetsAnimation == animation) {
566                 mInsetsAnimation = null
567             }
568         }
569     }
570 }
571