Implement Basic Controls(Toggle Switch, Checkbox and Radio Button) (#214)

* Implement Basic Controls(Toggle Switch, Checkbox and Radio Button)
* Remove appcompat Dependency from Controls module
* Resolving comments
This commit is contained in:
NamanPandey 2022-06-28 15:36:29 +05:30 коммит произвёл GitHub
Родитель 18d9e0432b
Коммит ef6a2db75d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 902 добавлений и 48 удалений

Просмотреть файл

@ -34,6 +34,7 @@
<activity android:name="com.microsoft.fluentuidemo.demos.AvatarGroupViewActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.BasicInputsActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.V2BasicInputsActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.V2BasicControlsActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.BottomNavigationActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.BottomSheetActivity" />
<activity android:name="com.microsoft.fluentuidemo.demos.CalendarViewActivity" />

Просмотреть файл

@ -12,9 +12,9 @@ open class MyButtonTokens : ButtonTokens() {
@Composable
override fun fixedHeight(buttonInfo: ButtonInfo): Dp {
return when (buttonInfo.size) {
ButtonSize.Small -> 50.dp
ButtonSize.Medium -> 60.dp
ButtonSize.Large -> 100.dp
ButtonSize.Small -> 40.dp
ButtonSize.Medium -> 48.dp
ButtonSize.Large -> 60.dp
}
}
}

Просмотреть файл

@ -15,6 +15,7 @@ const val AVATAR_VIEW = "AvatarView"
const val AVATAR_GROUP_VIEW = "AvatarGroupView"
const val BASIC_INPUTS = "Basic Inputs"
const val V2BASIC_INPUTS = "V2 Basic Inputs"
const val V2BASIC_CONTROLS = "V2 Basic Controls"
const val BOTTOM_NAVIGATION = "BottomNavigation"
const val BOTTOM_SHEET = "BottomSheet"
const val CALENDAR_VIEW = "CalendarView"
@ -42,6 +43,7 @@ val DEMOS = arrayListOf(
Demo(AVATAR_GROUP_VIEW, AvatarGroupViewActivity::class),
Demo(BASIC_INPUTS, BasicInputsActivity::class),
Demo(V2BASIC_INPUTS, V2BasicInputsActivity::class),
Demo(V2BASIC_CONTROLS, V2BasicControlsActivity::class),
Demo(BOTTOM_NAVIGATION, BottomNavigationActivity::class),
Demo(BOTTOM_SHEET, BottomSheetActivity::class),
Demo(CALENDAR_VIEW, CalendarViewActivity::class),

Просмотреть файл

@ -0,0 +1,154 @@
package com.microsoft.fluentuidemo.demos
import android.os.Bundle
import android.widget.Toast
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.theme.token.MyAliasTokens
import com.example.theme.token.MyGlobalTokens
import com.microsoft.fluentui.theme.AppThemeController
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.ThemeMode
import com.microsoft.fluentui.theme.token.AliasTokens
import com.microsoft.fluentui.theme.token.GlobalTokens
import com.microsoft.fluentui.controls.CheckBox
import com.microsoft.fluentui.controls.RadioButton
import com.microsoft.fluentui.controls.ToggleSwitch
import com.microsoft.fluentuidemo.DemoActivity
import com.microsoft.fluentuidemo.R
class V2BasicControlsActivity : DemoActivity() {
override val contentLayoutId: Int
get() = R.layout.v2_activity_compose
override val contentNeedsScrollableContainer: Boolean
get() = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = this
val compose_here = findViewById<ComposeView>(R.id.compose_here)
compose_here.setContent {
val globalTokens: GlobalTokens by AppThemeController.observeGlobalToken(initial = GlobalTokens())
val aliasTokens: AliasTokens by AppThemeController.observeAliasToken(initial = AliasTokens())
FluentTheme(globalTokens = globalTokens, aliasTokens = aliasTokens) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(16.dp)) {
var checked by remember { mutableStateOf(globalTokens !is MyGlobalTokens) }
var enabled by remember { mutableStateOf(false) }
val themes = listOf("Theme 1", "Theme 2")
val selectedOption = remember { mutableStateOf(themes[0]) }
Row(horizontalArrangement = Arrangement.spacedBy(30.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) {
Text(text = "Toggle Switch enable",
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1F),
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode = ThemeMode.Auto))
ToggleSwitch(true, enabled, {
enabled = it
Toast.makeText(context, "Switch 1 Toggled", Toast.LENGTH_SHORT).show()
})
}
Divider()
Row(horizontalArrangement = Arrangement.spacedBy(30.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) {
Text(text = "Toggle Global/Alias Theme",
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1F),
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode = ThemeMode.Auto))
ToggleSwitch(enabled, checked, {
checked = it
if (checked) {
AppThemeController.updateGlobalTokens(GlobalTokens())
AppThemeController.updateAliasTokens(AliasTokens())
selectedOption.value = themes[0]
} else {
AppThemeController.updateGlobalTokens(MyGlobalTokens())
AppThemeController.updateAliasTokens(MyAliasTokens(MyGlobalTokens()))
selectedOption.value = themes[1]
}
Toast.makeText(context, "Switch 2 Toggled", Toast.LENGTH_SHORT).show()
})
}
Row(horizontalArrangement = Arrangement.spacedBy(30.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()) {
Text(text = "Toggle Global/Alias Theme",
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1F),
color = AppThemeController.aliasTokens.value!!.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode = ThemeMode.Auto))
CheckBox(enabled = enabled, checked = !checked, onCheckedChanged = {
checked = !it
if (checked) {
AppThemeController.updateAliasTokens(AliasTokens())
AppThemeController.updateGlobalTokens(GlobalTokens())
selectedOption.value = themes[0]
} else {
AppThemeController.updateAliasTokens(MyAliasTokens(MyGlobalTokens()))
AppThemeController.updateGlobalTokens(MyGlobalTokens())
selectedOption.value = themes[1]
}
})
}
themes.forEach { theme ->
Row(horizontalArrangement = Arrangement.spacedBy(30.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = (theme == selectedOption.value),
onClick = { },
role = Role.RadioButton,
interactionSource = MutableInteractionSource(),
indication = null
)) {
Text(text = theme,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1F),
color = AppThemeController.aliasTokens.value!!.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode = ThemeMode.Auto))
RadioButton(enabled = enabled,
selected = (selectedOption.value == theme),
onClick = {
selectedOption.value = theme
if (theme == "Theme 1") {
AppThemeController.updateAliasTokens(AliasTokens())
AppThemeController.updateGlobalTokens(GlobalTokens())
checked = true
} else {
AppThemeController.updateAliasTokens(MyAliasTokens(MyGlobalTokens()))
AppThemeController.updateGlobalTokens(MyGlobalTokens())
checked = false
}
Toast.makeText(context, "Radio Button: ${theme} selected", Toast.LENGTH_SHORT).show()
}
)
}
}
}
}
}
}
}

