Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ mobile-ink is currently used in production in MathNotes: https://apps.apple.com/
| Area | Current support |
| --- | --- |
| iOS Apple Pencil drawing | Used in production |
| Native rendering | Custom `MTKView` backed by C++ Skia/Metal |
| Native rendering | Custom iOS `MTKView` and Android `TextureView` backed by the shared C++ Skia engine |
| Continuous notebooks | Fixed native engine pool with momentum scroll and pinch zoom |
| Tools | Pen, highlighter, crayon, calligraphy, eraser, selection, and shape recognition |
| Serialization | JSON notebook payloads plus native page load/save/export helpers |
| Example app | Expo dev-client app with blank continuous notebook, tools, selection, save/reload, and zoom |
| Android | Not supported yet |
| Android | V1 native drawing support with GPU-backed Ganesh rendering, pooled pages, previews, save/reload, eraser, selection, and PDF backgrounds |
| Expo Go | Not supported because this package includes native code |

## Demos
Expand All @@ -68,7 +68,7 @@ npm install @mathnotes/mobile-ink \
cd ios && pod install
```

For Expo apps, use a dev client or prebuild. Expo Go cannot load this native module.
For Expo apps, use a dev client or prebuild. Expo Go cannot load this native module. Android builds also need a configured Android SDK, NDK, and CMake toolchain.

Your app Babel config must include the Reanimated/Worklets plugin expected by your React Native/Reanimated version. For Expo SDK 54/Reanimated 4:

Expand Down Expand Up @@ -132,6 +132,8 @@ export function Notebook() {

The `example/` folder is an Expo dev-client app that exercises the reusable canvas stack, not a MathNotes screen. It demos the full continuous canvas path: pencil drawing with finger navigation by default, optional draw-with-finger mode, pinch zoom, momentum scroll, engine-pool page assignment, MathNotes-style one-page trailing blank growth, tools, selection, and local save/reload on a blank page background.

Expo Go cannot run the example because this package includes native Kotlin, C++, and iOS code. Use a dev-client build:

```sh
cd example
npm install
Expand All @@ -144,6 +146,21 @@ For a simulator:
npx expo run:ios
```

For Android:

```sh
npx expo run:android
```

The first Android build compiles the shared C++ drawing engine and can take a while. If the generated native project is stale after Android native changes, run:

```sh
npx expo prebuild --platform android --clean
npx expo run:android
```

The Android example runs the drawing canvas path. The benchmark screen and CPU/Ganesh backend toggle are currently iOS-only. See [example/README.md](example/README.md) for Android prerequisites, Metro/dev-client commands, and smoke checks.

## Documentation

- [Architecture](docs/architecture.md)
Expand All @@ -152,14 +169,14 @@ npx expo run:ios

## Roadmap

Near-term work is focused on making the public package easier to adopt and easier to contribute to, and achieving Android parity:
Near-term work is focused on making the public package easier to adopt and easier to contribute to, and hardening Android v1:

- Improve install and troubleshooting docs for React Native and Expo dev-client apps.
- Add more integration recipes for save/load, tool switching, and app-owned storage.
- Tighten selection transform performance for large stroke groups.
- Improve edge-case zoom behavior near page and canvas boundaries.
- Continue hardening the example app as a small regression harness.
- Complete Android parity with iOS. We started this but it is still not quite there.
- Add the Android native benchmark runner and expose Android benchmark controls in the example app.

## Development

Expand All @@ -173,6 +190,7 @@ npm pack --dry-run --ignore-scripts
npm ci --prefix example
npm run test:example:typecheck
npm run test:example:export:ios
npm run test:example:export:android
```

