TouchHelper API

Complete reference for TouchHelper lifecycle, methods, and callbacks, with working Kotlin examples.

Lifecycle state machine

TouchHelper must progress through states in order. Skipping or reversing steps produces silent failures (no pen events, empty regions, or crashes).

┌──────────┐
│  Created  │  TouchHelper.create(view, callback)
└─────┬────┘
      │
┌─────▼──────┐
│ Configured  │  setLimitRect(limitRects, excludeRects)
└─────┬──────┘
      │
┌─────▼──────┐
│   Opened   │  openRawDrawing()
└─────┬──────┘
      │
┌─────▼──────┐
│  Active    │  setRawDrawingEnabled(true)   ← pen events flow
└─────┬──────┘
      │ pause/resume
┌─────▼──────┐
│  Paused    │  setRawDrawingEnabled(false)  ← pen events suspended
└─────┬──────┘
      │
┌─────▼──────┐
│  Closed    │  closeRawDrawing()            ← native thread stopped
└────────────┘

Creating the helper

val helper: TouchHelper = TouchHelper.create(view, callback)

view must be attached to a window and have non-zero dimensions. Create the helper inside a ViewTreeObserver.OnGlobalLayoutListener to guarantee these conditions.

Setting the active region

fun setLimitRect(view: View, helper: TouchHelper) {
    val screenRect = Rect()
    val excludeRects = mutableListOf<Rect>()

    if (!view.getGlobalVisibleRect(screenRect) || screenRect.isEmpty) return

    // Exclude system gesture strips so swipe-up/down still work.
    // Use insets from WindowInsetsCompat; fall back to safe minimums in
    // fullscreen mode where insets report zero.
    val density = view.resources.displayMetrics.density
    val w = view.rootView.width
    val h = view.rootView.height
    val insets = ViewCompat.getRootWindowInsets(view)
        ?.getInsets(WindowInsetsCompat.Type.systemGestures())
    val bottom = maxOf(insets?.bottom ?: 0, (40 * density).toInt())
    val top    = maxOf(insets?.top    ?: 0, (24 * density).toInt())
    excludeRects.add(Rect(0, h - bottom, w, h))   // home gesture strip
    excludeRects.add(Rect(0, 0, w, top))           // notification pull-down

    helper.setLimitRect(mutableListOf(screenRect), excludeRects)
}

Important

setLimitRect takes screen-absolute coordinates. The SDK maps these to the digitizer’s native coordinate space internally via EpdController.mapToRawTouchPoint. Passing view-local coordinates (Rect(0, 0, view.width, view.height)) produces an incorrect (possibly empty) hardware event filter.

Opening and enabling

After setLimitRect, open and enable in sequence:

helper.setLimitRect(limitRects, excludeRects)
helper.openRawDrawing()
helper.setRawDrawingRenderEnabled(true)   // SDK renders A2 live
helper.setPenUpRefreshEnabled(false)      // suppress pen-up blink
helper.setRawDrawingEnabled(true)         // start accepting pen events

Handling view size changes

If the view is resized (e.g. keyboard shown, split-screen), update the region:

override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
    super.onSizeChanged(w, h, oldW, oldH)
    if (w <= 0 || h <= 0 || touchHelper == null) return
    touchHelper?.apply {
        setRawDrawingEnabled(false)
        closeRawDrawing()
        setLimitRect(view, this)
        openRawDrawing()
        setRawDrawingEnabled(true)
    }
}

The close setLimitRect open sequence is required by the SDK; updating setLimitRect without closing first has no effect.

Activity lifecycle integration

// Activity or Fragment:
override fun onResume() {
    super.onResume()
    inkView.onResume()
}

override fun onPause() {
    inkView.onPause()
    super.onPause()
}

// InkView:
fun onResume() = touchHelper?.setRawDrawingEnabled(true)
fun onPause()  = touchHelper?.setRawDrawingEnabled(false)

Cleanup

