Jetpack Compose初体验--(布局、动画等)

概览

Jetpack Compose 是用来构建Android界面的新款工具包,前段时间beta版本刚发布的时候就已经心痒难耐的想要尝试了,最近轻松点了赶忙尝试一波。java

之前咱们都是经过xml布局,经过findViewById()找到控件以后手动给控件赋值。这样的缺点首先是解析xml生成view对象须要经过反射浪费性能,而后是手动给控件赋值容易出错,好比过个地方控制一个view的显示和隐藏,随着控制它的地方愈来愈多,维护起来也愈来愈复杂。android

这几年,整个行业都开始向声明式界面模型转换,这个模型大大的简化了咱们构建界面的流程。在更新界面的时候能够智能的找到应该更新的部分而且只刷新此部分的视图。web

Compose就是一个声明式的UI框架spring

为了更好的边写Jetpack Compose ,最好下载Androidstudio的 最新 Canary 版的 Android Studio 预览版。他能够跟咱们使用的release版本共存。使用它建立项目的时候直接有内置的建立Compose项目的模板。api

简单了解一个Compose函数:缓存

@Composable
fun Greeting(name: String) {
    Column (modifier = Modifier.padding(16.dp)){
        Text(text = "Hello $name!")
        Text(text = "你好 $name!")
    }
}
复制代码

compose_0.png

  • 该函数须要有@Composable注解,全部Compose相关的函数都须要有这个注解,这个注解的做用是告诉编译器该函数是用来生成UI的。
  • 该函数能够接受参数,这些参数可让应用逻辑来描述界面,好比上面的这个函数,接收一个字符串名字,这个名字就能够影响界面的显示
  • 该函数中Column()至关于以前xml中的LinearLayout,Text()至关于以前xml中的TextVIew

使用Compose的时候须要注意markdown

  • 可组合函数能够按照任意顺序执行
  • 可组合函数能够并行执行
  • 重组的时候尽量的避开不须要更新的可组合函数
  • 重组是乐观操做能够随时取消
  • 可组合函数会像动画的每一帧同样频繁的执行

Compose的状态框架

下面的函数能够实现一个在文本框中输入文字的时候,动态的更改Text显示的内容ide

@Composable
fun HelloContent(){
    Column(modifier = Modifier.padding(16.dp)) {
        var name = remember{ mutableStateOf("")}
        if(name.value.isNotEmpty()){
           Text(text = "Hello,${name.value}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name.value, onValueChange = { name.value = it },label = {Text("Name")})
    }
}
复制代码

remember是一个能够保存Composable中的数值的函数,只是临时保存,Composable移除后也会跟着移除或者被打断后。函数

想要被打断以后还能保存状态,好比来电了,可使用rememberSaveable

咱们可使用MutableState来观察数据的状态从而动态更新界面。除了使用MutableState以外,还可使用LiveData、Flow、Rxjava2,使用这几个的时候咱们须要将其转化为State接口,这样才能让compose识别。

好比LiveData中的转换,给LiveData设置一个扩展方法,也是使用remember建立一个state并返回。

@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
    val lifecycleOwner = LocalLifecycleOwner.current
    val state = remember { mutableStateOf(initial) }
    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> { state.value = it }
        observe(lifecycleOwner, observer)
        onDispose { removeObserver(observer) }
    }
    return state
}
复制代码

通常状况下数据不会直接放在Composable修饰的方法里面而是提出来好比下面的代码,这样更容易代码的复用和管理

@Composable
fun HelloScreen(){
    var name = remember{ mutableStateOf("")}
    HelloContent(name = name.value, onNmeChange = { name.value = it })
}