Просмотреть файл

@ -24,10 +24,11 @@ import androidx.compose.ui.unit.dp
import com.example.theme.token.MyAliasTokens
import com.example.theme.token.MyButtonTokens
import com.example.theme.token.MyGlobalTokens
import com.microsoft.fluentui.button.Button
import com.microsoft.fluentui.button.FloatingActionButton
import com.microsoft.fluentui.controls.Button
import com.microsoft.fluentui.controls.FloatingActionButton
import com.microsoft.fluentui.theme.AppThemeController
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.FluentTheme.themeMode
import com.microsoft.fluentui.theme.ThemeMode
import com.microsoft.fluentui.theme.token.AliasTokens
import com.microsoft.fluentui.theme.token.ControlTokens
@ -61,7 +62,8 @@ class V2BasicInputsActivity : DemoActivity() {
) {
FluentTheme(globalTokens = globalTokens, aliasTokens = aliasTokens, controlTokens = controlTokens) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Button to update Theme via Global & Alias token")
Text("Button to update Theme via Global & Alias token",
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode))
Row(
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
@ -71,9 +73,9 @@ class V2BasicInputsActivity : DemoActivity() {
style = ButtonStyle.OutlinedButton,
size = ButtonSize.Medium,
onClick = {
AppThemeController.onGlobalChanged(GlobalTokens())
AppThemeController.onAliasChanged(AliasTokens())
AppThemeController.onControlChanged(ControlTokens().updateTokens(ControlTokens.ControlType.Button, ButtonTokens()))
AppThemeController.updateGlobalTokens(GlobalTokens())
AppThemeController.updateAliasTokens(AliasTokens())
AppThemeController.updateControlTokens(ControlTokens().updateTokens(ControlTokens.ControlType.Button, ButtonTokens()))
},
text = "Set Default Theme"
)
@ -82,9 +84,9 @@ class V2BasicInputsActivity : DemoActivity() {
style = ButtonStyle.OutlinedButton,
size = ButtonSize.Medium,
onClick = {
AppThemeController.onGlobalChanged(MyGlobalTokens())
AppThemeController.onAliasChanged(MyAliasTokens(MyGlobalTokens()))
AppThemeController.onControlChanged(ControlTokens().updateTokens(ControlTokens.ControlType.Button, MyButtonTokens()))
AppThemeController.updateGlobalTokens(MyGlobalTokens())
AppThemeController.updateAliasTokens(MyAliasTokens(MyGlobalTokens()))
AppThemeController.updateControlTokens(ControlTokens().updateTokens(ControlTokens.ControlType.Button, MyButtonTokens()))
},
text = "Set New Theme"
)
@ -96,14 +98,16 @@ class V2BasicInputsActivity : DemoActivity() {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
item {
Text("Default Button from provided base token & Auto theme")
Text("Default Button from provided base token & Auto theme",
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode))
FluentTheme {
CreateButtons()
}
}
item {
Text("Button with Selected Theme and Colorful mode")
Text("Button with Selected Theme and Colorful mode",
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode))
FluentTheme(
globalTokens = globalTokens,
aliasTokens = aliasTokens,
@ -124,11 +128,13 @@ class V2BasicInputsActivity : DemoActivity() {
}
}
item {
FluentTheme(globalTokens = globalTokens, aliasTokens = aliasTokens, controlTokens = controlTokens) {
Text("Button with selected theme, auto mode and default control token")
FluentTheme(globalTokens = globalTokens, aliasTokens = aliasTokens) {
Text("Button with selected theme, auto mode and default control token",
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode))
CreateButtons()
Text("Button with selected theme, auto mode and overridden control token")
Text("Button with selected theme, auto mode and overridden control token",
color = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.Foreground1].value(themeMode))
CreateButtons(MyButtonTokens())
}
}

