mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Move additional fragments to core UI.
This commit is contained in:
committed by
Greyson Parrelli
parent
8d749c404f
commit
62d951b438
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
api(libs.google.zxing.core)
|
||||
api(libs.material.material)
|
||||
api(libs.androidx.window.window)
|
||||
api(libs.accompanist.permissions)
|
||||
|
||||
// JUnit is used by test fixtures
|
||||
|
||||
@@ -20,7 +20,15 @@ object CoreUiDependencies {
|
||||
val isIncognitoKeyboardEnabled: Boolean
|
||||
get() = _provider.provideIsIncognitoKeyboardEnabled()
|
||||
|
||||
val isScreenSecurityEnabled: Boolean
|
||||
get() = _provider.provideIsScreenSecurityEnabled()
|
||||
|
||||
val forceSplitPane: Boolean
|
||||
get() = _provider.provideForceSplitPane()
|
||||
|
||||
interface Provider {
|
||||
fun provideIsIncognitoKeyboardEnabled(): Boolean
|
||||
fun provideIsScreenSecurityEnabled(): Boolean
|
||||
fun provideForceSplitPane(): Boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.signal.core.ui.util.ThemeUtil
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import com.google.android.material.R as MaterialR
|
||||
|
||||
/**
|
||||
* Forces rounded corners on BottomSheet.
|
||||
*
|
||||
* Expects [R.attr.fixedRoundedCornerBottomSheetStyle] to be defined in your app theme, pointing to a
|
||||
* style that extends [Widget.CoreUi.FixedRoundedCorners]. Subclasses can override [themeResId] to
|
||||
* use an alternate style.
|
||||
*/
|
||||
abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
/**
|
||||
* Sheet corner radius in DP
|
||||
*/
|
||||
protected open val cornerRadius: Int
|
||||
get() = if (resources.getWindowSizeClass().isSplitPane()) {
|
||||
32
|
||||
} else {
|
||||
18
|
||||
}
|
||||
|
||||
protected open val peekHeightPercentage: Float = 0.5f
|
||||
|
||||
protected open val themeResId: Int
|
||||
get() = ThemeUtil.getThemedResourceId(requireContext(), R.attr.fixedRoundedCornerBottomSheetStyle)
|
||||
|
||||
@ColorInt
|
||||
protected var backgroundColor: Int = Color.TRANSPARENT
|
||||
|
||||
private lateinit var dialogBackground: MaterialShapeDrawable
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, themeResId)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dialog?.window?.initializeScreenshotSecurity()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
|
||||
|
||||
dialog.behavior.peekHeight = (resources.displayMetrics.heightPixels * peekHeightPercentage).toInt()
|
||||
|
||||
val cornerRadiusPx = DimensionUnit.DP.toPixels(cornerRadius.toFloat())
|
||||
val shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setTopLeftCorner(CornerFamily.ROUNDED, cornerRadiusPx)
|
||||
.setTopRightCorner(CornerFamily.ROUNDED, cornerRadiusPx)
|
||||
.build()
|
||||
|
||||
dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
|
||||
val bottomSheetStyle = ThemeUtil.getThemedResourceId(ContextThemeWrapper(requireContext(), themeResId), MaterialR.attr.bottomSheetStyle)
|
||||
backgroundColor = ThemeUtil.getThemedColor(ContextThemeWrapper(requireContext(), bottomSheetStyle), MaterialR.attr.backgroundTint)
|
||||
dialogBackground.fillColor = ColorStateList.valueOf(backgroundColor)
|
||||
|
||||
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (bottomSheet.background !== dialogBackground) {
|
||||
bottomSheet.background = dialogBackground
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
20
core/ui/src/main/java/org/signal/core/ui/WindowExtensions.kt
Normal file
20
core/ui/src/main/java/org/signal/core/ui/WindowExtensions.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui
|
||||
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Initializes screenshot security on the window based on user preferences.
|
||||
*/
|
||||
fun Window.initializeScreenshotSecurity() {
|
||||
if (CoreUiDependencies.isScreenSecurityEnabled) {
|
||||
addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import androidx.window.core.layout.computeWindowSizeClass
|
||||
|
||||
val WindowSizeClass.listPaneDefaultPreferredWidth: Dp get() = if (isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)) 416.dp else 316.dp
|
||||
val WindowSizeClass.horizontalPartitionDefaultSpacerSize: Dp get() = 12.dp
|
||||
val WindowSizeClass.detailPaneMaxContentWidth: Dp get() = 624.dp
|
||||
|
||||
val WindowSizeClass.isWidthCompact
|
||||
get() = !isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
|
||||
|
||||
val WindowSizeClass.isHeightCompact
|
||||
get() = !isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)
|
||||
|
||||
fun Resources.getWindowSizeClass(): WindowSizeClass {
|
||||
return WindowSizeClass.BREAKPOINTS_V1.computeWindowSizeClass(
|
||||
widthDp = displayMetrics.widthPixels / displayMetrics.density,
|
||||
heightDp = displayMetrics.heightPixels / displayMetrics.density
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the UI should display in split-pane mode based on available screen space.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun WindowSizeClass.isSplitPane(
|
||||
forceSplitPane: Boolean = CoreUiDependencies.forceSplitPane
|
||||
): Boolean {
|
||||
if (forceSplitPane) {
|
||||
return true
|
||||
}
|
||||
|
||||
return isAtLeastBreakpoint(
|
||||
widthDpBreakpoint = WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND,
|
||||
heightDpBreakpoint = WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
|
||||
abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
protected open val forceDarkTheme = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val isDark = if (forceDarkTheme) {
|
||||
true
|
||||
} else {
|
||||
LocalConfiguration.current.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
SignalTheme(isDarkMode = isDark) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(cornerRadius.dp, cornerRadius.dp),
|
||||
color = SignalTheme.colors.colorSurface1,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
SheetContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
abstract fun SheetContent()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.R
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.initializeScreenshotSecurity
|
||||
import org.signal.core.ui.util.ThemeUtil
|
||||
|
||||
/**
|
||||
* Generic Compose-based full screen dialog fragment.
|
||||
*
|
||||
* Expects [R.attr.fullScreenDialogStyle] to be defined in your app theme, pointing to a style
|
||||
* suitable for full screen dialogs.
|
||||
*/
|
||||
abstract class ComposeFullScreenDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val fullScreenDialogStyle = ThemeUtil.getThemedResourceId(requireContext(), R.attr.fullScreenDialogStyle)
|
||||
setStyle(STYLE_NO_FRAME, fullScreenDialogStyle)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
SignalTheme {
|
||||
DialogContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireDialog().window?.initializeScreenshotSecurity()
|
||||
}
|
||||
|
||||
@Composable
|
||||
abstract fun DialogContent()
|
||||
}
|
||||
7
core/ui/src/main/res/anim/slide_from_bottom.xml
Normal file
7
core/ui/src/main/res/anim/slide_from_bottom.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<translate
|
||||
android:duration="350"
|
||||
android:fromYDelta="100%"
|
||||
android:toYDelta="0%" />
|
||||
</set>
|
||||
7
core/ui/src/main/res/anim/slide_to_bottom.xml
Normal file
7
core/ui/src/main/res/anim/slide_to_bottom.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<translate
|
||||
android:duration="350"
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="100%" />
|
||||
</set>
|
||||
4
core/ui/src/main/res/values-sw480dp/dimens.xml
Normal file
4
core/ui/src/main/res/values-sw480dp/dimens.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="bottom_sheet_corner_size">32dp</dimen>
|
||||
</resources>
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="theme_type" format="string"/>
|
||||
<attr name="fullScreenDialogStyle" format="reference"/>
|
||||
<attr name="fixedRoundedCornerBottomSheetStyle" format="reference"/>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="gutter">16dp</dimen>
|
||||
<dimen name="bottom_sheet_corner_size">18dp</dimen>
|
||||
</resources>
|
||||
27
core/ui/src/main/res/values/themes.xml
Normal file
27
core/ui/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Widget.CoreUi.FixedRoundedCorners" parent="ThemeOverlay.Material3.BottomSheetDialog">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||
<item name="bottomSheetStyle">@style/Widget.CoreUi.FixedRoundedCorners.BottomSheet</item>
|
||||
<item name="android:windowEnterAnimation">@anim/slide_from_bottom</item>
|
||||
<item name="android:windowExitAnimation">@anim/slide_to_bottom</item>
|
||||
<item name="android:navigationBarColor" tools:targetApi="lollipop">@color/signal_colorSurface1</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.CoreUi.FixedRoundedCorners.BottomSheet" parent="Widget.Material3.BottomSheet.Modal">
|
||||
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Signal.BottomSheet.Rounded</item>
|
||||
<item name="backgroundTint">@color/signal_colorSurface1</item>
|
||||
<item name="android:elevation" tools:ignore="NewApi">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.BottomSheet.Rounded" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopRight">@dimen/bottom_sheet_corner_size</item>
|
||||
<item name="cornerSizeTopLeft">@dimen/bottom_sheet_corner_size</item>
|
||||
<item name="cornerSizeBottomLeft">0dp</item>
|
||||
<item name="cornerSizeBottomRight">0dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -22,5 +22,7 @@ class CoreUiDependenciesRule(
|
||||
val isIncognitoKeyboardEnabled: Boolean
|
||||
): CoreUiDependencies.Provider {
|
||||
override fun provideIsIncognitoKeyboardEnabled(): Boolean = isIncognitoKeyboardEnabled
|
||||
override fun provideIsScreenSecurityEnabled(): Boolean = false
|
||||
override fun provideForceSplitPane(): Boolean = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user