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

    Integrate the InProgressStrokesView into your view hierarchy.

    <FrameLayout>  <ScrollView  android:id="@+id/my_content"  android:width="match_parent"  android:height="match_parent"  >  <!-- Your content here. -->  </ScrollView>  <androidx.ink.authoring.InProgressStrokesView  android:id="@+id/in_progress_strokes_view"  android:width="match_parent"  android:height="match_parent"  /> </FrameLayout> 

  2. Instantiate InProgressStrokesView

    Within your activity or fragment's [onCreate()][ink-draw-include6] method, obtain a reference to the InProgressStrokesView and establish a touch listener for managing user input.

    class MyActivity : View.OnTouchListener {  private lateinit var contentView: ScrollView  private lateinit var inProgressStrokesView: InProgressStrokesView  private lateinit var predictor: MotionEventPredictor  // ... other variables  override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  predictor = MotionEventPredictor.newInstance(contentView)  contentView = findViewById(R.id.my_content)  contentView.setOnTouchListener(touchListener)  inProgressStrokesView = findViewById(R.id.in_progress_strokes_view)  }  // ... (touchListener implementation) } 

  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

    class MyActivity : View.OnTouchListener {  private lateinit var contentView: ScrollView  private lateinit var inProgressStrokesView: InProgressStrokesView  private var pointerId = -1  private var strokeId: InProgressStrokeId? = null  private lateinit var predictor: MotionEventPredictor  override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)  contentView = findViewById(R.id.my_content)  predictor = MotionEventPredictor.create(contentView)  contentView.setOnTouchListener(touchListener)  inProgressStrokesView = findViewById(R.id.in_progress_strokes_view)  }  private val touchListener = { view: View, event: MotionEvent ->  predictor.record(event)  when (event.actionMasked) {  MotionEvent.ACTION_DOWN -> {  // First pointer - treat it as inking.  view.requestUnbufferedDispatch(event)  val pointerIndex = event.actionIndex  pointerIdToStrokeId[event.getPointerId(pointerIndex)] =  inProgressStrokesView.startStroke(event, pointerId)  return true  }  MotionEvent.ACTION_POINTER_DOWN -> {  val stroke = strokeId ?: return false  inProgressStrokesView.cancelStroke(stroke, event)  strokeId = null  pointerId = -1  return false  }  MotionEvent.ACTION_MOVE -> {  val predictedEvent = predictor.predict()  try  {  for (pointerIndex in 0 until pointerCount) {  val strokeId =  pointerIdToStrokeId[event.getPointerId(pointerIndex)] ?: continue  inProgressStrokesView.addToStroke(event, pointerId, strokeId, predictedEvent)  } finally {  predictedEvent?.recycle()  }  }  }  MotionEvent.ACTION_UP -> {  val pointerIndex = event.actionIndex  val strokeId =  pointerIdToStrokeId[event.getPointerId(pointerIndex)] ?: return false  inProgressStrokesView.finishStroke(event, pointerId, strokeId)  return true  }  MotionEvent.ACTION_CANCEL -> {  val pointerIndex = event.actionIndex  val strokeId =  pointerIdToStrokeId[event.getPointerId(pointerIndex)] ?: return false  inProgressStrokesView.cancelStroke(strokeId, event)  return true  }  }  return false  } } 

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

    Once you have retrieved the finished strokes, you can use the ViewStrokeRenderer as a higher-level abstraction built on top of the CanvasStrokeRenderer. This can further simplify the rendering process within your view hierarchy.

    class DrawingView(context: Context) : View(context) {  private val viewStrokeRenderer = ViewStrokeRenderer(myCanvasStrokeRenderer, this)  override fun onDraw(canvas: Canvas) {  viewStrokeRenderer.drawWithStrokes(canvas) { scope ->  canvas.scale(myZoomLevel)  canvas.rotate(myRotation)  canvas.translate(myPanX, myPanY)  scope.drawStroke(myStroke)  // Draw other objects including more strokes, apply more transformations, ...  }  } }