From df21dfc809c24276501927a166501ea58cfb022f Mon Sep 17 00:00:00 2001 From: TY J Date: Mon, 6 Jul 2026 14:43:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?kal=20myscript-iink=20=EB=A5=BC=20iink=204.?= =?UTF-8?q?5.0=20=EC=9C=BC=EB=A1=9C=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UIReferenceImplementation 을 MyScript 4.5 공식 참조구현으로 재벤더링 - iink 3.0.2 → 4.5.0, compileSdk/targetSdk 33 → 36 - targetSdk 36 의 16KB 페이지 정렬 대응(4.5 AAR 네이티브 라이브러리) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../UIReferenceImplementation/build.gradle | 10 +- .../uireferenceimplementation/Canvas.java | 212 ++++++++++++--- .../ContextualActions.java | 14 +- .../ContextualActionsHelper.java | 9 + .../EditorBinding.java | 43 ++- .../uireferenceimplementation/EditorData.java | 10 +- .../uireferenceimplementation/EditorView.java | 67 +++-- .../FontMetricsProvider.java | 9 +- .../FrameTimeEstimator.java | 135 +++++++++ .../ImageLoader.java | 28 +- .../ImagePainter.java | 26 +- .../InputController.java | 256 ++++++++++++++---- .../uireferenceimplementation/LayerView.java | 182 ++++++++----- .../SmartGuideView.java | 12 +- myscript-iink/build.gradle | 4 +- 15 files changed, 780 insertions(+), 237 deletions(-) create mode 100644 myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java diff --git a/myscript-iink/UIReferenceImplementation/build.gradle b/myscript-iink/UIReferenceImplementation/build.gradle index 06ee34b..4ed6328 100644 --- a/myscript-iink/UIReferenceImplementation/build.gradle +++ b/myscript-iink/UIReferenceImplementation/build.gradle @@ -5,13 +5,13 @@ plugins { android { namespace 'com.myscript.iink.uireferenceimplementation' - compileSdk 33 + compileSdk 36 defaultConfig { minSdk 21 - targetSdk 33 - versionCode 2110 - versionName '2.1.1' + targetSdk 36 + versionCode 4500 + versionName '4.5.0' vectorDrawables.useSupportLibrary true } @@ -20,5 +20,5 @@ android { dependencies { implementation "androidx.appcompat:appcompat:1.5.1" implementation "com.google.code.gson:gson:2.10.1" - api "com.myscript:iink:3.0.2" + api "com.myscript:iink:4.5.0" } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java index d84d3d2..a787f79 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/Canvas.java @@ -6,41 +6,48 @@ import android.graphics.DashPathEffect; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.ColorUtils; - +import android.graphics.Xfermode; import android.text.TextPaint; import android.util.Log; +import com.myscript.iink.GLRenderer; +import com.myscript.iink.ParameterSet; import com.myscript.iink.graphics.Color; +import com.myscript.iink.graphics.ExtraBrushStyle; import com.myscript.iink.graphics.FillRule; import com.myscript.iink.graphics.ICanvas; import com.myscript.iink.graphics.IPath; +import com.myscript.iink.graphics.InkPoints; import com.myscript.iink.graphics.LineCap; import com.myscript.iink.graphics.LineJoin; -import com.myscript.iink.graphics.Point; import com.myscript.iink.graphics.Style; import com.myscript.iink.graphics.Transform; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Objects; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.ColorUtils; public class Canvas implements ICanvas { private static final Style DEFAULT_SVG_STYLE = new Style(); + private static final PorterDuffXfermode xferModeSrcOver = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER); - @NonNull - private final android.graphics.Canvas canvas; + @Nullable + private android.graphics.Canvas canvas; @NonNull private final Paint strokePaint; @@ -76,13 +83,18 @@ public class Canvas implements ICanvas @Nullable private final ImageLoader imageLoader; private final OfflineSurfaceManager offlineSurfaceManager; + @Nullable + private GLRenderer glRenderer; + private boolean keepGLRenderer = false; + + private boolean clearOnStartDraw = true; - private final Set clips; + private final List clips; private final Map typefaceMap; private float[] dashArray; - private int dashOffset = 0; + private float dashOffset = 0; private final float xdpi; private final float ydpi; @@ -92,7 +104,37 @@ public class Canvas implements ICanvas @NonNull private final Matrix pointScaleMatrix; - public Canvas(@NonNull android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, @Nullable OfflineSurfaceManager offlineSurfaceManager, float xdpi, float ydpi) + public static class ExtraBrushConfig + { + @NonNull + public final String baseName; + @NonNull + public final Bitmap stampBitmap; + @Nullable + public final Bitmap backgroundBitmap; + @NonNull + public final ParameterSet config; + + public ExtraBrushConfig(@NonNull String baseName, @NonNull Bitmap stampBitmap, @Nullable Bitmap backgroundBitmap, @NonNull ParameterSet config) + { + this.baseName = baseName; + this.stampBitmap = stampBitmap; + this.backgroundBitmap = backgroundBitmap; + this.config = config; + } + } + + public Canvas(@Nullable android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi) + { + this(canvas, Collections.emptyList(), typefaceMap, imageLoader, null, xdpi, ydpi); + } + + public Canvas(@Nullable android.graphics.Canvas canvas, @NonNull List extraBrushConfigs, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi) + { + this(canvas, extraBrushConfigs, typefaceMap, imageLoader, null, xdpi, ydpi); + } + + public Canvas(@Nullable android.graphics.Canvas canvas, @NonNull List extraBrushConfigs, Map typefaceMap, ImageLoader imageLoader, @Nullable OfflineSurfaceManager offlineSurfaceManager, float xdpi, float ydpi) { this.canvas = canvas; this.typefaceMap = typefaceMap; @@ -101,7 +143,14 @@ public Canvas(@NonNull android.graphics.Canvas canvas, Map typ this.xdpi = xdpi; this.ydpi = ydpi; - clips = new HashSet<>(); + if (!extraBrushConfigs.isEmpty() && GLRenderer.isDeviceSupported()) + { + glRenderer = new GLRenderer(); + for (ExtraBrushConfig config : extraBrushConfigs) + glRenderer.configureBrush(config.baseName, config.stampBitmap, config.backgroundBitmap, config.config); + } + + clips = new ArrayList<>(); strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); strokePaint.setStyle(Paint.Style.STROKE); @@ -135,9 +184,28 @@ public Canvas(@NonNull android.graphics.Canvas canvas, Map typ applyStyle(DEFAULT_SVG_STYLE); } - public Canvas(@NonNull android.graphics.Canvas canvas, Map typefaceMap, ImageLoader imageLoader, float xdpi, float ydpi) + public void destroy() + { + if (glRenderer != null) + { + glRenderer.destroy(); + glRenderer = null; + } + } + + public void setCanvas(@NonNull android.graphics.Canvas canvas) + { + this.canvas = canvas; + } + + public void setClearOnStartDraw(boolean clearOnStartDraw) { - this(canvas, typefaceMap, imageLoader, null, xdpi, ydpi); + this.clearOnStartDraw = clearOnStartDraw; + } + + public void setKeepGLRenderer(boolean keepGLRenderer) + { + this.keepGLRenderer = keepGLRenderer; } private void applyStyle(@NonNull Style style) @@ -174,6 +242,7 @@ public void setTransform(@NonNull Transform transform) transformValues[Matrix.MSCALE_Y] = (float) transform.yy; transformValues[Matrix.MTRANS_Y] = (float) transform.ty; + Objects.requireNonNull(canvas); transformMatrix.setValues(transformValues); canvas.setMatrix(transformMatrix); @@ -265,6 +334,7 @@ public void setStrokeDashArray(float[] strokeDashArray) @Override public void setStrokeDashOffset(float strokeDashOffset) { + dashOffset = strokeDashOffset; if (dashArray != null) strokePaint.setPathEffect(new DashPathEffect(dashArray, dashOffset)); else @@ -313,6 +383,7 @@ public final void setFontProperties(@NonNull String fontFamily, float fontLineHe @Override public void startDraw(int x, int y, int width, int height) { + Objects.requireNonNull(canvas); canvas.save(); pointsCache[0] = x; @@ -322,7 +393,7 @@ public void startDraw(int x, int y, int width, int height) // When offscreen rendering is supported, clear the destination // Otherwise, do not clear the destination (e.g. when exporting image, we want a white background) - if (offlineSurfaceManager != null) + if (offlineSurfaceManager != null && clearOnStartDraw) canvas.drawRect(pointsCache[0], pointsCache[1], pointsCache[2], pointsCache[3], clearPaint); // Hardware canvas does not support PorterDuffXfermode @@ -332,6 +403,13 @@ public void startDraw(int x, int y, int width, int height) @Override public void endDraw() { + if (!keepGLRenderer && glRenderer != null) + { + glRenderer.destroy(); + glRenderer = null; + } + + Objects.requireNonNull(canvas); canvas.restore(); } @@ -340,6 +418,7 @@ public void startGroup(@NonNull String id, float x, float y, float width, float { if (clipContent) { + Objects.requireNonNull(canvas); clips.add(id); canvas.save(); @@ -350,9 +429,12 @@ public void startGroup(@NonNull String id, float x, float y, float width, float @Override public void endGroup(@NonNull String id) { - if (clips.remove(id)) + int index = clips.lastIndexOf(id); + if (index != -1) { + Objects.requireNonNull(canvas); canvas.restore(); + clips.remove(index); } } @@ -378,6 +460,7 @@ public final IPath createPath() @Override public void drawPath(@NonNull IPath ipath) { + Objects.requireNonNull(canvas); Path path = (Path) ipath; if (android.graphics.Color.alpha(fillPaint.getColor()) != 0) @@ -391,9 +474,65 @@ public void drawPath(@NonNull IPath ipath) } } + @Override + public boolean isExtraBrushSupported(@NonNull String brushName) + { + return glRenderer != null && glRenderer.isBrushSupported(brushName); + } + + @Override + public void drawStrokeWithExtraBrush(@NonNull InkPoints[] vInkPoints, int temporaryPoints, + @NonNull ExtraBrushStyle style, boolean fullStroke, long id) + { + Objects.requireNonNull(canvas); + + if (!isExtraBrushSupported(style.brushName)) + return; + + if (vInkPoints.length == 0 || vInkPoints[0].x.length == 0 || style.strokeWidth <= 0.f || android.graphics.Color.alpha(fillPaint.getColor()) == 0) + return; + + if (!glRenderer.isInitialized()) + { + glRenderer.initialize(keepGLRenderer, canvas.getWidth(), canvas.getHeight(), xdpi, ydpi); + } + + Xfermode xfm = fillPaint.getXfermode(); + + try + { + canvas.setMatrix(null); // GLRenderer works with pixels + fillPaint.setXfermode(xferModeSrcOver); + + PointF strokeOrigin = glRenderer.drawStroke(vInkPoints, temporaryPoints, transformValues, style, fillPaint, fullStroke, id); + Bitmap strokeBitmap = glRenderer.saveStroke(); + if (strokeBitmap != null) + canvas.drawBitmap(strokeBitmap, strokeOrigin.x, strokeOrigin.y, fillPaint); + + if (temporaryPoints > 0 && vInkPoints.length == 1) + { + PointF temporaryOrigin = glRenderer.drawTemporary(vInkPoints, temporaryPoints, transformValues, style, fillPaint); + Bitmap temporaryBitmap = glRenderer.saveTemporary(); + if (temporaryBitmap != null) + canvas.drawBitmap(temporaryBitmap, temporaryOrigin.x, temporaryOrigin.y, fillPaint); + } + } + catch (Exception e) + { + Log.e("Canvas", "Error trying to draw stroke with extra brush: " + e.getMessage(), e); + } + finally + { + // restore + fillPaint.setXfermode(xfm); + canvas.setMatrix(transformMatrix); + } + } + @Override public void drawRectangle(float x, float y, float width, float height) { + Objects.requireNonNull(canvas); if (android.graphics.Color.alpha(fillPaint.getColor()) != 0) { canvas.drawRect(x, y, x + width, y + height, fillPaint); @@ -407,6 +546,7 @@ public void drawRectangle(float x, float y, float width, float height) @Override public void drawLine(float x1, float y1, float x2, float y2) { + Objects.requireNonNull(canvas); canvas.drawLine(x1, y1, x2, y2, strokePaint); } @@ -416,16 +556,16 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f if (imageLoader == null) return; - Point screenMin = new Point(x, y); - transform.apply(screenMin); - Point screenMax = new Point(x + width, y + height); - transform.apply(screenMax); + Objects.requireNonNull(canvas); + + RectF pixelSize = new RectF(x,y,x + width, y + height); + transformMatrix.mapRect(pixelSize); final Rect targetRect = new Rect( - (int) Math.floor(screenMin.x), - (int) Math.floor(screenMin.y), - (int) (Math.ceil(screenMax.x) - x), - (int) (Math.ceil(screenMax.y) - y)); + (int) Math.floor(pixelSize.left), + (int) Math.floor(pixelSize.top), + (int) (Math.ceil(pixelSize.right)), + (int) (Math.ceil(pixelSize.bottom))); synchronized (imageLoader) { @@ -441,22 +581,6 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f } else { - // adjust rectangle so that the image gets fit into original rectangle - float fx = width / image.getWidth(); - float fy = height / image.getHeight(); - if (fx > fy) - { - float w = image.getWidth() * fy; - x += (width - w) / 2; - width = w; - } - else - { - float h = image.getHeight() * fx; - y += (height - h) / 2; - height = h; - } - // draw the image Rect srcRect = new Rect(0, 0, image.getWidth(), image.getHeight()); RectF dstRect = new RectF(x, y, x + width, y + height); @@ -471,6 +595,7 @@ public void drawObject(@NonNull String url, @NonNull String mimeType, float x, f @Override public void drawText(@NonNull String label, float x, float y, float xmin, float ymin, float xmax, float ymax) { + Objects.requireNonNull(canvas); // transform the insertion point so that it is not impacted by text scale pointsCache[0] = x; pointsCache[1] = y; @@ -496,6 +621,7 @@ public void blendOffscreen(int id, float srcX, float srcY, float srcWidth, float if (bitmap != null) { + Objects.requireNonNull(canvas); floatRectCache.set(destX, destY, destX + destWidth, destY + destHeight); simpleRectCache.set(Math.round(srcX), Math.round(srcY), Math.round(srcX + srcWidth), Math.round(srcY + srcHeight)); diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java index 6576ff8..ce2f7d4 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActions.java @@ -36,8 +36,18 @@ public enum ContextualActions { */ EXPORT, /** - * Format text. + * Change Text blocks format. * @see Editor#getSupportedTextFormats(ContentSelection) */ - FORMAT_TEXT + FORMAT_TEXT, + /** + * Change selection mode. + * @see Editor#getAvailableSelectionModes() + */ + SELECTION_MODE, + /** + * Change selection type. + * @see Editor#getAvailableSelectionTypes(ContentSelection) + */ + SELECTION_TYPE } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java index bb0901a..972d845 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ContextualActionsHelper.java @@ -5,6 +5,7 @@ import com.myscript.iink.ContentBlock; import com.myscript.iink.ContentPart; import com.myscript.iink.ContentSelection; +import com.myscript.iink.ContentSelectionMode; import com.myscript.iink.Editor; import java.util.EnumSet; @@ -37,6 +38,8 @@ public static EnumSet getAvailableActionsForBlock(@NonNull Ed boolean displayConvert = !blockIsEmpty && editor.getSupportedTargetConversionStates(block).length > 0; boolean displayExport = editor.getSupportedExportMimeTypes(block).length > 0; boolean displayFormatText = editor.getSupportedTextFormats(block).size() > 0; + boolean displaySelectionMode = editor.getAvailableSelectionModes().size() > 0; + boolean displaySelectionType = editor.getAvailableSelectionTypes(block).length > 0; if (displayAddBlock) actions.add(ContextualActions.ADD_BLOCK); if (displayRemove) actions.add(ContextualActions.REMOVE); @@ -45,6 +48,8 @@ public static EnumSet getAvailableActionsForBlock(@NonNull Ed if (isRootBlock) actions.add(ContextualActions.PASTE); if (displayExport) actions.add(ContextualActions.EXPORT); if (displayFormatText) actions.add(ContextualActions.FORMAT_TEXT); + if (displaySelectionMode) actions.add(ContextualActions.SELECTION_MODE); + if (displaySelectionType) actions.add(ContextualActions.SELECTION_TYPE); } return actions; } @@ -57,12 +62,16 @@ public static EnumSet getAvailableActionsForSelection(@NonNul boolean displayConvert = editor.getSupportedTargetConversionStates(selection).length > 0; boolean displayExport = editor.getSupportedExportMimeTypes(selection).length > 0; boolean displayFormatText = selection != null && !editor.getSupportedTextFormats(selection).isEmpty(); + boolean displaySelectionMode = editor.getAvailableSelectionModes().size() > 0; + boolean displaySelectionType = editor.getAvailableSelectionTypes(selection).length > 0; actions.add(ContextualActions.REMOVE); if (displayConvert) actions.add(ContextualActions.CONVERT); actions.add(ContextualActions.COPY); if (displayExport) actions.add(ContextualActions.EXPORT); if (displayFormatText) actions.add(ContextualActions.FORMAT_TEXT); + if (displaySelectionMode) actions.add(ContextualActions.SELECTION_MODE); + if (displaySelectionType) actions.add(ContextualActions.SELECTION_TYPE); return actions; } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java index f53a607..90c6e8b 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorBinding.java @@ -6,15 +6,15 @@ import android.graphics.Typeface; import android.util.DisplayMetrics; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.myscript.iink.Editor; import com.myscript.iink.Engine; import com.myscript.iink.Renderer; import java.util.Map; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public final class EditorBinding { @Nullable @@ -30,25 +30,6 @@ public EditorBinding(@Nullable Engine engine, @NonNull Map typ this.typefaces = typefaces; } - @NonNull - public final EditorData openEditor(@Nullable EditorView editorView) - { - Editor editor = null; - Renderer renderer = null; - if (engine != null && editorView != null) - { - Resources resources = editorView.getResources(); - DisplayMetrics displayMetrics = resources.getDisplayMetrics(); - renderer = engine.createRenderer(displayMetrics.xdpi, displayMetrics.ydpi, editorView); - renderer.setViewOffset(0.0f, 0.0f); - renderer.setViewScale(1.0f); - editor = engine.createEditor(renderer, engine.createToolController()); - editor.setFontMetricsProvider(new FontMetricsProvider(displayMetrics, typefaces)); - bindEditor(editorView, editor); - } - return new EditorData(editor, renderer, inputController); - } - private void bindEditor(@NonNull EditorView editorView, @Nullable Editor editor) { editorView.setTypefaces(typefaces); @@ -66,4 +47,22 @@ private void bindEditor(@NonNull EditorView editorView, @Nullable Editor editor) editorView.setOnTouchListener(inputController); } + @NonNull + public EditorData openEditor(@Nullable EditorView editorView) + { + Editor editor = null; + Renderer renderer = null; + if (engine != null && editorView != null) + { + Resources resources = editorView.getResources(); + DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + renderer = engine.createRenderer(displayMetrics.xdpi, displayMetrics.ydpi, editorView); + renderer.setViewOffset(0.0f, 0.0f); + renderer.setViewScale(1.0f); + editor = engine.createEditor(renderer, engine.createToolController()); + editor.setFontMetricsProvider(new FontMetricsProvider(displayMetrics, typefaces)); + bindEditor(editorView, editor); + } + return new EditorData(editor, renderer, inputController); + } } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java index 283ce0f..ec781bf 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorData.java @@ -2,11 +2,11 @@ package com.myscript.iink.uireferenceimplementation; -import androidx.annotation.Nullable; - import com.myscript.iink.Editor; import com.myscript.iink.Renderer; +import androidx.annotation.Nullable; + public final class EditorData { @Nullable @@ -17,19 +17,19 @@ public final class EditorData private final InputController inputController; @Nullable - public final Editor getEditor() + public Editor getEditor() { return this.editor; } @Nullable - public final Renderer getRenderer() + public Renderer getRenderer() { return this.renderer; } @Nullable - public final InputController getInputController() + public InputController getInputController() { return this.inputController; } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java index 712efd2..b557b04 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java @@ -10,19 +10,21 @@ import android.view.View; import android.widget.FrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.myscript.iink.Editor; import com.myscript.iink.IRenderTarget; import com.myscript.iink.Renderer; import com.myscript.iink.graphics.ICanvas; import com.myscript.iink.graphics.Point; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public class EditorView extends FrameLayout implements IRenderTarget, InputController.ViewListener { private int viewWidth; @@ -40,17 +42,17 @@ public class EditorView extends FrameLayout implements IRenderTarget, InputContr private LayerView layerView; private Map typefaceMap = new HashMap<>(); + @NonNull + private List extraBrushConfigs = Collections.emptyList(); public EditorView(Context context) { - super(context); - offlineSurfaceManager = new OfflineSurfaceManager(); + this(context, null, 0); } public EditorView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - offlineSurfaceManager = new OfflineSurfaceManager(); + this(context, attrs, 0); } public EditorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) @@ -127,6 +129,31 @@ public Renderer getRenderer() return renderer; } + public void setExtraBrushConfigs(@NonNull List extraBrushConfigs) + { + if (editor != null) + { + throw new IllegalStateException("Please set the extra brush configs of the EditorView before binding the editor (through EditorView.setEngine() or EditorView.setEditor())"); + } + + this.extraBrushConfigs = extraBrushConfigs; + for (int i = 0, count = getChildCount(); i < count; ++i) + { + View view = getChildAt(i); + if (view instanceof LayerView) + { + LayerView layerView = (LayerView) view; + layerView.setExtraBrushConfigs(extraBrushConfigs); + } + } + } + + @NonNull + public List getExtraBrushConfigs() + { + return extraBrushConfigs; + } + public void setImageLoader(ImageLoader imageLoader) { this.imageLoader = imageLoader; @@ -138,6 +165,12 @@ public void setImageLoader(ImageLoader imageLoader) } } + @Nullable + public ImageLoader getImageLoader() + { + return imageLoader; + } + public void setTypefaces(@NonNull Map typefaceMap) { if (editor != null) @@ -162,19 +195,13 @@ public Map getTypefaces() return typefaceMap; } - @Nullable - public ImageLoader getImageLoader() - { - return imageLoader; - } - @Override protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { viewWidth = newWidth; viewHeight = newHeight; - if (editor != null && !editor.isClosed()) + if (editor != null) { editor.setViewSize(newWidth, newHeight); invalidate(renderer, EnumSet.allOf(IRenderTarget.LayerType.class)); @@ -197,7 +224,7 @@ public final void invalidate(@NonNull Renderer renderer, int x, int y, int width if (layerView != null) { - layerView.update(renderer, x, y, width, height, layers); + layerView.update(renderer, x, y, width, height); } } @@ -263,15 +290,7 @@ public ICanvas createOffscreenRenderCanvas(int offscreenID) if (offlineBitmap == null) return null; android.graphics.Canvas canvas = new android.graphics.Canvas(offlineBitmap); - try { - return new Canvas(canvas, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY()); - } catch(Exception e) { - /* - * 지금 이 함수는 별도 쓰레드에서 실행되는데, 다른 쓰레드에서 renderer 가 close 될 경우 익셉션이 발생되는데 - * close 된 renderer 가 사용될 경우가 없을 것이라 판단, null 을 리턴. - */ - return null; - } + return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY()); } @Override diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java index 0270cc6..1b13888 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FontMetricsProvider.java @@ -19,6 +19,7 @@ import android.text.style.MetricAffectingSpan; import android.text.style.TextAppearanceSpan; import android.util.DisplayMetrics; +import android.util.TypedValue; import androidx.annotation.NonNull; import androidx.collection.LruCache; @@ -88,11 +89,6 @@ public FontMetricsProvider(DisplayMetrics displayMetrics, Map this.typefaceMap = typefaceMap; } - private float x_mm2px(float mm) - { - return (mm / 25.4f) * displayMetrics.xdpi; - } - private float y_mm2px(float mm) { return (mm / 25.4f) * displayMetrics.ydpi; @@ -127,7 +123,7 @@ public Rectangle[] getCharacterBoundingBoxes(@NonNull Text text, TextSpan[] span @Override public float getFontSizePx(Style style) { - return style.getFontSize() * displayMetrics.scaledDensity; + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, style.getFontSize(), displayMetrics); } @Override @@ -188,6 +184,7 @@ public synchronized GlyphMetrics[] getGlyphMetrics(Text text, TextSpan[] spans) int typefaceStyle = FontUtils.getTypefaceStyle(style); int fontSize = Math.round(y_mm2px(style.getFontSize())); + fontSize = Math.max(fontSize, 1); int start = text.getGlyphBeginAt(spans[i].beginPosition); int end = text.getGlyphEndAt(spans[i].endPosition - 1); diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java new file mode 100644 index 0000000..8c61275 --- /dev/null +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/FrameTimeEstimator.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Adapted from: +// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:input/input-motionprediction/ + +package com.myscript.iink.uireferenceimplementation; + +import android.content.Context; +import android.os.Build; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.DoNotInline; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +/** + * Get screen fastest refresh rate (in ms) + */ +@SuppressWarnings("deprecation") +public class FrameTimeEstimator { + private static final float LEGACY_FRAME_TIME_MS = 16f; + private static final float MS_IN_A_SECOND = 1000f; + + static public float getFrameTime(@NonNull Context context) + { + return getFastestFrameTimeMs(context); + } + + private static Display getDisplayForContext(Context context) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + { + return Api30Impl.getDisplayForContext(context); + } + return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } + + private static float getFastestFrameTimeMs(Context context) + { + Display defaultDisplay = getDisplayForContext(context); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + return Api23Impl.getFastestFrameTimeMs(defaultDisplay); + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + return Api21Impl.getFastestFrameTimeMs(defaultDisplay); + } + else + { + return LEGACY_FRAME_TIME_MS; + } + } + + @SuppressWarnings("deprecation") + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + static class Api21Impl + { + private Api21Impl() + { + // Not instantiable + } + + @DoNotInline + static float getFastestFrameTimeMs(Display display) + { + float[] refreshRates = display.getSupportedRefreshRates(); + float largestRefreshRate = refreshRates[0]; + + for (int c = 1; c < refreshRates.length; c++) + { + if (refreshRates[c] > largestRefreshRate) + largestRefreshRate = refreshRates[c]; + } + + return MS_IN_A_SECOND / largestRefreshRate; + } + } + + @RequiresApi(Build.VERSION_CODES.M) + static class Api23Impl + { + private Api23Impl() + { + // Not instantiable + } + + @DoNotInline + static float getFastestFrameTimeMs(Display display) + { + Display.Mode[] displayModes = display.getSupportedModes(); + float largestRefreshRate = displayModes[0].getRefreshRate(); + + for (int c = 1; c < displayModes.length; c++) + { + float currentRefreshRate = displayModes[c].getRefreshRate(); + if (currentRefreshRate > largestRefreshRate) + largestRefreshRate = currentRefreshRate; + } + + return MS_IN_A_SECOND / largestRefreshRate; + } + } + + @RequiresApi(Build.VERSION_CODES.R) + static class Api30Impl + { + private Api30Impl() + { + // Not instantiable + } + + @DoNotInline + static Display getDisplayForContext(Context context) + { + return context.getDisplay(); + } + } +} diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java index d82e044..0c15530 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImageLoader.java @@ -21,13 +21,14 @@ public class ImageLoader @NonNull private final Editor editor; LruCache cache; + static final float CACHE_MAX_MEMORY_RATIO = 1.f / 8; // in ]0, 1[ public ImageLoader(@NonNull Editor editor) { this.editor = editor; - // Use a part of the maximum available memory to define the cache's size - int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 8); + // Use a part of the maximum available memory to define the cache's size (in Bytes) + int cacheSize = (int) (Runtime.getRuntime().maxMemory() * CACHE_MAX_MEMORY_RATIO); this.cache = new LruCache(cacheSize) { @@ -59,12 +60,22 @@ public synchronized Bitmap getImage(final String url, final String mimeType, fin { Bitmap image = cache.get(url); if (image != null) - return image; + return image; // found Pair newImage = renderObject(url, mimeType, dstWidth, dstHeight); if (newImage.second) // Not dummy + { + int imageSize = newImage.first.getByteCount(); + if (imageSize > cache.maxSize()) + { + Log.w("ImageLoader", "Image too big for cache: resizing cache (" + + imageSize / (1024.f * 1024.f) + "MB > " + cache.maxSize() / (1024.f * 1024.f) + "MB)"); + cache.resize(imageSize); + } + cache.put(url, newImage.first); + } return newImage.first; } @@ -87,12 +98,18 @@ private Pair renderObject(String url, String mimeType, int dstW if (scaledImage != null) return Pair.create(scaledImage, true); + else + Log.e("ImageLoader", "Unable to scale image: using placeholder image"); } else { return Pair.create(image, true); } } + else + { + Log.e("ImageLoader", "Unable to decode file: using placeholder image"); + } } catch (Exception e) { @@ -102,7 +119,7 @@ private Pair renderObject(String url, String mimeType, int dstW catch (OutOfMemoryError e) { // Error: use fallback bitmap - Log.w("ImageLoader", "Out of memory: unable to load image, using placeholder instead", e); + Log.w("ImageLoader", "Out of memory: unable to load image: using placeholder instead", e); } } @@ -110,6 +127,9 @@ private Pair renderObject(String url, String mimeType, int dstW Bitmap image = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); if (image != null) image.eraseColor(Color.WHITE); + else + Log.e("ImageLoader", "Unable to render image nor placeholder"); + return Pair.create(image, false); } } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java index 70b8b08..54be6df 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/ImagePainter.java @@ -5,8 +5,6 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Typeface; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; import com.myscript.iink.IImagePainter; import com.myscript.iink.graphics.ICanvas; @@ -14,19 +12,35 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Collections; +import java.util.List; import java.util.Map; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + public class ImagePainter implements IImagePainter { private ImageLoader imageLoader = null; private Map typefaceMap = null; + protected android.graphics.Canvas canvas = null; private Bitmap bitmap = null; - private android.graphics.Canvas canvas = null; - private float dpi = 96; - + @NonNull + private final List extraBrushConfigs; + private float dpi = 96.f; @ColorInt private int backgroundColor = Color.WHITE; + public ImagePainter() + { + this(Collections.emptyList()); + } + + public ImagePainter(@NonNull List extraBrushConfigs) + { + this.extraBrushConfigs = extraBrushConfigs; + } + public void setImageLoader(ImageLoader imageLoader) { this.imageLoader = imageLoader; @@ -45,7 +59,7 @@ public void setBackgroundColor(@ColorInt int backgroundColor) @Override public ICanvas createCanvas() { - return new Canvas(canvas, typefaceMap, imageLoader, null, dpi, dpi); + return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, dpi, dpi); } @Override diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java index fcef6a7..4a812e9 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java @@ -7,23 +7,26 @@ import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.core.view.GestureDetectorCompat; import com.myscript.iink.ContentBlock; import com.myscript.iink.Editor; import com.myscript.iink.IRenderTarget; import com.myscript.iink.PointerEvent; import com.myscript.iink.PointerEventType; +import com.myscript.iink.PointerTool; import com.myscript.iink.PointerType; +import com.myscript.iink.Renderer; +import com.myscript.iink.ToolController; import com.myscript.iink.graphics.Point; import java.util.EnumSet; -public class InputController implements View.OnTouchListener, GestureDetector.OnGestureListener +public class InputController implements View.OnTouchListener, GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener { public interface ViewListener @@ -31,34 +34,43 @@ public interface ViewListener void showScrollbars(); } - public interface OnTouchListener { - void onTouch(View v, MotionEvent e); - } - public static final int INPUT_MODE_NONE = -1; public static final int INPUT_MODE_FORCE_PEN = 0; public static final int INPUT_MODE_FORCE_TOUCH = 1; public static final int INPUT_MODE_AUTO = 2; public static final int INPUT_MODE_ERASER = 3; - private final IRenderTarget renderTarget; + private static final float SCALING_SENSIBILITY = 1.5f; + private static final float SCALING_THRESHOLD = 0.02f; + + private final EditorView editorView; private final Editor editor; private int _inputMode; - private final GestureDetectorCompat gestureDetector; + private final GestureDetector gestureDetector; + private final ScaleGestureDetector scaleGestureDetector; private IInputControllerListener _listener; private final long eventTimeOffset; @VisibleForTesting public PointerType iinkPointerType; private ViewListener _viewListener; - private OnTouchListener touchListener; - public InputController(Context context, IRenderTarget renderTarget, Editor editor) + private boolean isScalingEnabled = false; + private float getPreviousScalingSpan; + private float previousScalingFocusX; + private float previousScalingFocusY; + private boolean isMultiFingerTouch = false; + private int previousPointerId; + + private boolean isScrollingEnabled = true; + + public InputController(Context context, EditorView editorView, Editor editor) { - this.renderTarget = renderTarget; + this.editorView = editorView; this.editor = editor; _listener = null; _inputMode = INPUT_MODE_AUTO; - gestureDetector = new GestureDetectorCompat(context, this); + scaleGestureDetector = new ScaleGestureDetector(context, this); + gestureDetector = new GestureDetector(context, this); long rel_t = SystemClock.uptimeMillis(); long abs_t = System.currentTimeMillis(); @@ -85,22 +97,33 @@ public final synchronized void setListener(IInputControllerListener listener) this._listener = listener; } + public final synchronized void setScalingEnabled(boolean enabled) + { + isScalingEnabled = enabled; + } + + public final synchronized void setScrollingEnabled(boolean enabled) { + isScrollingEnabled = enabled; + } + public final synchronized IInputControllerListener getListener() { return _listener; } - public final OnTouchListener setOnTouchListener(OnTouchListener listener) { - return this.touchListener = listener; + public final synchronized int getPreviousPointerId() + { + return previousPointerId; } private boolean handleOnTouchForPointer(MotionEvent event, int actionMask, int pointerIndex) { final int pointerId = event.getPointerId(pointerIndex); final int pointerType = event.getToolType(pointerIndex); + final int historySize = event.getHistorySize(); + final boolean useTiltInfo = pointerType == MotionEvent.TOOL_TYPE_STYLUS; int inputMode = getInputMode(); - if (inputMode == INPUT_MODE_FORCE_PEN) { iinkPointerType = PointerType.PEN; @@ -115,13 +138,9 @@ else if (inputMode == INPUT_MODE_FORCE_TOUCH) { case MotionEvent.TOOL_TYPE_STYLUS: if (inputMode == INPUT_MODE_ERASER) - { iinkPointerType = PointerType.ERASER; - } else - { iinkPointerType = PointerType.PEN; - } break; case MotionEvent.TOOL_TYPE_FINGER: case MotionEvent.TOOL_TYPE_MOUSE: @@ -133,51 +152,139 @@ else if (inputMode == INPUT_MODE_FORCE_TOUCH) } } + if (isScalingEnabled) + scaleGestureDetector.onTouchEvent(event); + if (iinkPointerType == PointerType.TOUCH) - { gestureDetector.onTouchEvent(event); - } - - int historySize = event.getHistorySize(); switch (actionMask) { + // ACTION_POINTER_DOWN is "A non-primary pointer has gone down", only called when a pointer is already on the touchscreen. case MotionEvent.ACTION_POINTER_DOWN: + { + isMultiFingerTouch = true; + if (previousPointerId != -1) + { + editor.pointerCancel(previousPointerId); + previousPointerId = -1; + } + return true; + } case MotionEvent.ACTION_DOWN: - editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + { + previousPointerId = pointerId; + isMultiFingerTouch = false; + // Request unbuffered events for tools that require low capture latency + ToolController toolController = editor.getToolController(); + PointerTool tool = toolController.getToolForType(iinkPointerType); + if (tool == PointerTool.PEN || tool == PointerTool.HIGHLIGHTER) + editorView.requestUnbufferedDispatch(event); + + try + { + if (useTiltInfo) + editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), + event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId); + else + editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + } + catch (UnsupportedOperationException e) { + // Special case: pointerDown already called, discard previous and retry + editor.pointerCancel(pointerId); + if (useTiltInfo) + editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), + event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId); + else + editor.pointerDown(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + } return true; - + } case MotionEvent.ACTION_MOVE: + { + if (isMultiFingerTouch) + return true; + if (historySize > 0) { PointerEvent[] pointerEvents = new PointerEvent[historySize + 1]; - for (int i = 0; i < historySize; ++i) - pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId); - pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + if (useTiltInfo) + { + for (int i = 0; i < historySize; ++i) + { + pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), + event.getHistoricalPressure(pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex, i), iinkPointerType, pointerId); + } + pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), + event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId); + } + else + { + for (int i = 0; i < historySize; ++i) + { + pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), + event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId); + } + pointerEvents[historySize] = new PointerEvent(PointerEventType.MOVE, event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + } editor.pointerEvents(pointerEvents, true); } - else + else // no history { - editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + if (useTiltInfo) + editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), + event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId); + else + editor.pointerMove(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); } return true; - + } + // ACTION_POINTER_UP is "A non-primary pointer has gone up", at least one finger is still on the touchscreen. case MotionEvent.ACTION_POINTER_UP: + { + return true; + } case MotionEvent.ACTION_UP: + { + if (isMultiFingerTouch) + { + isMultiFingerTouch = false; + return true; + } if (historySize > 0) { PointerEvent[] pointerEvents = new PointerEvent[historySize]; - for (int i = 0; i < historySize; ++i) - pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId); + if (useTiltInfo) + { + for (int i = 0; i < historySize; ++i) + { + pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), + event.getHistoricalPressure(pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, pointerIndex, i), event.getHistoricalAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex, i), iinkPointerType, pointerId); + } + } + else + { + for (int i = 0; i < historySize; ++i) + { + pointerEvents[i] = new PointerEvent(PointerEventType.MOVE, event.getHistoricalX(pointerIndex, i), event.getHistoricalY(pointerIndex, i), eventTimeOffset + event.getHistoricalEventTime(i), + event.getHistoricalPressure(pointerIndex, i), iinkPointerType, pointerId); + } + } editor.pointerEvents(pointerEvents, true); } - editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); - return true; + if (useTiltInfo) + editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), + event.getPressure(), event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex), event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex), iinkPointerType, pointerId); + else + editor.pointerUp(event.getX(pointerIndex), event.getY(pointerIndex), eventTimeOffset + event.getEventTime(), event.getPressure(), iinkPointerType, pointerId); + return true; + } case MotionEvent.ACTION_CANCEL: + { editor.pointerCancel(pointerId); return true; - + } default: return false; } @@ -186,14 +293,6 @@ else if (inputMode == INPUT_MODE_FORCE_TOUCH) @Override public boolean onTouch(View v, MotionEvent event) { - Boolean result = processOnTouch(v, event); - if (this.touchListener != null) { - this.touchListener.onTouch(v, event); - } - return result; - } - - private boolean processOnTouch(View v, MotionEvent event) { if (editor == null) { return false; @@ -217,7 +316,8 @@ private boolean processOnTouch(View v, MotionEvent event) { { try { - consumed = consumed || handleOnTouchForPointer(event, actionMask, pointerIndex); + boolean handled = handleOnTouchForPointer(event, actionMask, pointerIndex); + consumed = consumed || handled; } catch(Exception e) { // ignore spurious invalid touch events that may occurs when spamming undo/redo button @@ -231,7 +331,7 @@ private boolean processOnTouch(View v, MotionEvent event) { Log.e("InputController", "bad touch sequence", e); return false; } -} + } @Override public boolean onDown(MotionEvent event) @@ -275,13 +375,12 @@ public void onLongPress(MotionEvent event) @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (editor.isScrollAllowed()) + if (editor.isScrollAllowed() && isScrollingEnabled) { Point oldOffset = editor.getRenderer().getViewOffset(); Point newOffset = new Point(oldOffset.x + distanceX, oldOffset.y + distanceY); - editor.clampViewOffset(newOffset); editor.getRenderer().setViewOffset(Math.round(newOffset.x), Math.round(newOffset.y)); - renderTarget.invalidate(editor.getRenderer(), EnumSet.allOf(IRenderTarget.LayerType.class)); + editorView.invalidate(editor.getRenderer(), EnumSet.allOf(IRenderTarget.LayerType.class)); if(_viewListener != null) { _viewListener.showScrollbars(); @@ -296,4 +395,63 @@ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float ve { return false; } + + @Override + public boolean onScale(ScaleGestureDetector scaleGestureDetector) + { + Renderer renderer = editorView.getRenderer(); + + // Store the current focus of the scaleGestureDetector + float currentScalingFocusX = scaleGestureDetector.getFocusX(); + float currentScalingFocusY = scaleGestureDetector.getFocusY(); + float currentSpan = scaleGestureDetector.getCurrentSpan(); + + // Measure the delta of the currentFocus to the previous + float distanceX = previousScalingFocusX - currentScalingFocusX; + float distanceY = previousScalingFocusY - currentScalingFocusY; + + previousScalingFocusX = currentScalingFocusX; + previousScalingFocusY = currentScalingFocusY; + + Point oldOffset = renderer.getViewOffset(); + Point newOffset = new Point(oldOffset.x + distanceX, oldOffset.y + distanceY); + + // Apply the translation of the scaling focus to the render + renderer.setViewOffset(Math.round(newOffset.x), Math.round(newOffset.y)); + + float deltaSpan = getPreviousScalingSpan / currentSpan; + // Apply a ratio in order to avoid the scaling to move too fast + deltaSpan = 1.0f + ((1.0f - deltaSpan) / SCALING_SENSIBILITY); + + // Do not move if the scaling is too small + if (deltaSpan > (1 + SCALING_THRESHOLD) || deltaSpan < (1 - SCALING_THRESHOLD)) + { + renderer.zoomAt(new Point(currentScalingFocusX, currentScalingFocusY), deltaSpan); + } + + // Store the span for next time + getPreviousScalingSpan = currentSpan; + editorView.invalidate(renderer, EnumSet.allOf(IRenderTarget.LayerType.class)); + + if(_viewListener != null) + { + _viewListener.showScrollbars(); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) + { + getPreviousScalingSpan = scaleGestureDetector.getCurrentSpan(); + previousScalingFocusX = scaleGestureDetector.getFocusX(); + previousScalingFocusY = scaleGestureDetector.getFocusY(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) + { + // no-op + } } diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java index 7d126c1..de30b2e 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/LayerView.java @@ -8,6 +8,7 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Typeface; +import android.os.Build; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; @@ -17,9 +18,13 @@ import com.myscript.iink.IRenderTarget.LayerType; import com.myscript.iink.Renderer; +import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Map; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class LayerView extends View @@ -35,23 +40,29 @@ public class LayerView extends View private Renderer lastRenderer = null; @Nullable - private Rect updateArea; - @Nullable - private Bitmap[] bitmap; + private OfflineSurfaceManager offlineSurfaceManager = null; @Nullable - private android.graphics.Canvas[] sysCanvas; + private Renderer renderer = null; + @NonNull + private Rect updateArea = new Rect(0, 0, 0, 0); + @NonNull + private Rect localUpdateArea = new Rect(0, 0, 0, 0); @Nullable - private Canvas[] iinkCanvas; + private Bitmap bitmap = null; // for API < 28 @Nullable - private OfflineSurfaceManager offlineSurfaceManager = null; + private android.graphics.Canvas sysCanvas = null; // for API < 28 @Nullable - private Renderer renderer = null; + private Canvas iinkCanvas = null; + @NonNull + private List extraBrushConfigs = Collections.emptyList(); + private int pageWidth = 0; private int pageHeight = 0; - private int viewHeight = 0; private int viewWidth = 0; - private int pageWidth = 0; - private int yMin = 0; + private int viewHeight = 0; + private int canvasWidth = 0; + private int canvasHeight = 0; private int xMin = 0; + private int yMin = 0; public LayerView(Context context) { @@ -66,8 +77,6 @@ public LayerView(Context context, @Nullable AttributeSet attrs) public LayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - - updateArea = null; } public void setRenderTarget(IRenderTarget renderTarget) @@ -75,6 +84,11 @@ public void setRenderTarget(IRenderTarget renderTarget) // do not need the renderTarget } + public void setExtraBrushConfigs(@NonNull List extraBrushConfigs) + { + this.extraBrushConfigs = extraBrushConfigs; + } + public void setOfflineSurfaceManager(@Nullable OfflineSurfaceManager offlineSurfaceManager) { this.offlineSurfaceManager = offlineSurfaceManager; @@ -98,64 +112,93 @@ public void setCustomTypefaces(Map typefaceMap) @Override protected final void onDraw(android.graphics.Canvas canvas) { - Rect localUpdateArea; - Renderer renderer; + super.onDraw(canvas); - synchronized (this) + // Draw directly in hardware-accelerated Canvas if scaling is supported (since API 28) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - localUpdateArea = this.updateArea; + Renderer renderer; + synchronized (this) + { + localUpdateArea.set(0, 0, canvasWidth, canvasHeight); + renderer = lastRenderer; + } - this.updateArea = null; - renderer = lastRenderer; - lastRenderer = null; - } + iinkCanvas.setCanvas(canvas); + prepare(canvas, localUpdateArea); - if (localUpdateArea != null) - { - prepare(sysCanvas[MODEL], localUpdateArea); try { - renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas[MODEL]); + renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas); + renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas); } finally { - restore(sysCanvas[MODEL]); + restore(canvas); } - - prepare(sysCanvas[CAPTURE], localUpdateArea); - try + } + else // Draw in intermediate bitmap + { + Renderer renderer; + synchronized (this) { - renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas[CAPTURE]); + localUpdateArea.set(this.updateArea); + this.updateArea.setEmpty(); + + renderer = lastRenderer; + lastRenderer = null; } - finally + + if (!localUpdateArea.isEmpty()) { - restore(sysCanvas[CAPTURE]); + prepare(sysCanvas, localUpdateArea); + try + { + renderer.drawModel(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas); + renderer.drawCaptureStrokes(localUpdateArea.left, localUpdateArea.top, localUpdateArea.width(), localUpdateArea.height(), iinkCanvas); + } + finally + { + restore(sysCanvas); + } } - } - canvas.drawBitmap(bitmap[MODEL], 0, 0, null); - canvas.drawBitmap(bitmap[CAPTURE], 0, 0, null); + canvas.drawBitmap(bitmap, 0, 0, null); + } } @Override protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { - if (bitmap != null) - { - bitmap[MODEL].recycle(); - bitmap[CAPTURE].recycle(); - } DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - bitmap = new Bitmap[2]; - bitmap[MODEL] = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); - bitmap[CAPTURE] = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); - sysCanvas = new android.graphics.Canvas[2]; - sysCanvas[MODEL] = new android.graphics.Canvas(bitmap[MODEL]); - sysCanvas[CAPTURE] = new android.graphics.Canvas(bitmap[CAPTURE]); - iinkCanvas = new Canvas[2]; - iinkCanvas[MODEL] = new Canvas(sysCanvas[MODEL], typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi); - iinkCanvas[CAPTURE] = new Canvas(sysCanvas[CAPTURE], typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi); + synchronized (this) + { + // Direct draw + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + { + if (iinkCanvas != null) + iinkCanvas.destroy(); + + iinkCanvas = new Canvas(null, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi); + } + else // Bitmap draw + { + if (bitmap != null) + bitmap.recycle(); + if (iinkCanvas != null) + iinkCanvas.destroy(); + + bitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); + sysCanvas = new android.graphics.Canvas(bitmap); + iinkCanvas = new Canvas(sysCanvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, metrics.xdpi, metrics.ydpi); + } + + iinkCanvas.setClearOnStartDraw(false); + iinkCanvas.setKeepGLRenderer(true); + canvasWidth = newWidth; + canvasHeight = newHeight; + } super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); } @@ -164,7 +207,8 @@ private void prepare(android.graphics.Canvas canvas, Rect clipRect) { canvas.save(); canvas.clipRect(clipRect); - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } private void restore(android.graphics.Canvas canvas) @@ -172,36 +216,46 @@ private void restore(android.graphics.Canvas canvas) canvas.restore(); } - public final void update(Renderer renderer, int x, int y, int width, int height, EnumSet layers) + public final void update(Renderer renderer, int x, int y, int width, int height) { boolean emptyArea; - synchronized (this) + + // Direct draw + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (updateArea != null) - updateArea.union(x, y, x + width, y + height); - else - updateArea = new Rect(x, y, x + width, y + height); + Rect updatedArea = new Rect(x, y, x + width, y + height); + synchronized (this) + { + if (canvasWidth > 0 && canvasHeight > 0) + updatedArea.intersect(new Rect(0, 0, canvasWidth, canvasHeight)); - if (bitmap != null) + emptyArea = updatedArea.isEmpty(); + lastRenderer = renderer; + } + } + else // Bitmap draw + { + synchronized (this) { - if (bitmap[MODEL] != null) - updateArea.intersect(new Rect(0, 0, bitmap[MODEL].getWidth(), bitmap[MODEL].getHeight())); - if (bitmap[CAPTURE] != null) - updateArea.intersect(new Rect(0, 0, bitmap[CAPTURE].getWidth(), bitmap[CAPTURE].getHeight())); + updateArea.union(x, y, x + width, y + height); + if (canvasWidth > 0 && canvasHeight > 0) + updateArea.intersect(new Rect(0, 0, canvasWidth, canvasHeight)); + + emptyArea = updateArea.isEmpty(); + lastRenderer = renderer; } - emptyArea = updateArea.isEmpty(); - lastRenderer = renderer; } + if (!emptyArea) { postInvalidate(x, y, x + width, y + height); } } - public void setScrollbar(Renderer renderer, int viewWidthPx, int pageWidthtPx, int xMin, int viewHeightPx, int pageHeightPx, int yMin) + public void setScrollbar(Renderer renderer, int viewWidthPx, int pageWidthPx, int xMin, int viewHeightPx, int pageHeightPx, int yMin) { this.viewWidth = viewWidthPx; - this.pageWidth = pageWidthtPx; + this.pageWidth = pageWidthPx; this.renderer = renderer; this.pageHeight = pageHeightPx; this.viewHeight = viewHeightPx; diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java index 374ce9a..9389293 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/SmartGuideView.java @@ -311,12 +311,15 @@ public void setEditor(@Nullable Editor editor) Engine engine = editor.getEngine(); exportParams = engine.createParameterSet(); - exportParams.setBoolean("export.jiix.text.words", true); - exportParams.setBoolean("export.jiix.strokes", false); exportParams.setBoolean("export.jiix.bounding-box", false); exportParams.setBoolean("export.jiix.glyphs", false); exportParams.setBoolean("export.jiix.primitives", false); - exportParams.setBoolean("export.jiix.chars", false); + exportParams.setBoolean("export.jiix.strokes", false); + exportParams.setBoolean("export.jiix.text.chars", false); + exportParams.setBoolean("export.jiix.text.lines", false); + exportParams.setBoolean("export.jiix.text.spans", false); + exportParams.setBoolean("export.jiix.text.structure", false); + exportParams.setBoolean("export.jiix.text.words", true); importParams = engine.createParameterSet(); importParams.setString("diagram.import.jiix.action", "update"); @@ -330,7 +333,6 @@ public void setEditor(@Nullable Editor editor) fadeOutOtherDelay = configuration.getNumber("smart-guide.fade-out-delay.other", SMART_GUIDE_FADE_OUT_DELAY_OTHER_DEFAULT).intValue(); removeHighlightDelay = configuration.getNumber("smart-guide.highlight-removal-delay", SMART_GUIDE_HIGHLIGHT_REMOVAL_DELAY_DEFAULT).intValue(); } - } public void setMenuListener(@Nullable MenuListener moreMenuListener) @@ -408,7 +410,7 @@ public void contentChanged(@NonNull Editor editor, String[] blockIds) } else { - return; + update(null, UpdateCause.EDIT); } } diff --git a/myscript-iink/build.gradle b/myscript-iink/build.gradle index 4d71eb9..3cc9f8d 100644 --- a/myscript-iink/build.gradle +++ b/myscript-iink/build.gradle @@ -5,11 +5,11 @@ plugins { android { namespace 'com.knowre.android.myscript.iink' - compileSdk 33 + compileSdk 36 defaultConfig { minSdk 21 - targetSdk 33 + targetSdk 36 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 9d96326bbc62f7476183c570d1578cea1f475b62 Mon Sep 17 00:00:00 2001 From: TY J Date: Mon, 6 Jul 2026 14:43:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?myscript=20UIReferenceImplementation=20?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20=EC=BB=A4=EC=8A=A4=ED=84=B0=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=9E=AC=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InputController: OnTouchListener (MyScript.kt 자동변환 억제에서 소비) - EditorView: createOffscreenRenderCanvas null-guard, onSizeChanged isClosed 가드 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../uireferenceimplementation/EditorView.java | 12 ++++++++++-- .../InputController.java | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java index b557b04..59fca96 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/EditorView.java @@ -201,7 +201,7 @@ protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldH viewWidth = newWidth; viewHeight = newHeight; - if (editor != null) + if (editor != null && !editor.isClosed()) { editor.setViewSize(newWidth, newHeight); invalidate(renderer, EnumSet.allOf(IRenderTarget.LayerType.class)); @@ -290,7 +290,15 @@ public ICanvas createOffscreenRenderCanvas(int offscreenID) if (offlineBitmap == null) return null; android.graphics.Canvas canvas = new android.graphics.Canvas(offlineBitmap); - return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY()); + try { + return new Canvas(canvas, extraBrushConfigs, typefaceMap, imageLoader, offlineSurfaceManager, renderer.getDpiX(), renderer.getDpiY()); + } catch(Exception e) { + /* + * 지금 이 함수는 별도 쓰레드에서 실행되는데, 다른 쓰레드에서 renderer 가 close 될 경우 익셉션이 발생되는데 + * close 된 renderer 가 사용될 경우가 없을 것이라 판단, null 을 리턴. + */ + return null; + } } @Override diff --git a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java index 4a812e9..d732d66 100644 --- a/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java +++ b/myscript-iink/UIReferenceImplementation/src/main/java/com/myscript/iink/uireferenceimplementation/InputController.java @@ -34,6 +34,10 @@ public interface ViewListener void showScrollbars(); } + public interface OnTouchListener { + void onTouch(View v, MotionEvent e); + } + public static final int INPUT_MODE_NONE = -1; public static final int INPUT_MODE_FORCE_PEN = 0; public static final int INPUT_MODE_FORCE_TOUCH = 1; @@ -53,6 +57,7 @@ public interface ViewListener @VisibleForTesting public PointerType iinkPointerType; private ViewListener _viewListener; + private OnTouchListener touchListener; private boolean isScalingEnabled = false; private float getPreviousScalingSpan; @@ -116,6 +121,10 @@ public final synchronized int getPreviousPointerId() return previousPointerId; } + public final OnTouchListener setOnTouchListener(OnTouchListener listener) { + return this.touchListener = listener; + } + private boolean handleOnTouchForPointer(MotionEvent event, int actionMask, int pointerIndex) { final int pointerId = event.getPointerId(pointerIndex); @@ -292,6 +301,15 @@ else if (inputMode == INPUT_MODE_FORCE_TOUCH) @Override public boolean onTouch(View v, MotionEvent event) + { + boolean result = processOnTouch(v, event); + if (this.touchListener != null) { + this.touchListener.onTouch(v, event); + } + return result; + } + + private boolean processOnTouch(View v, MotionEvent event) { if (editor == null) { From 7d976618ef3274d43bea817ef31f336c5a229bb5 Mon Sep 17 00:00:00 2001 From: TY J Date: Mon, 6 Jul 2026 14:53:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?CI:=20actions/cache=20=EB=A5=BC=20v4=20?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AC=EB=A0=A4=20=EC=BA=90=EC=8B=9C=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EC=A2=85=EB=A3=8C=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/cache@v4.1.0 은 GitHub 이 종료 중인 구 캐시 서비스를 사용해 자동 실패됨. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e8eecbf..f5c71d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: cache: gradle - name: Gradle Caching - uses: actions/cache@v4.1.0 + uses: actions/cache@v4 with: path: | ~/.gradle/caches