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