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佈局徹底不同,因此扔須要多多練習。

參考官方文檔

相關文章
相關標籤/搜索