Skip to content

Commit 103634d

Browse files
committed
Redesign paused in debugger overlay (Android)
1 parent 8bb555a commit 103634d

16 files changed

Lines changed: 589 additions & 400 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt‎

Lines changed: 10 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,8 @@
77

88
package com.facebook.react.devsupport
99

10-
import android.animation.Animator
11-
import android.animation.AnimatorListenerAdapter
12-
import android.animation.AnimatorSet
13-
import android.animation.ObjectAnimator
1410
import android.graphics.Color
15-
import android.graphics.drawable.GradientDrawable
16-
import android.os.Build
17-
import android.os.SystemClock
18-
import android.util.TypedValue
19-
import android.view.Gravity
20-
import android.view.LayoutInflater
21-
import android.view.View
22-
import android.view.ViewGroup
23-
import android.view.WindowManager
24-
import android.view.animation.AccelerateInterpolator
25-
import android.view.animation.DecelerateInterpolator
26-
import android.widget.ImageView
27-
import android.widget.PopupWindow
28-
import android.widget.TextView
29-
import androidx.core.view.ViewCompat
30-
import androidx.core.view.WindowInsetsCompat
31-
import com.facebook.common.logging.FLog
32-
import com.facebook.react.R
3311
import com.facebook.react.bridge.UiThreadUtil
34-
import com.facebook.react.common.ReactConstants
3512
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
3613
import java.util.Locale
3714

@@ -40,12 +17,9 @@ import java.util.Locale
4017
* the top of the screen. All methods are thread safe.
4118
*/
4219
public class DefaultDevLoadingViewImplementation(
43-
private val reactInstanceDevHelper: ReactInstanceDevHelper,
20+
reactInstanceDevHelper: ReactInstanceDevHelper,
4421
) : DevLoadingViewManager {
45-
private var devLoadingView: TextView? = null
46-
private var devLoadingPopup: PopupWindow? = null
47-
private var hideAnimator: Animator? = null
48-
private var showTimeMs = 0L
22+
private val banner = DevBanner(reactInstanceDevHelper)
4923

5024
override fun showMessage(message: String) {
5125
showMessage(message, color = null, backgroundColor = null, dismissButton = false)
@@ -61,7 +35,12 @@ public class DefaultDevLoadingViewImplementation(
6135
return
6236
}
6337
UiThreadUtil.runOnUiThread {
64-
showInternal(message, color, backgroundColor, dismissButton ?: false)
38+
banner.show(
39+
message,
40+
color?.toInt() ?: Color.WHITE,
41+
backgroundColor?.toInt() ?: Color.rgb(64, 64, 64), // Default grey
42+
dismissButton ?: false,
43+
)
6544
}
6645
}
6746

@@ -75,239 +54,21 @@ public class DefaultDevLoadingViewImplementation(
7554
else if (done != null && total != null && total > 0)
7655
String.format(Locale.getDefault(), " %.1f%%", done.toFloat() / total * 100)
7756
else ""
78-
devLoadingView?.text =
79-
"${status ?: "Loading"}${percentage}\u2026" // `...` character at the end
57+
banner.updateMessage("${status ?: "Loading"}${percentage}…") // `...` character at the end
8058
}
8159
}
8260

8361
public override fun hide() {
8462
if (isEnabled) {
85-
UiThreadUtil.runOnUiThread { hideInternal() }
63+
UiThreadUtil.runOnUiThread { banner.hide() }
8664
}
8765
}
8866