Просмотреть файл

@ -31,6 +31,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api project(':fluentui_calendar')
api project(':fluentui_ccb')
api project(':fluentui_controls')
api project(':fluentui_core')
api project(':fluentui_drawer')
api project(':fluentui_listitem')

Просмотреть файл

@ -12,6 +12,7 @@
* fluentui_drawer and FluentUI should increment their respective version ids
*/
project.ext.fluentui_calendar_versionid='0.0.23'
project.ext.fluentui_controls_versionid='0.1.0'
project.ext.fluentui_core_versionid='0.1.0'
project.ext.fluentui_listitem_versionid='0.0.23'
project.ext.fluentui_tablayout_versionid='0.0.23'
@ -26,6 +27,7 @@ project.ext.fluentui_persona_versionid='0.0.23'
project.ext.fluentui_progress_versionid='0.0.23'
project.ext.FluentUI_versionid='0.1.0'
project.ext.fluentui_calendar_version_code=23
project.ext.fluentui_controls_version_code=1
project.ext.fluentui_core_version_code=23
project.ext.fluentui_listitem_version_code=23
project.ext.fluentui_tablayout_version_code=23

1
fluentui_controls/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
/build

Просмотреть файл

@ -0,0 +1,71 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply from: '../config.gradle'
apply from: '../publish.gradle'
android {
compileSdkVersion constants.compileSdkVersion
defaultConfig {
minSdkVersion constants.minSdkVersion
targetSdkVersion constants.targetSdkVersion
versionCode project.ext.fluentui_controls_version_code
versionName project.ext.fluentui_controls_versionid
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
productFlavors {
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion composeVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':fluentui_core')
implementation 'androidx.core:core-ktx:1.7.0'
implementation("androidx.compose.foundation:foundation:$composeVersion")
implementation("androidx.compose.material:material:$composeVersion")
implementation("androidx.compose.runtime:runtime:$composeVersion")
implementation("androidx.compose.ui:ui:$composeVersion")
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "sources"
}
project.afterEvaluate {
project.ext.publishingFunc('fluentui_others')
}

Просмотреть файл

21
fluentui_controls/proguard-rules.pro поставляемый Normal file
Просмотреть файл

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Просмотреть файл

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Microsoft Corporation. All rights reserved.
~ Licensed under the MIT License.
-->
<manifest
package="com.microsoft.fluentui.controls" />

Просмотреть файл

@ -1,4 +1,4 @@
package com.microsoft.fluentui.button
package com.microsoft.fluentui.controls
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource

Просмотреть файл

@ -0,0 +1,117 @@
package com.microsoft.fluentui.controls
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.controls.backgroundColor
import com.microsoft.fluentui.controls.borderStroke
import com.microsoft.fluentui.controls.iconColor
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens.ControlType
import com.microsoft.fluentui.theme.token.controlTokens.CheckBoxInfo
import com.microsoft.fluentui.theme.token.controlTokens.CheckBoxTokens
val LocalCheckBoxTokens = compositionLocalOf { CheckBoxTokens() }
val LocalCheckBoxInfo = compositionLocalOf { CheckBoxInfo() }
@Composable
fun CheckBox(enabled: Boolean = true,
checked: Boolean = false,
onCheckedChanged: (Boolean) -> Unit?,
modifier: Modifier = Modifier,
checkBoxToken: CheckBoxTokens? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }) {
val token = checkBoxToken
?: FluentTheme.controlTokens.tokens[ControlType.CheckBox] as CheckBoxTokens
CompositionLocalProvider(
LocalCheckBoxTokens provides token,
LocalCheckBoxInfo provides CheckBoxInfo(checked)
) {
val toggleModifier =
if (onCheckedChanged != null) {
Modifier.triStateToggleable(
state = ToggleableState(checked),
enabled = enabled,
onClick = { onCheckedChanged(!checked) },
role = Role.Checkbox,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = 24.dp
)
)
} else {
Modifier
}
val backgroundColor: Color = backgroundColor(getCheckBoxToken(), getCheckBoxInfo(),
enabled, interactionSource)
val iconColor: Color = iconColor(getCheckBoxToken(), getCheckBoxInfo(),
enabled, interactionSource)
val shape: Shape = RoundedCornerShape(getCheckBoxToken().fixedBorderRadius)
val borders: List<BorderStroke> =
borderStroke(getCheckBoxToken(), getCheckBoxInfo(), enabled, interactionSource)
var borderModifier: Modifier = Modifier
var borderWidth = 0.dp
for (border in borders) {
borderWidth += border.width
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
}
Box(modifier = Modifier.indication(interactionSource, null),
contentAlignment = Alignment.Center) {
Spacer(modifier = Modifier
.size(getCheckBoxToken().fixedSize)
.clip(shape)
.background(backgroundColor)
.then(borderModifier)
.then(toggleModifier))
AnimatedVisibility(checked, enter = fadeIn(), exit = fadeOut()) {
Icon(Icons.Filled.Done,
"Done",
modifier = Modifier.size(getCheckBoxToken().fixedIconSize),
tint = iconColor)
}
}
}
}
@Composable
fun getCheckBoxToken(): CheckBoxTokens {
return LocalCheckBoxTokens.current
}
@Composable
fun getCheckBoxInfo(): CheckBoxInfo {
return LocalCheckBoxInfo.current
}

