@@ -173,16 +173,49 @@ public bool HandleMotionEvent(MotionEvent e)
173173 releaseAllButtons ( ) ;
174174 }
175175
176+ // Locate the actual stylus pointer rather than blindly reading index 0. When
177+ // a finger is also touching the screen (palm-on-screen while writing, common
178+ // with the S Pen), the stylus is frequently delivered at pointer index 1
179+ // and index 0 is the finger. Reading the finger's coordinates and feeding
180+ // them into the stylus pipeline produced exactly the "stuck top-left" snap
181+ // the user reports — when the finger is briefly at (0,0) (the bottom-left
182+ // origin in window coords on some devices, or a transient lift sample) the
183+ // mapped output is the screen origin.
184+ //
185+ // Falling back to 0 keeps the existing behaviour for the well-formed
186+ // single-pointer case where every pointer in the event is the stylus.
187+ int stylusPointerIndex = findStylusPointerIndex ( e ) ;
188+ if ( stylusPointerIndex < 0 ) return true ;
189+
176190 // Process all batched historical events for maximum accuracy.
177191 int historySize = e . HistorySize ;
178192 for ( int i = 0 ; i < historySize ; i ++ )
179- handlePointer ( e , i , actionMasked ) ;
193+ handlePointer ( e , i , actionMasked , stylusPointerIndex ) ;
180194
181- handlePointer ( e , - 1 , actionMasked ) ;
195+ handlePointer ( e , - 1 , actionMasked , stylusPointerIndex ) ;
182196
183197 return true ;
184198 }
185199
200+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
201+ private static int findStylusPointerIndex ( MotionEvent e )
202+ {
203+ int count = e . PointerCount ;
204+ if ( count <= 0 ) return - 1 ;
205+
206+ for ( int i = 0 ; i < count ; i ++ )
207+ {
208+ var toolType = e . GetToolType ( i ) ;
209+ if ( toolType == MotionEventToolType . Stylus || toolType == MotionEventToolType . Eraser )
210+ return i ;
211+ }
212+
213+ // No pointer self-identifies as a stylus (some devices/SDKs lose the tool-type
214+ // tag on hover-only events even when MotionEvent.Source still has the Stylus
215+ // bit). Default to index 0 to preserve the existing single-pointer behaviour.
216+ return 0 ;
217+ }
218+
186219 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
187220 private void releaseAllButtons ( )
188221 {
@@ -202,26 +235,27 @@ private void releaseAllButtons()
202235 private Vector2 lastTouchPosition ;
203236
204237 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
205- private void handlePointer ( MotionEvent e , int historyIndex , MotionEventActions actionMasked )
238+ private void handlePointer ( MotionEvent e , int historyIndex , MotionEventActions actionMasked , int pointerIndex )
206239 {
207- const int pointer_index = 0 ;
208- if ( e . PointerCount <= pointer_index ) return ;
209-
210- float rawX = historyIndex < 0 ? e . GetX ( pointer_index ) : e . GetHistoricalX ( pointer_index , historyIndex ) ;
211- float rawY = historyIndex < 0 ? e . GetY ( pointer_index ) : e . GetHistoricalY ( pointer_index , historyIndex ) ;
212- float pressure = historyIndex < 0 ? e . GetPressure ( pointer_index ) : e . GetHistoricalPressure ( pointer_index , historyIndex ) ;
213-
214- // Drop (0, 0, 0) garbage samples. The Samsung digitizer occasionally emits a
215- // single (rawX=0, rawY=0, pressure=0) sample when the pen wakes up after sleep,
216- // when the activity regains focus, or as the very first HoverEnter sample
217- // before the real coordinate is latched. Mapping that sample produces a snap
218- // to the top-left of the screen — the long-standing "S Pen stuck top-left"
219- // bug. A real pen sample would always have *some* coordinate (the pen is
220- // physically *somewhere* on the digitizer to have triggered an event), so a
221- // strict triple-zero match is a safe filter that doesn't drop legitimate
222- // edge-of-digitizer samples (which would have pressure > 0 on contact, or
223- // non-zero hover Y/X off the screen origin).
224- if ( rawX == 0f && rawY == 0f && pressure == 0f )
240+ if ( e . PointerCount <= pointerIndex ) return ;
241+
242+ float rawX = historyIndex < 0 ? e . GetX ( pointerIndex ) : e . GetHistoricalX ( pointerIndex , historyIndex ) ;
243+ float rawY = historyIndex < 0 ? e . GetY ( pointerIndex ) : e . GetHistoricalY ( pointerIndex , historyIndex ) ;
244+ float pressure = historyIndex < 0 ? e . GetPressure ( pointerIndex ) : e . GetHistoricalPressure ( pointerIndex , historyIndex ) ;
245+
246+ // Drop (0, 0) garbage samples regardless of pressure. The Samsung digitizer
247+ // emits a (rawX=0, rawY=0) sample when the pen wakes up after sleep, when
248+ // the activity regains focus, and as the very first HoverEnter/Down sample
249+ // before the real coordinate is latched. Older versions only filtered when
250+ // pressure was also exactly zero — but device logs show contact-down and
251+ // ButtonPress samples occasionally landing at (0, 0) with pressure > 0,
252+ // which would still snap the cursor to the top-left.
253+ //
254+ // A real pen sample is *physically somewhere* on the digitizer to have
255+ // triggered the event, so a strict (rawX==0 && rawY==0) match is a safe
256+ // filter — legitimate edge-of-digitizer samples will always have at least
257+ // sub-pixel float noise on one of the two axes.
258+ if ( rawX == 0f && rawY == 0f )
225259 return ;
226260
227261 // Auto-expand tablet size if the digitizer reports coordinates beyond current bounds.
0 commit comments