89-
private fun showInternal(
90-
message: String,
91-
color: Double?,
92-
backgroundColor: Double?,
93-
dismissButton: Boolean,
94-
) {
95-
// A new message replaces one that is still animating out.
96-
hideAnimator?.cancel()
97-
showTimeMs = SystemClock.uptimeMillis()
98-
99-
val textColor = color?.toInt() ?: Color.WHITE
100-
val bgColor = backgroundColor?.toInt() ?: Color.rgb(64, 64, 64) // Default grey
101-
102-
val existingPopup = devLoadingPopup
103-
if (existingPopup?.isShowing == true) {
104-
applyContent(existingPopup.contentView, message, textColor, bgColor, dismissButton)
105-
return
106-
}
107-
108-
val currentActivity = reactInstanceDevHelper.currentActivity
109-
if (currentActivity == null) {
110-
FLog.e(
111-
ReactConstants.TAG,
112-
"Unable to display loading message because react " + "activity isn't available",
113-
)
114-
return
115-
}
116-
117-
try {
118-
val decorView = currentActivity.window.decorView
119-
val inflater = LayoutInflater.from(currentActivity)
120-
val pill = inflater.inflate(R.layout.dev_loading_view, null) as ViewGroup
121-
applyContent(pill, message, textColor, bgColor, dismissButton)
122-
123-
// Sits 12dp below a top inset such as the status bar, or 16dp from the top edge without one,
124-
// centered and kept clear of the side insets with a 16dp margin.
125-
val insets =
126-
ViewCompat.getRootWindowInsets(decorView)
127-
?.getInsets(
128-
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
129-
)
130-
val topInset = insets?.top ?: 0
131-
val sideInset = maxOf(insets?.left ?: 0, insets?.right ?: 0) + dpToPx(pill, 16f)
132-
val textView = pill.findViewById<TextView>(R.id.loading_text)
133-
val dismissButtonView = pill.findViewById<View>(R.id.dismiss_button)
134-
val dismissButtonWidth =
135-
if (dismissButton) {
136-
(dismissButtonView.layoutParams as ViewGroup.MarginLayoutParams).let {
137-
it.width + it.marginStart
138-
}
139-
} else {
140-
0
141-
}
142-
textView.maxWidth =
143-
decorView.width - 2 * sideInset - pill.paddingStart - pill.paddingEnd - dismissButtonWidth
144-
145-
pill.setOnClickListener { hideInternal() }
146-
dismissButtonView.setOnClickListener { hideInternal() }
147-
148-
val popup =
149-
PopupWindow(
150-
pill,
151-
ViewGroup.LayoutParams.WRAP_CONTENT,
152-
ViewGroup.LayoutParams.WRAP_CONTENT,
153-
)
154-
popup.showAtLocation(
155-
decorView,
156-
Gravity.TOP or Gravity.CENTER_HORIZONTAL,
157-
0,
158-
if (topInset > 0) topInset + dpToPx(pill, 12f) else dpToPx(pill, 16f),
159-
)
160-
// A new message resizes the window about its center. Without this, the system animates that
161-
// move, so the capsule grows from its old left edge and then slides back to center.
162-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
163-
val popupRoot = pill.rootView
164-
(popupRoot.layoutParams as? WindowManager.LayoutParams)?.let {
165-
it.setCanPlayMoveAnimation(false)
166-
currentActivity.windowManager.updateViewLayout(popupRoot, it)
167-
}
168-
}
169-
devLoadingView = textView
170-
devLoadingPopup = popup
171-
animateIn(pill)
172-
173-
// TODO T164786028: Find out the root cause of the BadTokenException exception here
174-
} catch (e: WindowManager.BadTokenException) {
175-
FLog.e(
176-
ReactConstants.TAG,
177-
"Unable to display loading message because react activity isn't active, message: $message",
178-
)
179-
}
180-
}
181-
182-
private fun applyContent(
183-
pill: View,
184-
message: String,
185-
textColor: Int,
186-
bgColor: Int,
187-
dismissButton: Boolean,
188-
) {
189-
pill.background =
190-
GradientDrawable().apply {
191-
setColor(bgColor)
192-
// Larger than any banner height, so the corners always form a capsule.
193-
cornerRadius = dpToPx(pill, 1000f).toFloat()
194-
}
195-
val textView = pill.findViewById<TextView>(R.id.loading_text)
196-
textView.text = message
197-
textView.setTextColor(textColor)
198-
199-
val dismissButtonView = pill.findViewById<ImageView>(R.id.dismiss_button)
200-
dismissButtonView.visibility = if (dismissButton) View.VISIBLE else View.GONE
201-
// The round button sits closer to the capsule's end than the text would.
202-
pill.setPaddingRelative(
203-
pill.paddingStart,
204-
pill.paddingTop,
205-
dpToPx(pill, if (dismissButton) 9f else 14f),
206-
pill.paddingBottom,
207-
)
208-
dismissButtonView.setColorFilter(textColor)
209-
dismissButtonView.background =
210-
GradientDrawable().apply {
211-
shape = GradientDrawable.OVAL
212-
setColor(
213-
Color.argb(51, Color.red(textColor), Color.green(textColor), Color.blue(textColor))
214-
)
215-
}
216-
}
217-
218-
private fun animateIn(pill: View) {
219-
pill.alpha = 0f
220-
pill.scaleX = HIDDEN_SCALE
221-
pill.scaleY = HIDDEN_SCALE
222-
pill.post {
223-
setHiddenPivot(pill)
224-
pill
225-
.animate()
226-
.alpha(1f)
227-
.scaleX(1f)
228-
.scaleY(1f)
229-
.setDuration(ANIMATION_DURATION_MS)
230-
.setInterpolator(DecelerateInterpolator())
231-
.start()
232-
}
233-
}
234-
235-
private fun hideInternal() {
236-
val popup = devLoadingPopup ?: return
237-
if (!popup.isShowing || hideAnimator != null) {
238-
return
239-
}
240-
val pill = popup.contentView
241-
setHiddenPivot(pill)
242-
val animator =
243-
AnimatorSet().apply {
244-
playTogether(
245-
ObjectAnimator.ofFloat(pill, View.ALPHA, 0f),
246-
ObjectAnimator.ofFloat(pill, View.SCALE_X, HIDDEN_SCALE),
247-
ObjectAnimator.ofFloat(pill, View.SCALE_Y, HIDDEN_SCALE),
248-
)
249-
// Keeps a message that is shown and hidden in quick succession, such as a fast refresh,
250-
// on screen long enough to read.
251-
startDelay = maxOf(0L, MIN_PRESENTED_TIME_MS - (SystemClock.uptimeMillis() - showTimeMs))
252-
duration = ANIMATION_DURATION_MS
253-
interpolator = AccelerateInterpolator()
254-
addListener(
255-
object : AnimatorListenerAdapter() {
256-
private var canceled = false
257-
258-
override fun onAnimationCancel(animation: Animator) {
259-
canceled = true
260-
}
261-
262-
override fun onAnimationEnd(animation: Animator) {
263-
hideAnimator = null
264-
// A new message canceled the hide, so the banner stays and returns to full size.
265-
if (canceled) {
266-
pill
267-
.animate()
268-
.alpha(1f)
269-
.scaleX(1f)
270-
.scaleY(1f)
271-
.setDuration(ANIMATION_DURATION_MS)
272-
.setInterpolator(DecelerateInterpolator())
273-
.start()
274-
return
275-
}
276-
if (popup.isShowing) {
277-
popup.dismiss()
278-
}
279-
if (devLoadingPopup === popup) {
280-
devLoadingPopup = null
281-
devLoadingView = null
282-
}
283-
}
284-
}
285-
)
286-
}
287-
hideAnimator = animator
288-
animator.start()
289-
}
290-
29167
public companion object {
292-
private const val ANIMATION_DURATION_MS = 200L
293-
private const val MIN_PRESENTED_TIME_MS = 600L
294-
private const val HIDDEN_SCALE = 0.85f
295-
// The hidden state scales about a point this fraction of the banner's height above its center.
296-
private const val HIDDEN_ANCHOR_OFFSET = 0.05f
297-
29868
private var isEnabled = true
29969

30070
public fun setDevLoadingEnabled(enabled: Boolean) {
30171
isEnabled = enabled
30272
}
303-
304-
private fun setHiddenPivot(pill: View) {
305-
pill.pivotX = pill.width / 2f
306-
pill.pivotY = pill.height * (0.5f - HIDDEN_ANCHOR_OFFSET)
307-
}
308-
309-
private fun dpToPx(view: View, dp: Float): Int =
310-
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, view.resources.displayMetrics)
311-
.toInt()
31273
}
31374
}

0 commit comments

Comments
 (0)