Просмотреть файл

@ -1,4 +1,4 @@
package com.microsoft.fluentui.button
package com.microsoft.fluentui.controls
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.*

Просмотреть файл

@ -0,0 +1,94 @@
package com.microsoft.fluentui.controls
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
import com.microsoft.fluentui.theme.token.controlTokens.RadioButtonInfo
import com.microsoft.fluentui.theme.token.controlTokens.RadioButtonTokens
val LocalRadioButtonTokens = compositionLocalOf { RadioButtonTokens() }
val LocalRadioButtonInfo = compositionLocalOf { RadioButtonInfo() }
@Composable
fun RadioButton(enabled: Boolean = true,
selected: Boolean = false,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
radioButtonToken: RadioButtonTokens? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val token = radioButtonToken
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.RadioButton] as RadioButtonTokens
CompositionLocalProvider(
LocalRadioButtonTokens provides token,
LocalRadioButtonInfo provides RadioButtonInfo(selected)
) {
val dotRadius = animateDpAsState(
targetValue = if (selected) getRadioButtonTokens().innerCircleRadius else 0.dp,
animationSpec = tween(durationMillis = 100)
)
val selectableModifier = if (onClick != null) {
modifier.selectable(selected = selected,
enabled = enabled,
onClick = onClick,
role = Role.RadioButton,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = 24.dp
)
)
} else {
modifier
}
val outerStrokeColor = backgroundColor(getRadioButtonTokens(), getRadioButtonInfo(),
enabled, interactionSource)
val innerColor = iconColor(getRadioButtonTokens(), getRadioButtonInfo(),
enabled, interactionSource)
val outerRadius = getRadioButtonTokens().outerCircleRadius
val strokeWidth = getRadioButtonTokens().strokeWidthInwards
Canvas(modifier = Modifier
.then(selectableModifier)
.size(24.dp)
.wrapContentSize(Alignment.Center)) {
drawCircle(outerStrokeColor, (outerRadius - (strokeWidth / 2)).toPx(), style = Stroke(1.5.dp.toPx()))
if (dotRadius.value > 0.dp) {
drawCircle(innerColor, (dotRadius.value).toPx(), style = Fill)
}
}
}
}
@Composable
fun getRadioButtonTokens(): RadioButtonTokens {
return LocalRadioButtonTokens.current
}
@Composable
fun getRadioButtonInfo(): RadioButtonInfo {
return LocalRadioButtonInfo.current
}

