使用 Compose UI 构建应用的最大问题是 Material Compose 的灵活性不足。Material Compose 的可定制性不足以让你在其上构建自己的设计系统,因此你最终只能对其组件进行修改。另一方面,Compose Foundation 又过于“原始”——它有行和列,但没有按钮或底部表单。而且,由于主题设置与 Material 绑定,如果不完全遵循 Material 的设计决策,你甚至无法为你的应用设置主题。
Box(modifier=Modifier.fillMaxSize().background(Brush.linearGradient(listOf(Color(0xFFED213A),Color(0xFF93291E)))),contentAlignment=Alignment.Center){valinteractionSource=remember{MutableInteractionSource()}valisFocusedbyinteractionSource.collectIsFocusedAsState()valisPressedbyinteractionSource.collectIsPressedAsState()valstate=rememberSliderState(initialValue=0.7f)Row(verticalAlignment=Alignment.CenterVertically,horizontalArrangement=Arrangement.spacedBy(12.dp),modifier=Modifier.padding(horizontal=16.dp).widthIn(max=480.dp).fillMaxWidth()){Button(onClick={state.value-=0.1f},modifier=Modifier.shadow(4.dp,CircleShape),shape=CircleShape,backgroundColor=Color.White,contentPadding=PaddingValues(8.dp),){Icon(VolumeDown,"Decrease")}Slider(interactionSource=interactionSource,state=state,modifier=Modifier.weight(1f),track={Box(Modifier.fillMaxWidth().height(8.dp).padding(horizontal=16.dp).clip(RoundedCornerShape(100.dp))){// the 'not yet completed' part of the trackBox(Modifier.fillMaxHeight().fillMaxWidth().background(Color(0xFF93291E)))// the 'completed' part of the trackBox(Modifier.fillMaxHeight().fillMaxWidth(state.value).background(Color.White))}},thumb={valthumbSizebyanimateDpAsState(targetValue=if(isPressed)22.dpelse18.dp)valthumbInteractionSource=remember{MutableInteractionSource()}valisHoveredbythumbInteractionSource.collectIsHoveredAsState()valglowColorbyanimateColorAsState(if(isFocused||isHovered)Color.White.copy(0.33f)elseColor.Transparent)// keep the size fixed to ensure that the resizing animation is always centeredBox(modifier=Modifier.size(36.dp).clip(CircleShape).background(glowColor),contentAlignment=Alignment.Center){Thumb(color=Color.White,modifier=Modifier.size(thumbSize).shadow(4.dp,CircleShape).hoverable(thumbInteractionSource),shape=CircleShape,)}})Button(onClick={state.value+=0.1f},modifier=Modifier.shadow(4.dp,CircleShape),shape=CircleShape,backgroundColor=Color.White,contentPadding=PaddingValues(8.dp),){Icon(VolumeUp,"Increase")}}}
// define your theme propertiesprivatevalcolors=ThemeProperty<Color>("colors")privatevaltypography=ThemeProperty<TextStyle>("typography")privatevalshapes=ThemeProperty<Shape>("shapes")privatevalelevation=ThemeProperty<Dp>("elevation")// define your theme tokens.// those are the potential values of your theme propertiesprivatevalbackground=ThemeToken<Color>("background")privatevalcard=ThemeToken<Color>("surface")privatevalonCard=ThemeToken<Color>("onCard")privatevaloutline=ThemeToken<Color>("outline")privatevalaccent=ThemeToken<Color>("accent")privatevalprimary=ThemeToken<Color>("primary")privatevalonPrimary=ThemeToken<Color>("onPrimary")privatevalonSecondary=ThemeToken<Color>("onSecondary")privatevalsecondary=ThemeToken<Color>("secondary")privatevalsubtle=ThemeToken<Dp>("subtle")privatevaltitleMedium=ThemeToken<TextStyle>("titleMedium")privatevalbodyMedium=ThemeToken<TextStyle>("bodyMedium")privatevalcardShape=ThemeToken<Shape>("cardShape")privatevalalbumCoverShape=ThemeToken<Shape>("albumCoverShape")privatevalbuttonShape=ThemeToken<Shape>("buttonShape")// create your Compose Theme and assign values to each tokenprivatevalLightTheme=buildTheme{name="LightTheme"properties[colors]=mapOf(accenttoColor(0xFF3B82F6),cardtoColor.White,onCardtoColor(0xFF1E293B),outlinetoColor(0xFFE2E8F0),primarytoColor(0xFF2563EB),onPrimarytoColor.White,secondarytoColor(0xFFE2E8F0),onSecondarytoColor(0xFF64748B),backgroundtoColor(0xFFF8F9FA),)properties[typography]=mapOf(titleMediumtoTextStyle(fontSize=18.sp,fontWeight=FontWeight.SemiBold,fontFamily=loadInterFont(),),bodyMediumtoTextStyle(fontSize=14.sp,fontWeight=FontWeight.Normal,fontFamily=loadInterFont(),))properties[shapes]=mapOf(cardShapetoRoundedCornerShape(16.dp),albumCoverShapetoRoundedCornerShape(12.dp),buttonShapetoCircleShape)properties[elevation]=mapOf(subtleto8.dp)}
@ComposablefunMusicPlayerCard(modifier:Modifier=Modifier){valsliderState=rememberSliderState(initialValue=0.3f)Box(modifier=modifier.outline(1.dp,Theme[colors][outline],Theme[shapes][cardShape]).shadow(Theme[elevation][subtle],Theme[shapes][cardShape]).background(Theme[colors][card],Theme[shapes][cardShape]).padding(24.dp)){ProvideContentColor(Theme[colors][onCard]){Column(verticalArrangement=Arrangement.spacedBy(20.dp)){Row(verticalAlignment=Alignment.CenterVertically,horizontalArrangement=Arrangement.spacedBy(16.dp)){Image(painter=painterResource(Res.drawable.just_hoist_it_cover),modifier=Modifier.clip(Theme[shapes][albumCoverShape]).background(Theme[colors][primary]).size(80.dp),contentDescription="Album Cover",contentScale=ContentScale.Crop)Column(modifier=Modifier.weight(1f)){Text("Just hoist it!",style=Theme[typography][titleMedium])Spacer(Modifier.height(4.dp))Text("The Deprecated",style=Theme[typography][bodyMedium],color=Theme[colors][onSecondary])}}Slider(state=sliderState,modifier=Modifier.fillMaxWidth(),track={Box(Modifier.fillMaxWidth().height(4.dp).clip(RoundedCornerShape(2.dp))){// the empty part of the trackBox(Modifier.fillMaxSize().background(Theme[colors][secondary]))// the filled part of the trackBox(Modifier.fillMaxWidth(sliderState.value).fillMaxSize().background(Theme[colors][accent]))}},thumb={Thumb(color=Theme[colors][accent],modifier=Modifier.size(16.dp),shape=Theme[shapes][buttonShape])})Row(modifier=Modifier.fillMaxWidth(),horizontalArrangement=Arrangement.SpaceEvenly,verticalAlignment=Alignment.CenterVertically){Button(onClick={},contentPadding=PaddingValues(12.dp),shape=Theme[shapes][buttonShape]){Icon(imageVector=Lucide.SkipBack,contentDescription="Previous",modifier=Modifier.size(20.dp))}Button(onClick={},backgroundColor=Theme[colors][primary],contentColor=Theme[colors][onPrimary],contentPadding=PaddingValues(16.dp),shape=Theme[shapes][buttonShape]){Icon(imageVector=Lucide.Pause,contentDescription="Pause",modifier=Modifier.size(24.dp))}Button(onClick={},contentPadding=PaddingValues(12.dp),shape=Theme[shapes][buttonShape]){Icon(imageVector=Lucide.SkipForward,contentDescription="Next",modifier=Modifier.size(20.dp))}}}}}}
轮廓修饰符
最后但同样重要的是,Compose Unstyled 引入了一些 Compose Foundation 中缺少的样式 Modifier,但这些修饰符对于构建视觉丰富的界面必不可少:
轮廓
与 Compose Foundation 的 border() 修饰符不同,此修饰符不会影响布局。它还会在组件周围而不是内部进行绘制。当你需要一个与阴影完美融合的半透明轮廓时,这个功能非常方便: