Draw a stroke

To achieve optimal drawing performance, use the startStroke(), addToStroke(), and finishStroke() methods of the InProgressStrokesView class, passing MotionEvent objects as input.

  1. Set up UI component

    Incorporate an AndroidView composable into your drawing composable function.

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

  2. Instantiate InProgressStrokesView

    在 activity 的 onCreate() 方法中,获取对 InProgressStrokesView 的引用,并建立触摸监听器以管理用户输入。

    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. Handle touch events

    Having established the UI components, you can now initiate drawing based on touch events.

    MotionEvent action

    InProgressStrokesView method

    Description

    ACTION_DOWN

    startStroke()

    Begin stroke rendering

    ACTION_MOVE

    addToStroke()

    Continue rendering the stroke

    ACTION_UP

    finishStroke()

    Finalize the stroke rendering

    ACTION_CANCEL or FLAG_CANCELED

    cancelStroke()

    Implement palm rejection; cancel the stroke

    @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. Handle finished strokes

    Upon calling finishStroke(), the stroke is marked for completion. However, the finalization process isn't instantaneous. The stroke is fully processed and becomes accessible to your application shortly after finishStroke() is called, specifically when there are no other strokes in progress. This ensures that all drawing operations are concluded before the stroke is handed off to the client as finished.

    To retrieve finished strokes, you have two options:

    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)
      }
    }
    

    检索完已完成的笔触后,您可以使用 CanvasStrokesRenderer 将其提交到屏幕。

    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)
                }
            }
        }
    }