Просмотреть файл

@ -0,0 +1,137 @@
package com.microsoft.fluentui.controls
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material.swipeable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.controls.backgroundColor
import com.microsoft.fluentui.controls.iconColor
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens.ControlType
import com.microsoft.fluentui.theme.token.controlTokens.ToggleSwitchInfo
import com.microsoft.fluentui.theme.token.controlTokens.ToggleSwitchTokens
import kotlin.math.roundToInt
val LocalToggleSwitchTokens = compositionLocalOf { ToggleSwitchTokens() }
val LocalToggleSwitchInfo = compositionLocalOf { ToggleSwitchInfo() }
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ToggleSwitch(enabledSwitch: Boolean = true,
checkedState: Boolean = false,
onValueChange: ((Boolean) -> Unit)? = null,
switchTokens: ToggleSwitchTokens? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val token = switchTokens
?: FluentTheme.controlTokens.tokens[ControlType.ToggleSwitch] as ToggleSwitchTokens
CompositionLocalProvider(
LocalToggleSwitchTokens provides token,
LocalToggleSwitchInfo provides ToggleSwitchInfo(checkedState)
) {
val backgroundColor: Color = backgroundColor(getToggleSwitchToken(), getToggleSwitchInfo(),
enabledSwitch, interactionSource)
val foregroundColor: Color = iconColor(getToggleSwitchToken(), getToggleSwitchInfo(),
enabledSwitch, interactionSource)
val padding: Dp = getToggleSwitchToken().paddingTrack
// Swipe Logic
val knobMovementWidth = 23.dp
val minBound = with(LocalDensity.current) { padding.toPx() }
val maxBound = with(LocalDensity.current) { knobMovementWidth.toPx() }
val AnimationSpec = TweenSpec<Float>(durationMillis = 100)
val swipeState = rememberSwipeableState(checkedState, AnimationSpec, confirmStateChange = { true })
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val forceAnimationCheck = remember { mutableStateOf(false) }
LaunchedEffect(checkedState, forceAnimationCheck.value) {
if (checkedState != swipeState.currentValue) {
swipeState.animateTo(checkedState)
}
}
DisposableEffect(swipeState.currentValue) {
if (checkedState != swipeState.currentValue) {
onValueChange?.invoke(swipeState.currentValue)
forceAnimationCheck.value = !forceAnimationCheck.value
}
onDispose { }
}
// Toggle Logic
val toggleModifier =
if (onValueChange != null) {
Modifier.toggleable(value = getToggleSwitchInfo().checked,
enabled = enabledSwitch,
role = Role.Switch,
onValueChange = onValueChange,
interactionSource = interactionSource,
indication = null)
} else
Modifier
// UI Implementation
Box(modifier = Modifier
.then(toggleModifier)
.swipeable(
state = swipeState,
anchors = mapOf(minBound to false, maxBound to true),
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Horizontal,
enabled = enabledSwitch && onValueChange != null,
reverseDirection = isRtl,
interactionSource = interactionSource,
resistance = null
), contentAlignment = Alignment.CenterStart) {
Box(modifier = Modifier
.width(getToggleSwitchToken().fixedTrackWidth)
.height(getToggleSwitchToken().fixedTrackHeight)
.clip(CircleShape)
.background(backgroundColor))
Spacer(modifier = Modifier
.offset { IntOffset(swipeState.offset.value.roundToInt(), 0) }
.indication(
interactionSource = interactionSource,
indication = rememberRipple(false,
getToggleSwitchToken().knobRippleRadius)
)
.size(getToggleSwitchToken().fixedKnobDiameter)
.clip(CircleShape)
.background(foregroundColor))
}
}
}
@Composable
fun getToggleSwitchToken(): ToggleSwitchTokens {
return LocalToggleSwitchTokens.current
}
@Composable
fun getToggleSwitchInfo(): ToggleSwitchInfo {
return LocalToggleSwitchInfo.current
}