@Composable
fun HelloContent(name: String,onNmeChange:(String)->Unit){
    Column(modifier = Modifier.padding(16.dp)) {
        if(name.isNotEmpty()){
           Text(text = "Hello,${name}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name, onValueChange = onNmeChange,label = {Text("Name")})
    }
}
复制代码

HelloScreen 负责数据状态的更改逻辑,HelloContent负责UI的展现和事件的传递。

compose_1.gif

使用ViewModel

使用ViewModel来管理数据的状态 改造一下上面的例子

class HelloViewModel:ViewModel() {
    private val _name = MutableLiveData("")
    val name : LiveData<String> = _name

    fun onNameChanged(newName:String){
        _name.value = newName
    }
}
复制代码

MainActivity中

private val viewModel:HelloViewModel by viewModels()
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(color = MaterialTheme.colors.background) {
                    Column {
                        HelloScreen(viewModel)
                    }
                }
            }
        }
    }
@Composable
fun HelloScreen(viewModel: HelloViewModel){
    val name:String by viewModel.name.observeAsState("")
    HelloContent(name = name, onNmeChange = { viewModel.onNameChanged(it) })
}

@Composable
fun HelloScreen(){
    var name = remember{ mutableStateOf("")}
    HelloContent(name = name.value, onNmeChange = { name.value = it })
}