`npm run build` creates `lib/` for npm packaging. The React Native entry still points at `src/index.ts` so Metro can transform worklet directives with the consuming app's Babel config.
Expand Down
2 changes: 2 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.cxx/
/build/
92 changes: 92 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
buildscript {
def kotlinVersion = rootProject.ext.has("kotlinVersion")
? rootProject.ext.get("kotlinVersion")
: "2.1.20"

repositories {
google()
mavenCentral()
}

dependencies {
classpath("com.android.tools.build:gradle:8.10.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}

def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

static def findNodeModules(baseDir) {
def basePath = baseDir.toPath().normalize()
while (basePath != null) {
def nodeModulesPath = basePath.resolve("node_modules").toFile()
def reactNativePath = new File(nodeModulesPath, "react-native")
if (nodeModulesPath.exists() && reactNativePath.exists()) {
return nodeModulesPath.absolutePath
}
basePath = basePath.getParent()
}
throw new GradleException("@mathnotes/mobile-ink: failed to find node_modules")
}

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

apply plugin: "com.android.library"
apply plugin: "kotlin-android"

def nodeModules = findNodeModules(projectDir)

android {
namespace "com.mathnotes.mobileink"
compileSdkVersion safeExtGet("compileSdkVersion", 35)

if (rootProject.ext.has("ndkPath")) {
ndkPath rootProject.ext.ndkPath
}
if (rootProject.ext.has("ndkVersion")) {
ndkVersion rootProject.ext.ndkVersion
}

defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 24)
targetSdkVersion safeExtGet("targetSdkVersion", 35)

externalNativeBuild {
cmake {
cppFlags "-std=c++17", "-frtti", "-fexceptions", "-DSK_BUILD_FOR_ANDROID", "-DON_ANDROID", "-DONANDROID"
arguments "-DNODE_MODULES_DIR=${nodeModules}",
"-DANDROID_STL=c++_shared",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
abiFilters(*reactNativeArchitectures())
}
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}

packagingOptions {
excludes = [
"**/libc++_shared.so",
"META-INF/**"
]
}
}

dependencies {
implementation "com.facebook.react:react-android"
}
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
60 changes: 60 additions & 0 deletions android/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
cmake_minimum_required(VERSION 3.13)
project(mobileink)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Node modules path passed from Gradle
set(NODE_MODULES_DIR ${NODE_MODULES_DIR})

# Skia paths from react-native-skia
set(SKIA_DIR "${NODE_MODULES_DIR}/@shopify/react-native-skia")
set(SKIA_INCLUDE_DIR "${SKIA_DIR}/cpp/skia")
set(SKIA_LIB_DIR "${SKIA_DIR}/libs/android/${ANDROID_ABI}")

# Shared C++ drawing engine from this package root.
set(NATIVE_DRAWING_CPP_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../../cpp")

message(STATUS "mobileink: NODE_MODULES_DIR=${NODE_MODULES_DIR}")
message(STATUS "mobileink: SKIA_DIR=${SKIA_DIR}")
message(STATUS "mobileink: NATIVE_DRAWING_CPP_DIR=${NATIVE_DRAWING_CPP_DIR}")
message(STATUS "mobileink: ANDROID_ABI=${ANDROID_ABI}")

# Include directories
include_directories(
${SKIA_INCLUDE_DIR}
${SKIA_INCLUDE_DIR}/include
${SKIA_INCLUDE_DIR}/modules/pathops/include
${NATIVE_DRAWING_CPP_DIR}
)

file(GLOB NATIVE_DRAWING_SOURCES CONFIGURE_DEPENDS
"${NATIVE_DRAWING_CPP_DIR}/*.cpp"
)

# Add source files
add_library(
mobileink
SHARED
${NATIVE_DRAWING_SOURCES}
jni_bridge.cpp
)

# Link Skia library
add_library(skia STATIC IMPORTED)
set_target_properties(skia PROPERTIES IMPORTED_LOCATION "${SKIA_LIB_DIR}/libskia.a")

add_library(pathops STATIC IMPORTED)
set_target_properties(pathops PROPERTIES IMPORTED_LOCATION "${SKIA_LIB_DIR}/libpathops.a")

# Link libraries
target_link_libraries(
mobileink
skia
pathops
android
log
GLESv2
EGL
jnigraphics
)
Loading
Loading