Просмотреть файл

@ -1,4 +1,4 @@
package com.microsoft.fluentui.button
package com.microsoft.fluentui.controls
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.InteractionSource
@ -12,10 +12,7 @@ import com.microsoft.fluentui.theme.token.ControlInfo
import com.microsoft.fluentui.theme.token.ControlToken
import com.microsoft.fluentui.theme.token.StateBorderStroke
import com.microsoft.fluentui.theme.token.StateColor
import com.microsoft.fluentui.theme.token.controlTokens.ButtonInfo
import com.microsoft.fluentui.theme.token.controlTokens.ButtonTokens
import com.microsoft.fluentui.theme.token.controlTokens.FABInfo
import com.microsoft.fluentui.theme.token.controlTokens.FABTokens
import com.microsoft.fluentui.theme.token.controlTokens.*
import java.security.InvalidParameterException
@Composable
@ -53,6 +50,9 @@ fun backgroundColor(
when (tokens) {
is ButtonTokens -> tokens.backgroundColor(info as ButtonInfo)
is FABTokens -> tokens.backgroundColor(info as FABInfo)
is ToggleSwitchTokens -> tokens.TrackColor(info as ToggleSwitchInfo)
is CheckBoxTokens -> tokens.backgroundColor(info as CheckBoxInfo)
is RadioButtonTokens -> tokens.backgroundColor(info as RadioButtonInfo)
else -> throw InvalidParameterException()
}
@ -70,6 +70,9 @@ fun iconColor(
when (tokens) {
is ButtonTokens -> tokens.iconColor(info as ButtonInfo)
is FABTokens -> tokens.iconColor(info as FABInfo)
is ToggleSwitchTokens -> tokens.KnobColor(info as ToggleSwitchInfo)
is CheckBoxTokens -> tokens.iconColor(info as CheckBoxInfo)
is RadioButtonTokens -> tokens.iconColor(info as RadioButtonInfo)
else -> throw InvalidParameterException()
}
@ -104,6 +107,7 @@ fun borderStroke(
when (tokens) {
is ButtonTokens -> tokens.borderStroke(info as ButtonInfo)
is FABTokens -> tokens.borderStroke(info as FABInfo)
is CheckBoxTokens -> tokens.borderStroke(info as CheckBoxInfo)
else -> throw InvalidParameterException()
}

Просмотреть файл

@ -63,15 +63,15 @@ object AppThemeController : ViewModel() {
var aliasTokens: MutableLiveData<AliasTokens> = MutableLiveData(AliasTokens())
var controlTokens: MutableLiveData<ControlTokens> = MutableLiveData(ControlTokens())
fun onGlobalChanged(overrideGlobalTokens: GlobalTokens) {
fun updateGlobalTokens(overrideGlobalTokens: GlobalTokens) {
globalTokens.value = overrideGlobalTokens
}
fun onAliasChanged(overrideAliasTokens: AliasTokens) {
fun updateAliasTokens(overrideAliasTokens: AliasTokens) {
aliasTokens.value = overrideAliasTokens
}
fun onControlChanged(overrideControlTokens: ControlTokens) {
fun updateControlTokens(overrideControlTokens: ControlTokens) {
controlTokens.value = overrideControlTokens
}

Просмотреть файл

@ -6,8 +6,7 @@
package com.microsoft.fluentui.theme.token
import androidx.compose.runtime.compositionLocalOf
import com.microsoft.fluentui.theme.token.controlTokens.ButtonTokens
import com.microsoft.fluentui.theme.token.controlTokens.FABTokens
import com.microsoft.fluentui.theme.token.controlTokens.*
interface ControlInfo
@ -18,6 +17,9 @@ class ControlTokens {
enum class ControlType {
Button,
FloatingActionButton,
ToggleSwitch,
CheckBox,
RadioButton
}
val tokens: TokenSet<ControlType, ControlToken> by lazy {
@ -25,6 +27,9 @@ class ControlTokens {
when (token) {
ControlType.Button -> ButtonTokens()
ControlType.FloatingActionButton -> FABTokens()
ControlType.ToggleSwitch -> ToggleSwitchTokens()
ControlType.CheckBox -> CheckBoxTokens()
ControlType.RadioButton -> RadioButtonTokens()
}
}
}

Просмотреть файл

@ -0,0 +1,95 @@
package com.microsoft.fluentui.theme.token.controlTokens
import android.os.Parcelable
import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.*
import kotlinx.parcelize.Parcelize
data class CheckBoxInfo(
val checked: Boolean = false,
) : ControlInfo
@Parcelize
open class CheckBoxTokens : ControlToken, Parcelable {
companion object {
const val Type: String = "Checkbox"
}
val fixedSize: Dp = 20.dp
val fixedIconSize: Dp = 12.dp
val fixedBorderRadius: Dp = 4.dp
@Composable
open fun backgroundColor(checkBoxInfo: CheckBoxInfo): StateColor {
return when (checkBoxInfo.checked) {
true -> StateColor(
rest = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = FluentTheme.themeMode
),
pressed = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = FluentTheme.themeMode
),
disabled = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundDisabled].value(
themeMode = FluentTheme.themeMode
)
)
false -> StateColor()
}
}
@Composable
open fun iconColor(checkBoxInfo: CheckBoxInfo): StateColor {
return when (checkBoxInfo.checked) {
true -> StateColor(
rest = FluentTheme.aliasToken.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = FluentTheme.themeMode
),
pressed = FluentTheme.aliasToken.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = FluentTheme.themeMode
),
disabled = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInvertedDisabled].value(
themeMode = FluentTheme.themeMode
)
)
false -> StateColor()
}
}
@Composable
open fun borderStroke(checkBoxInfo: CheckBoxInfo): StateBorderStroke {
return when (checkBoxInfo.checked) {
true -> StateBorderStroke()
false -> StateBorderStroke(
rest = listOf(
BorderStroke(
1.5.dp,
FluentTheme.aliasToken.neutralStrokeColor[AliasTokens.NeutralStrokeColorTokens.StrokeAccessible].value(
themeMode = FluentTheme.themeMode
)
)
),
pressed = listOf(
BorderStroke(
1.5.dp,
FluentTheme.aliasToken.neutralStrokeColor[AliasTokens.NeutralStrokeColorTokens.StrokeAccessible].value(
themeMode = FluentTheme.themeMode
)
)
),
disabled = listOf(
BorderStroke(
1.5.dp,
FluentTheme.aliasToken.neutralStrokeColor[AliasTokens.NeutralStrokeColorTokens.StrokeDisabled].value(
themeMode = FluentTheme.themeMode
)
)
)
)
}
}
}

Просмотреть файл

@ -0,0 +1,67 @@
package com.microsoft.fluentui.theme.token.controlTokens
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.AliasTokens
import com.microsoft.fluentui.theme.token.ControlInfo
import com.microsoft.fluentui.theme.token.ControlToken
import com.microsoft.fluentui.theme.token.StateColor
import kotlinx.parcelize.Parcelize
data class RadioButtonInfo(
val selected: Boolean = false,
) : ControlInfo
@Parcelize
open class RadioButtonTokens : ControlToken, Parcelable {
companion object {
const val Type: String = "Checkbox"
}
open var innerCircleRadius = 5.dp
open var outerCircleRadius = 10.dp
open var strokeWidthInwards = 1.5.dp
@Composable
open fun backgroundColor(radioButtonInfo: RadioButtonInfo): StateColor {
return when (radioButtonInfo.selected) {
true -> StateColor(
rest = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = FluentTheme.themeMode
),
pressed = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = FluentTheme.themeMode
),
disabled = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundDisabled].value(
themeMode = FluentTheme.themeMode
)
)
false -> StateColor(
rest = FluentTheme.aliasToken.neutralStrokeColor[AliasTokens.NeutralStrokeColorTokens.StrokeAccessible].value(
themeMode = FluentTheme.themeMode
),
disabled = FluentTheme.aliasToken.neutralStrokeColor[AliasTokens.NeutralStrokeColorTokens.StrokeDisabled].value(
themeMode = FluentTheme.themeMode
)
)
}
}
@Composable
open fun iconColor(radioButtonInfo: RadioButtonInfo): StateColor {
return when (radioButtonInfo.selected) {
true -> StateColor(
rest = FluentTheme.aliasToken.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
themeMode = FluentTheme.themeMode
),
disabled = FluentTheme.aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundDisabled].value(
themeMode = FluentTheme.themeMode
)
)
false -> StateColor()
}
}
}

Просмотреть файл

@ -0,0 +1,86 @@
package com.microsoft.fluentui.theme.token.controlTokens
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.theme.FluentTheme.aliasToken
import com.microsoft.fluentui.theme.FluentTheme.themeMode
import com.microsoft.fluentui.theme.token.AliasTokens
import com.microsoft.fluentui.theme.token.ControlInfo
import com.microsoft.fluentui.theme.token.ControlToken
import com.microsoft.fluentui.theme.token.StateColor
import kotlinx.parcelize.Parcelize
data class ToggleSwitchInfo(
val checked: Boolean = true,
) : ControlInfo
@Parcelize
open class ToggleSwitchTokens : ControlToken, Parcelable {
companion object {
const val Type: String = "ToggleSwitch"
}
@Composable
open fun TrackColor(switchInfo: ToggleSwitchInfo): StateColor {
return when (switchInfo.checked) {
true -> StateColor(
rest = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = themeMode
),
pressed = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
themeMode = themeMode
),
disabled = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundDisabled].value(
themeMode = themeMode
)
)
false -> StateColor(
rest = aliasToken.neutralBackgroundColor[AliasTokens.NeutralBackgroundColorTokens.Background5].value(
themeMode = themeMode
),
pressed = aliasToken.neutralBackgroundColor[AliasTokens.NeutralBackgroundColorTokens.Background5].value(
themeMode = themeMode
),
disabled = aliasToken.neutralBackgroundColor[AliasTokens.NeutralBackgroundColorTokens.Background5].value(
themeMode = themeMode
)
)
}
}
@Composable
open fun KnobColor(switchInfo: ToggleSwitchInfo): StateColor {
return when (switchInfo.checked) {
true -> StateColor(
rest = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInverted].value(
themeMode = themeMode
),
pressed = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInverted].value(
themeMode = themeMode
),
disabled = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInvertedDisabled].value(
themeMode = themeMode
)
)
false -> StateColor(
rest = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInverted].value(
themeMode = themeMode
),
pressed = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInverted].value(
themeMode = themeMode
),
disabled = aliasToken.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundInvertedDisabled].value(
themeMode = themeMode
)
)
}
}
open val fixedTrackHeight = 32.dp
open val fixedTrackWidth = 52.dp
open val fixedKnobDiameter = 26.dp
open val knobRippleRadius = 24.dp
open val paddingTrack = 3.dp
}

Просмотреть файл

@ -34,20 +34,6 @@ android {
}
productFlavors {
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion composeVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
gradle.taskGraph.whenReady { taskGraph ->
@ -71,11 +57,6 @@ dependencies {
implementation "com.splitwise:tokenautocomplete:$tokenautocompleteVersion"
implementation "com.microsoft.device:dualscreen-layout:$duoVersion"
implementation("androidx.compose.foundation:foundation:$composeVersion")
implementation("androidx.compose.material:material:$composeVersion")
implementation("androidx.compose.runtime:runtime:$composeVersion")
implementation("androidx.compose.ui:ui:$composeVersion")
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"

Просмотреть файл

@ -13,3 +13,4 @@ include ':fluentui_tablayout'
include ':fluentui_others'
include ':FluentUI'
include ':fluentui_ccb'
include ':fluentui_controls'