override fun onDetachedFromWindow() {
    touchHelper?.closeRawDrawing()
    touchHelper = null
    super.onDetachedFromWindow()
}

Implementing RawInputCallback

All eight abstract methods must be overridden (the SDK uses an abstract class, not an interface):

val callback = object : RawInputCallback() {

    // Pen touches surface
    override fun onBeginRawDrawing(b: Boolean, p: TouchPoint) {}

    // Called for each sampled point during the stroke
    override fun onRawDrawingTouchPointMoveReceived(p: TouchPoint) {
        // Track points for your own preview rendering if needed.
        // The SDK's A2 waveform already renders the stroke visually;
        // you only need to record points here if you draw on your own Canvas.
    }

    // Pen lifts
    override fun onEndRawDrawing(b: Boolean, p: TouchPoint) {}

    // Full stroke delivered on pen-up
    override fun onRawDrawingTouchPointListReceived(list: TouchPointList) {
        val points = list.points.map { Point(it.x, it.y, it.pressure) }
        // Commit to your model, then call postInvalidate() so the Canvas
        // is repainted before the SDK's A2 buffer clears.
        commitStroke(points)
        postInvalidate()
    }

    // Eraser-end equivalents (implement as no-ops if erasing not supported)
    override fun onBeginRawErasing(b: Boolean, p: TouchPoint) {}
    override fun onEndRawErasing(b: Boolean, p: TouchPoint) {}
    override fun onRawErasingTouchPointMoveReceived(p: TouchPoint) {}
    override fun onRawErasingTouchPointListReceived(list: TouchPointList) {}
}

TouchPointList

TouchPointList.points returns a List<TouchPoint>. Each TouchPoint has:

Field

Type

Notes

x

Float

Screen-absolute X, matches getGlobalVisibleRect space

y

Float

Screen-absolute Y

pressure

Float

0.0 – 1.0 (normalised from digitizer range)

size

Float

Contact area (pen tip size)

timestamp

Long

Milliseconds since boot

Handling the Any? type wrapper

If your module compiles without the Onyx SDK (e.g. a shared module used by both Boox and non-Boox product variants), store TouchHelper as Any? and cast at the call site. This avoids NoClassDefFoundError on devices where the SDK is absent:

// Stored as Any? so InkView.class has no direct TouchHelper type reference
private var touchHelper: Any? = null

// At call site, inside a try/catch:
@Suppress("UNCHECKED_CAST")
(touchHelper as? TouchHelper)?.setRawDrawingEnabled(true)

Encapsulate all SDK calls in a separate object (e.g. OnyxPen) that is only referenced from Boox-specific code paths:

object OnyxPen {
    fun setRawDrawingEnabled(helper: Any?, enabled: Boolean) {
        try {
            @Suppress("UNCHECKED_CAST")
            (helper as TouchHelper?)?.setRawDrawingEnabled(enabled)
        } catch (_: Throwable) {}
    }
    // … other thin wrappers
}

Pen shape classes

onyxsdk-pen ships several Neo*Pen classes that apply different stroke rendering styles using the SDK’s own A2 renderer:

  • NeoBallpointPen

  • NeoBrushPen (may be unstable on some devices)

  • NeoCharcoalPen / NeoCharcoalPenV2 (may crash, disabled by default in some third-party apps)

  • NeoFountainPen

  • NeoMarkerPen (may be unstable)

  • NeoPencilPen

  • NeoSquarePen

These are used with MultiViewTouchHelper or passed to TouchHelper as brush configuration. If you render strokes on your own Canvas (setting setRawDrawingRenderEnabled(false)), these classes are not relevant.

MultiViewTouchHelper

com.onyx.android.sdk.pen.multiview.MultiViewTouchHelper manages multiple view regions simultaneously and handles view lifecycle events automatically. It is used by Onyx’s own note application when multiple canvas views are on screen. For single-view use cases, plain TouchHelper is sufficient.