Vẽ nét

Để đạt được hiệu suất vẽ tối ưu, hãy sử dụng phương thức startStroke(), addToStroke()finishStroke() của lớp InProgressStrokesView, truyền đối tượng MotionEvent làm dữ liệu đầu vào.

  1. Thiết lập thành phần giao diện người dùng

    Tích hợp thành phần kết hợp AndroidView vào hàm có khả năng kết hợp bản vẽ.

    @Composable
    fun DrawingView()
    {
      Box(modifier = Modifier.fillMaxSize()) {
        AndroidView(
          modifier = Modifier.fillMaxSize(),
          factory = { context ->
            val rootView = FrameLayout(context)
            //...
          },
        ) {
    
        }
      }
    }
    

  2. Tạo thực thể InProgressStrokesView

    Trong phương thức onCreate() của hoạt động, hãy lấy thông tin tham chiếu đến InProgressStrokesView và thiết lập trình nghe thao tác chạm để quản lý dữ liệu đầu vào của người dùng.

    class MainActivity : ComponentActivity(){
      private lateinit var inProgressStrokesView: InProgressStrokesView
    
      // ... other variables
    
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        inProgressStrokesView = InProgressStrokesView(this)
    
        setContent {
          // ...
          DrawingView(inProgressStrokesView = inProgressStrokesView)
        }
      }
    }
    
    @Composable
    fun DrawingView(
      inProgressStrokesView: InProgressStrokesView,
    ) {
    
      Box(modifier = Modifier.fillMaxSize()) {
        AndroidView(
          modifier = Modifier.fillMaxSize(),
          factory = { context ->
            val rootView = FrameLayout(context)
            inProgressStrokesView.apply {
              layoutParams =
              FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT,
              )
            }
            val predictor = MotionEventPredictor.newInstance(rootView)
            val touchListener =
            View.OnTouchListener { view, event ->
              // ... (handle touch events)
            }
    
            rootView.setOnTouchListener(touchListener)
            rootView.addView(inProgressStrokesView)
            rootView
          },
        ) {}
      }
    }
    

  3. Xử lý sự kiện chạm

    Sau khi thiết lập các thành phần giao diện người dùng, giờ đây, bạn có thể bắt đầu vẽ dựa trên các sự kiện chạm.

    Hành động MotionEvent

    Phương thức InProgressStrokesView

    Mô tả

    ACTION_DOWN

    startStroke()

    Bắt đầu kết xuất nét vẽ

    ACTION_MOVE

    addToStroke()

    Tiếp tục kết xuất nét vẽ

    ACTION_UP

    finishStroke()

    Hoàn tất quá trình kết xuất nét vẽ

    ACTION_CANCEL hoặc FLAG_CANCELED

    cancelStroke()

    Triển khai tính năng chống tì tay; huỷ thao tác vẽ

    @SuppressLint("ClickableViewAccessibility")
    @Composable
    fun DrawingSurface(
        inProgressStrokesView: InProgressStrokesView
    ) {
        val currentPointerId = remember { mutableStateOf<Int?>(null) }
        val currentStrokeId = remember { mutableStateOf<InProgressStrokeId?>(null) }
        val defaultBrush = Brush.createWithColorIntArgb(
            family = StockBrushes.pressurePenLatest,
            colorIntArgb = Color.Black.toArgb(),
            size = 5F,
            epsilon = 0.1F
        )
    
        Box(modifier = Modifier.fillMaxSize()) {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    val rootView = FrameLayout(context)
                    inProgressStrokesView.apply {
                        layoutParams =
                            FrameLayout.LayoutParams(
                                FrameLayout.LayoutParams.MATCH_PARENT,
                                FrameLayout.LayoutParams.MATCH_PARENT,
                            )
                    }
                    val predictor = MotionEventPredictor.newInstance(rootView)
                    val touchListener =
                        View.OnTouchListener { view, event ->
                            predictor.record(event)
                            val predictedEvent = predictor.predict()
    
                            try {
                                when (event.actionMasked) {
                                    MotionEvent.ACTION_DOWN -> {
                                        // First pointer - treat it as inking.
                                        view.requestUnbufferedDispatch(event)
                                        val pointerIndex = event.actionIndex
                                        val pointerId = event.getPointerId(pointerIndex)
                                        currentPointerId.value = pointerId
                                        currentStrokeId.value =
                                            inProgressStrokesView.startStroke(
                                                event = event,
                                                pointerId = pointerId,
                                                brush = defaultBrush
                                            )
                                        true
                                    }
    
                                    MotionEvent.ACTION_MOVE -> {
                                        val pointerId = checkNotNull(currentPointerId.value)
                                        val strokeId = checkNotNull(currentStrokeId.value)
    
                                        for (pointerIndex in 0 until event.pointerCount) {
                                            if (event.getPointerId(pointerIndex) != pointerId) continue
                                            inProgressStrokesView.addToStroke(
                                                event,
                                                pointerId,
                                                strokeId,
                                                predictedEvent
                                            )
                                        }
                                        true
                                    }
    
                                    MotionEvent.ACTION_UP -> {
                                        val pointerIndex = event.actionIndex
                                        val pointerId = event.getPointerId(pointerIndex)
                                        check(pointerId == currentPointerId.value)
                                        val currentStrokeId = checkNotNull(currentStrokeId.value)
                                        inProgressStrokesView.finishStroke(
                                            event,
                                            pointerId,
                                            currentStrokeId
                                        )
                                        view.performClick()
                                        true
                                    }
    
                                    MotionEvent.ACTION_CANCEL -> {
                                        val pointerIndex = event.actionIndex
                                        val pointerId = event.getPointerId(pointerIndex)
                                        check(pointerId == currentPointerId.value)
    
                                        val currentStrokeId = checkNotNull(currentStrokeId.value)
                                        inProgressStrokesView.cancelStroke(currentStrokeId, event)
                                        true
                                    }
    
                                    else -> false
                                }
                            } finally {
                                predictedEvent?.recycle()
                            }
    
                        }
                    rootView.setOnTouchListener(touchListener)
                    rootView.addView(inProgressStrokesView)
                    rootView
                },
            ) {
    
            }
        }
    }
    

  4. Xử lý nét vẽ đã hoàn tất

    Khi gọi finishStroke(), nét vẽ được đánh dấu là hoàn tất. Tuy nhiên, quá trình hoàn tất không diễn ra ngay lập tức. Nét vẽ được xử lý hoàn toàn và ứng dụng của bạn có thể truy cập được ngay sau khi gọi finishStroke(), đặc biệt là khi không có nét vẽ nào khác đang diễn ra. Điều này đảm bảo rằng tất cả các thao tác vẽ đều được kết thúc trước khi nét vẽ được chuyển cho ứng dụng dưới dạng hoàn tất.

    Để truy xuất các nét vẽ đã hoàn tất, bạn có hai lựa chọn:

    class MyActivity : ComponentActivity(), InProgressStrokesFinishedListener {
      ...
    
      private val finishedStrokesState = mutableStateOf(emptySet<Stroke>())
    
      override fun onCreate(savedInstanceState: Bundle?) {
        ...
        inProgressStrokesView.addFinishedStrokesListener(this)
      }
    
      // ... (handle touch events)
    
      @UiThread
      override fun onStrokesFinished(strokes: Map<InProgressStrokeId, Stroke>) {
        finishedStrokesState.value += strokes.values
        inProgressStrokesView.removeFinishedStrokes(strokes.keys)
      }
    }
    

    Sau khi truy xuất nét vẽ xong, bạn có thể sử dụng CanvasStrokesRenderer đưa chúng vào màn hình.

    class MainActivity : ComponentActivity(), InProgressStrokesFinishedListener {
      private lateinit var inProgressStrokesView: InProgressStrokesView
      private val finishedStrokesState = MutableState<emptySet<Stroke>()>
    
      override fun onCreate(savedInstanceState: Bundle?) {
        inProgressStrokesView = InProgressStrokesView(this)
        inProgressStrokesView.addFinishedStrokesListener(this)
        canvasStrokeRenderer = CanvasStrokeRenderer.create()
        //...
        DrawingSurface(
          inProgressStrokesView = inProgressStrokesView,
          canvasStrokeRenderer = canvasStrokeRenderer,
          finishedStrokesState = finishedStrokesState
        )
        //...
        }
      //...
    }
    
    @SuppressLint("ClickableViewAccessibility")
    @Composable
    fun DrawingSurface(
        inProgressStrokesView: InProgressStrokesView,
        finishedStrokesState: Set<Stroke>
    ) {
        val canvasStrokeRenderer = CanvasStrokeRenderer.create()
        //...
    
        Box(modifier = Modifier.fillMaxSize()) {
            AndroidView(
                //...
            )
    
            Canvas(modifier = Modifier) {
                val canvasTransform = Matrix()
                drawContext.canvas.nativeCanvas.concat(canvasTransform)
                val canvas = drawContext.canvas.nativeCanvas
    
                finishedStrokesState.value.forEach { stroke ->
                    canvasStrokeRenderer.draw(stroke = stroke, canvas = canvas, strokeToScreenTransform = canvasTransform)
                }
            }
        }
    }