@Composable
fun HelloContent(name: String,onNmeChange:(String)->Unit){
    Column(modifier = Modifier.padding(16.dp)) {
        if(name.isNotEmpty()){
           Text(text = "Hello,${name}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name, onValueChange = onNmeChange,label = {Text("Name")})
    }
}
复制代码

上面代码中 observeAsState能够观察LiveData,并返回State ,State Jetpack Compose 能够直接使用的可观察类型。前面说了observeAsState内部也是封装了remember函数。使用的时候须要引入下面的依赖

implementation "androidx.compose.runtime:runtime-livedata:1.0.0-beta01"
复制代码

Jetpack Compose是经过各个组件的组合来描述一个UI界面,当应用的状态发生变化的时候,Jetpack Compose 会安排重组,重组就是从新执行可能因状态改变而发生变化的组件。重组是更新界面的惟一方式。

也就是说一个组合就能够表明一个界面,其内部的可组合项的生命周期就是:进入组合、执行0次或者屡次重组、退出组合。

Compose中的重组通常是由State<T>接口触发,Compose会跟踪使用State<T>数据的可组合项,Compose在重组的时候只会更改发生变化的部分。

下面来了解一下经常使用的布局、列表、动画、手势等操做在Compose中的使用。

布局

Compose中的布局最经常使用的有三个:Column、Row和Box

  • Column 至关于纵向的LinearLayout
  • Row 至关于横向的LinearLayout
  • Box 至关于FrameLayout
@Composable
fun LayoutTest(){
    Column() {
        Row(modifier = Modifier.padding(10.dp),verticalAlignment = Alignment.CenterVertically) {
            Image(painter = painterResource(id = R.drawable.ic_launcher_background),
                contentDescription = "头像",
                Modifier
                    .width(60.dp)
                    .height(60.dp)
                    .clip(RoundedCornerShape(50)))
            Column(modifier = Modifier.padding(10.dp)) {
                Text(text = "名字")
                Text(text = "2 minute ago")
            }
        }
    }
}
复制代码

上面的代码轻松实现了左边头像右边竖向排列的两个文本的效果。

compose_2.png

Modifier是修饰符,专门用来控制视图的各类属性如padding、offset、background、圆角、大小、外观、行为、互动等,属性有好多,用的时候直接查文档或者点进去看看源码

注意:modifier的顺序对结果也有影响,好比下面的代码中,padding在clickable前面,那么padding部分是不能点击的padding若是在clickable后面,那么整个Column布局均可以点击

@Composable
fun ArtistCard(name: String,onClick: () -> Unit){
    Column(modifier = Modifier
        .padding(16.dp) //设置16dp的padding
        .clickable(onClick = onClick) //让改控件拥有点击属性和点击水波纹效果
        .fillMaxWidth() //宽度填充父控件
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Spacer(Modifier.size(20.dp)) //设置一个20dp的空间占位 宽高都是20的正方形
            Card(elevation = 4.dp) {
                Image(painter = painterResource(id = R.drawable.img_bg_2), contentDescription = "pic")
            }
        }
    }
}
复制代码

若是想要Column或者Row能够滚动,直接添加下面的属性就能够了。Modifier.verticalScroll(rememberScrollState())或者horizontalScroll(rememberScrollState())

Modifier.size 能够设置一个容器的宽高 其子布局默认不能超出他的范围,若是想要子布局能够超出它的范围,子布局可使用Modifier.requiredSize方法就能够了

Box(
        Modifier
            .size(90.dp, 150.dp)
            .background(Color.Green)) {
        Box(
            Modifier
                .requiredSize(100.dp, 100.dp)
                .background(Color.Red)) {
        }
    }
复制代码

也可使用weight属性按比例分布局的大小入下

@Composable
fun FlexibleComposable() {
    Row(Modifier.fillMaxWidth()) {
        Box(
            Modifier
                .weight(2f)
                .height(50.dp)
                .background(Color.Blue))
        Box(
            Modifier
                .weight(1f)
                .height(50.dp)
                .background(Color.Red))
    }
}
复制代码

compose_3.png

除了前面的三个布局容器,若是想要在Compose中的使用相对布局,可使用ConstraintLayout,在实现对其方式比较复杂的布局时比较有用。

虽然在用xml布局的时候为了更好的减小布局层级,推荐优先使用ConstraintLayout布局,不过在Compose中不用担忧布局层级的问题,因此建立布局时仍是首选Column、Row、Box

使用constraintlayout的时候须要单独引入

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha03"
复制代码

下面的代码实现了一个text在button的下面,距离button底部16dp

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // 给可组合对象建立引用
        val (button, text) = createRefs()
        Button(
            onClick = {  },
            //将button约束到布局距离顶部16dp的位置
            modifier = Modifier.constrainAs(button) { 
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }
        //将text约束到距离button底部16dp的地方
        Text("Text", Modifier.constrainAs(text) { 
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}
复制代码

自定义布局

自定义布局,在使用xml布局的时候,咱们须要继承ViewGroup并重写onMeasure和onLayout,在Compose中,只须要使用Layout可组合项编写一个函数就能够了

好比下面自定义实现一个相似Column的效果

@Composable
fun MyColumn(modifier: Modifier = Modifier,content:@Composable() ()->Unit){
    Layout(content = content,modifier = modifier, measurePolicy = MeasurePolicy{
        measurables, constraints ->
        //
        val placeables = measurables.map { measurable ->
            //测量每一个子view
            measurable.measure(constraints.copy(minWidth = 0))
        }

        layout(constraints.maxWidth,300){
            //跟踪子view的y坐标
            var yPosition = 0
            //在父view中放置子view
            placeables.forEach{ placeable ->
                placeable.place(x=0,y=yPosition)
                yPosition+=placeable.height
            }
        }
    })
}
//使用的时候直接以下
MyColumn (Modifier
      .padding(8.dp)
      .background(Color.Red)){
          Text("哈哈哈哈第一行")
          Text("哈哈哈哈第二行")
          Text("哈哈哈哈第三行")
          Text("哈哈哈哈第四行")
      }
复制代码

compose_4.png

除了自定义容器,若是以为修饰符Modifier不够用还能够自定Modifier,其实就是给Modifier添加了一个扩展函数,好比下面的给text添加一个从文字的基线到顶部的距离

fun Modifier.firstBaselineToTop(firstBaseLineToTop: Dp)=Modifier.layout{
    measurable, constraints ->
    //测量可测量的参数 这里就是指Text
    val placeable = measurable.measure(constraints)
    //检查是否有文本基线FirstBaseline
    check(placeable[FirstBaseline]!= AlignmentLine.Unspecified)
    val firstBaseLine = placeable[FirstBaseline]
    //高度减去firstBaseLine
    val placeableY = firstBaseLineToTop.toPx().toInt() - firstBaseLine
    val height = placeable.height + placeableY
    //经过layout指定可组合项的尺寸
    layout(placeable.width,height){
        placeable.place(0,placeableY)
    }
}
复制代码

使用的时候直接给Text添加firstBaselineToTop属性以下,预览就能够看到使用firstBaselineToTop的效果会比直接使用padding距离顶端的更小一些

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("firstBaselineToTop", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("普通padding!", Modifier.padding(top = 32.dp))
    }
}
复制代码

自定义View

在Compose中自定义View比以前使用xml简单了不少,好比下面的代码直接在Canvas里面划线、画圆圈、画矩形等等

@Composable
fun CanvasTest() {
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        drawLine(
            start = Offset(0f, 0f),
            end = Offset(size.width, size.height),
            color = Color.Blue,
            strokeWidth = 5f
        )
        rotate(degrees = 45f){
            drawRect(
                color = Color.Green,
                size = size/4f,
                topLeft = Offset(size.width/3f,size.height/3f)
            )
        }
        drawCircle(
            color = Color.Blue,
            center = Offset(size.width/2,size.height/2),
            radius = 50f
        )
        //多个状态组合 旋转和平移
        withTransform({
            translate(left = size.width/5f)
            rotate(degrees = 45f)
        }){
            drawRect(
                color = Color.Yellow,
                size = size/5f,
                topLeft = Offset(size.width/3f,size.height/3f)
            )
        }
    })
}
复制代码

compose_5.png

列表

前面尝试了给Column添加一个verticalScroll()就可让Column滚动了。不过这时候它只是至关于咱们用xml布局时候的ScrollView,每次会加载全部的内容。若是数据量太大会影响性能。

若是咱们想实现xml布局的时候的Recyclerview的各类缓存功能,Compose提供了LazyColumn和LazyRow。例如

@Composable
fun MessageList(messages:List<String>){
     LazyColumn{
        items(messages){ message ->
            Text(text = message)
        }
     }
}
复制代码

LazyColumn 内部能够经过item()来加载单个列表项,经过items()来加载多个列表项。还有一个itemsIndexed能够实现带索引的列表项。

@Composable
fun MessageList(messages:List<String>){
     LazyColumn{
         itemsIndexed(messages){index,message ->
             Text(text = "$message===$index")
         }
     }
}
复制代码

若是想要给列表添加一个粘性的头部可使用stickyHeader很方便的实现,不过这个目前是实验性的api,之后有可能会改或者去掉。

@Composable
fun MessageList(messages:List<String>){
     val listState = rememberLazyListState()
     LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp,vertical = 8.dp),
          verticalArrangement = Arrangement.spacedBy(4.dp),state = listState){
         println("滚动:${listState.firstVisibleItemIndex}==${listState.firstVisibleItemScrollOffset}")
         stickyHeader(1) {
             Text(text = "我是头部",Modifier.fillMaxWidth().height(60.dp).background(Color.Green))
         }
         itemsIndexed(messages){index,message ->
             Text(text = "$message===$index", Modifier.background(Color.Yellow).height(60.dp))
             Spacer(modifier = Modifier
                 .fillMaxWidth()
                 .height(5.dp)
                 .background(Color.Gray))
         }
     }
}
复制代码

compose_6.png

上面代码中的listState能够监听列表滚动时候的状态,好比第一个可见的位置。listState还提供了控制列表滚动的方法,好比scrollToItem、animateScrollToItem等。

网格列表能够经过LazyVerticalGrid来实现,这个api目前也是实验性的之后可能会变

@ExperimentalFoundationApi
@Composable
fun GridList(messages:List<String>){
    LazyVerticalGrid(cells = GridCells.Adaptive(minSize = 128.dp), content = {
        items(messages){ message ->
            Text(text = message,Modifier
                .background(Color.Yellow)
                .height(60.dp))
        }
    })
}
复制代码

动画

AnimatedVisibility

这个api能够很方便的组合多种动画,目前这个api目前是实验性的,将来有可能改变或者删除

Row{
        AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(initialOffsetY = {-40}) + expandVertically(expandFrom = Alignment.Top) +
                fadeIn(initialAlpha = 0.3f),
        exit = slideOutVertically()+ shrinkVertically()+ fadeOut()) {
        Text(text = "text",fontSize =30.sp)
    }
        Spacer(modifier = Modifier.size(20.dp))
        Button(onClick = { visible = !visible }) {
            Text(text = "点击")
        }
     
 }
复制代码

上面的代码点击按钮,能够控制一个text的从下往上划出页面,从上往下进入界面同时带有淡入淡出效果。多种效果能够直接用+号链接起来就行。

copmose_7.gif

animateContentSize

若是要给一个控件改变大小的时候添加动画,就使用animateContentSize,很是方便。

var message by remember { mutableStateOf("Hello") }
        Row {
            Box(
                modifier = Modifier
                    .background(Color.Blue)
                    .animateContentSize()
            ) {
                Text(text = message)
            }
            Button(onClick = { message += message }) {
                Text(text = "点击")
            }
        }
复制代码

上面的代码点击的时候,增长Text的文本,能够看到Text控件大小改变的时候会有过渡效果。

copmose_8.gif

Crossfade

直接使用Crossfade包裹控件就能很方便的实现切换的时候淡入淡出的效果

var currentPage by remember { mutableStateOf("A") }
Row {
    Crossfade(targetState = currentPage) { screen ->
       when(screen){
           "A" -> Text(text = "A",Modifier.background(Color.Green),fontSize = 30.sp)
           "B" -> Text(text = "B",Modifier.background(Color.Blue),fontSize = 30.sp)
       }
    }
    Spacer(modifier = Modifier.size(20.dp))
    Button(onClick = { if(currentPage=="A") currentPage="B" else currentPage="A" }) {
        Text(text = "点击")
    }
}
复制代码

上面的代码,点击按钮,Text切换A、B的时候会有淡入淡出的效果

copmose_9.gif

animate*AsState

*号表明多种数据类型,好比animateFloatAsState、animateDpAsState、animateSizeAsState、animateOffsetAsState、animateIntAsState等

var enabled by remember{mutableStateOf(true)}
 val alpha = animateFloatAsState(targetValue = if (enabled) 1f else 0.5f)
 Row {
     Box (
         Modifier
             .width(50.dp)
             .height(50.dp)
             .graphicsLayer(alpha = alpha.value)
             .background(Color.Red))
     Spacer(modifier = Modifier.size(20.dp))
     Button(onClick = { enabled = !enabled }) {
         Text(text = "点击")
     }
 }
复制代码

上面的代码,点击按钮的时候,让控件的背景的透明度从1到0.5过渡

copmose_10.gif

Animatable

Animatable是一个容器,能够经过animateTo方法给动画添加效果,Animatable的不少功能是以挂起函数的形式提供的,因此通常运行在一个协程的做用域内,可使用LaunchedEffect建立一个协程的做用域

var ok by remember{mutableStateOf(true)}
val color = remember{ Animatable(Color.Gray)}
LaunchedEffect(ok){
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Row {
    Box (
        Modifier
            .width(50.dp)
            .height(50.dp)
            .background(color.value))
    Spacer(modifier = Modifier.size(20.dp))
    Button(onClick = { ok = !ok }) {
        Text(text = "点击")
    }
}
复制代码

上面的代码,点击按钮控件的背景从绿色过渡到红色

copmose_11.gif

updateTransition

updateTransition是一个方法,返回一个Transition对象,Transition能够管理多个动画,并同时运行这些动画。

var currentState by remember{mutableStateOf(BoxState.Collapsed)}
        val transition = updateTransition(targetState = currentState)
        val size by transition.animateDp { state ->
            when (state) {
                BoxState.Collapsed -> 10.dp
                BoxState.Expanded -> 100.dp
            }
        }
        val coloranimate by transition.animateColor(
            transitionSpec = {
                when {
                    BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                        //spring 能够建立基于物理特性的动画好比先快后慢、回弹、匀速等
                        spring(stiffness = 50f)
                    else ->
                        tween(durationMillis = 500)
                }
            }
        ) { state ->
            when (state) {
                BoxState.Collapsed -> Color.Blue
                BoxState.Expanded -> Color.Yellow
            }
        }
        Row {
            Box(
                Modifier
                    .size(size)
                    .background(coloranimate)){
            }
            Button(onClick = {
                currentState = if(currentState == BoxState.Collapsed) BoxState.Expanded
                else BoxState.Collapsed
            }) {
                Text(text = "点击")
            }
        }
复制代码

上面的代码transition管理着两个动画,一个是大小从10变到100,一个是颜色从蓝色变到黄色。点击按钮的时候两个动画一块执行

copmose_12.gif

InfiniteTransition

InfiniteTransition 也能够保存多个动画,跟前面不一样的是 它的这些动画是布局的时候就当即运行

val infiniteTransition = rememberInfiniteTransition()
     val colorTran by infiniteTransition.animateColor(
         initialValue = Color.Red,
         targetValue = Color.Green,
         animationSpec = infiniteRepeatable(
             animation = tween(1000, easing = LinearEasing),
             repeatMode = RepeatMode.Reverse
         )
     )
     Row {
         Box(
             Modifier
                 .width(60.dp)
                 .height(60.dp)
                 .background(colorTran))
     }
复制代码

上面的代码,页面加载完成以后,控件的背景就会在红色和绿色之间不停的切换。

copmose_13.gif

手势

点击操做

直接使用Modifier的clickable就能够

@Composable
fun ClickableSample() {
    val count = remember { mutableStateOf(0) }
    Text(
        text = count.value.toString(),
        modifier = Modifier
            .width(30.dp)
            .height(30.dp)
            .background(Color.Gray)
            .wrapContentSize(Alignment.Center)
            .clickable { count.value += 1 },
        textAlign = TextAlign.Center
    )
}
复制代码

若是想要更精细的点击可使用 pointerInput 方法里面按下、长按、双击、单击都有

@Composable
fun PointerInputSample() {
    val count = remember { mutableStateOf(0) }
    Text(
        text = count.value.toString(),
        modifier = Modifier
            .width(30.dp)
            .height(30.dp)
            .background(Color.Gray)
            .wrapContentSize(Alignment.Center)
            .pointerInput (Unit){
               detectTapGestures (
                   onPress = {/*按下操做*/},
                   onLongPress = {/*长按操做*/},
                   onDoubleTap = {/*双击*/},
                   onTap = {/*单击*/}
               )
            },
        textAlign = TextAlign.Center
    )
}
复制代码

滚动操做

只需给一个页面元素添加verticalScroll或者horizontalScroll就能够实现竖向和横向的滚动了,相似咱们以前使用xml布局时的ScrollView

@Composable
fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}
复制代码

若是想要滚动到指定的位置,好比下面的代码点击按钮滚动到200的位置,可使用rememberScrollState的scrollTo方法来执行滚动操做。

copmose_14.gif

该操做须要运行在一个协程的做用域中,使用rememberCoroutineScope方法能够得到一个协程的做用域

@Composable
private fun ScrollBoxesSmooth() {
    val scrollState = rememberScrollState()
    val scope = rememberCoroutineScope()
    Column {
        Column(
            modifier = Modifier
                .background(Color.LightGray)
                .size(100.dp)
                .padding(horizontal = 8.dp)
                .verticalScroll(scrollState)
        ) {
            repeat(10) {
                Text("Item $it", modifier = Modifier.padding(2.dp))
            }
        }
        Button(onClick = {
            scope.launch { scrollState.scrollTo(200) }
        }) {
            Text(text = "点击")
        }
    }
}
复制代码

若是想要记录手指在屏幕上滑动的位置,可使用scrollable修饰符来记录。好比下面的代码中scrollable中的state就能够监听手指滚动的距离了。

@Composable
fun ScrollableDemo(){
    var offset by remember{ mutableStateOf(0f) }
    Box(
        Modifier
            .size(100.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.Gray),
        contentAlignment = Alignment.Center
    ) {
        Text(text = offset.toString())
    }
}
复制代码

copmose_15.gif

嵌套滚动 简单的嵌套滚动很简单,哪一个控件须要滚动就加上相应的verticalScroll或者horizontalScroll便可。

compose会自动处理滑动冲滚动突 子控件先滚动,滚动到边界以后 父控件开始。

下面的例子就是类表里面的每一个item仍是个列表,滑动的时候就能够看到内部先滑动,滑动到边界后外部列表在滑动。

@Composable
fun nestScrollDemo1(){
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .width(100.dp)
            .height(200.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Column(
                Modifier
                    .border(6.dp, Color.Blue)
                    .background(Color.Green)
                    .padding(15.dp)
                    .height(150.dp)
                    .verticalScroll(rememberScrollState())) {
                repeat(20){
                    Text("Item $it", modifier = Modifier.padding(2.dp))
                }
            }
        }
    }
}
复制代码

copmose_16.gif

拖动操做可使用draggable修饰符,它能够实现单一方向上的拖动好比横向的或者纵向的拖动。

好比下面的例子在draggable中设置拖拽的方向,监听到拖拽的距离以后设置给自身的offset方法就实现拖拽滑动了。

@Composable
fun draggableDemo(){
    var offsetX by remember{ mutableStateOf(0f) }
    Text(text = "横着拖拽我",
        modifier = Modifier
            .background(Color.Green)
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState(onDelta = { offsetX += it })
            ))
}
复制代码

copmose_17.gif

若是想要多个方向上拖动,可使用pointerInput修饰符,好比下面的例子记录X方向和Y方向上的偏移量,而后设置给自身就能够实现自由拖动了。

@Composable
fun draggableDemo1(){
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }
        Box(
            Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        )
    }
}
复制代码

copmose_18.gif

swipeable 能够在拖动控件松手以后 控件会朝着自定义的方向自动滑动,好比常见的滑动开关的效果

@ExperimentalMaterialApi
@Composable
fun SwipeableDemo() {
    val squareSize = 50.dp

    //手势滑动状态
    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    //滑动的锚点
    val anchors = mapOf(0f to 0, sizePx to 1)

    Box(
        modifier = Modifier
            .width(100.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                //松手以后是否自动滑动到指定位置的 阈值
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                //经过offset方法让自身偏移
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(squareSize)
                .background(Color.DarkGray)
        )
    }
}
复制代码

rememberSwipeableState 目前仍是实验性的api 未来可能会改变或者删除

想要实现多指触控的效果,可使用transformable操做符。好比下面的几行代码就能够实现双指放大缩小、旋转、移动的效果。

@Composable
fun TransformableDemo(){
    //首先设置全部的手势操做
    var scale by remember{ mutableStateOf(1f)}
    var rotation by remember{ mutableStateOf(0f)}
    var offset by remember{ mutableStateOf(Offset.Zero)}
    val state = rememberTransformableState{zoomChange, panChange, rotationChange ->
        scale *= zoomChange
        rotation += rotationChange
        offset += panChange
    }
    Box(Modifier
        .graphicsLayer(
           scaleX = scale,
           scaleY = scale,
           rotationZ = rotation,
           translationX = offset.x,
           translationY = offset.y
        ).transformable(state = state)
        .background(Color.Blue)
        .fillMaxSize()
    )
}
复制代码

copmose_20.gif

虽然Jetpack Compose用起来很是方便,可是跟以前咱们用的很熟的xml布局彻底不同,因此扔须要多多练习。

参考官方文档

相关文章
相关标签/搜索