1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.method;
18 
19 import android.text.Layout;
20 import android.text.Layout.Alignment;
21 import android.text.NoCopySpan;
22 import android.text.Spannable;
23 import android.view.KeyEvent;
24 import android.view.MotionEvent;
25 import android.view.ViewConfiguration;
26 import android.widget.TextView;
27 
28 public class Touch {
Touch()29     private Touch() { }
30 
31     /**
32      * Scrolls the specified widget to the specified coordinates, except
33      * constrains the X scrolling position to the horizontal regions of
34      * the text that will be visible after scrolling to the specified
35      * Y position.
36      */
scrollTo(TextView widget, Layout layout, int x, int y)37     public static void scrollTo(TextView widget, Layout layout, int x, int y) {
38         final int horizontalPadding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
39         final int availableWidth = widget.getWidth() - horizontalPadding;
40 
41         final int top = layout.getLineForVertical(y);
42         Alignment a = layout.getParagraphAlignment(top);
43         boolean ltr = layout.getParagraphDirection(top) > 0;
44 
45         int left, right;
46         if (widget.getHorizontallyScrolling()) {
47             final int verticalPadding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
48             final int bottom = layout.getLineForVertical(y + widget.getHeight() - verticalPadding);
49 
50             left = Integer.MAX_VALUE;
51             right = 0;
52 
53             for (int i = top; i <= bottom; i++) {
54                 left = (int) Math.min(left, layout.getLineLeft(i));
55                 right = (int) Math.max(right, layout.getLineRight(i));
56             }
57         } else {
58             left = 0;
59             right = availableWidth;
60         }
61 
62         final int actualWidth = right - left;
63 
64         if (actualWidth < availableWidth) {
65             if (a == Alignment.ALIGN_CENTER) {
66                 x = left - ((availableWidth - actualWidth) / 2);
67             } else if ((ltr && (a == Alignment.ALIGN_OPPOSITE)) ||
68                        (!ltr && (a == Alignment.ALIGN_NORMAL)) ||
69                        (a == Alignment.ALIGN_RIGHT)) {
70                 // align_opposite does NOT mean align_right, we need the paragraph
71                 // direction to resolve it to left or right
72                 x = left - (availableWidth - actualWidth);
73             } else {
74                 x = left;
75             }
76         } else {
77             x = Math.min(x, right - availableWidth);
78             x = Math.max(x, left);
79         }
80 
81         widget.scrollTo(x, y);
82     }
83 
84     /**
85      * Handles touch events for dragging.  You may want to do other actions
86      * like moving the cursor on touch as well.
87      */
onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)88     public static boolean onTouchEvent(TextView widget, Spannable buffer,
89                                        MotionEvent event) {
90         DragState[] ds;
91 
92         switch (event.getActionMasked()) {
93         case MotionEvent.ACTION_DOWN:
94             ds = buffer.getSpans(0, buffer.length(), DragState.class);
95 
96             for (int i = 0; i < ds.length; i++) {
97                 buffer.removeSpan(ds[i]);
98             }
99 
100             buffer.setSpan(new DragState(event.getX(), event.getY(),
101                             widget.getScrollX(), widget.getScrollY()),
102                     0, 0, Spannable.SPAN_MARK_MARK);
103             return true;
104 
105         case MotionEvent.ACTION_UP:
106             ds = buffer.getSpans(0, buffer.length(), DragState.class);
107 
108             for (int i = 0; i < ds.length; i++) {
109                 buffer.removeSpan(ds[i]);
110             }
111 
112             if (ds.length > 0 && ds[0].mUsed) {
113                 return true;
114             } else {
115                 return false;
116             }
117 
118         case MotionEvent.ACTION_MOVE:
119             ds = buffer.getSpans(0, buffer.length(), DragState.class);
120 
121             if (ds.length > 0) {
122                 ds[0].mIsSelectionStarted = false;
123 
124                 if (ds[0].mFarEnough == false) {
125                     int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
126 
127                     if (Math.abs(event.getX() - ds[0].mX) >= slop ||
128                         Math.abs(event.getY() - ds[0].mY) >= slop) {
129                         ds[0].mFarEnough = true;
130                         if (event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
131                             ds[0].mIsActivelySelecting = true;
132                             ds[0].mIsSelectionStarted = true;
133                         }
134                     }
135                 }
136 
137                 if (ds[0].mFarEnough) {
138                     ds[0].mUsed = true;
139                     boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0
140                             || MetaKeyKeyListener.getMetaState(buffer,
141                                     MetaKeyKeyListener.META_SHIFT_ON) == 1
142                             || MetaKeyKeyListener.getMetaState(buffer,
143                                     MetaKeyKeyListener.META_SELECTING) != 0;
144 
145                     if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
146                         ds[0].mIsActivelySelecting = false;
147                     }
148 
149                     float dx;
150                     float dy;
151                     if (cap && event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
152                         // if we're selecting, we want the scroll to go in
153                         // the direction of the drag
154                         dx = event.getX() - ds[0].mX;
155                         dy = event.getY() - ds[0].mY;
156                     } else {
157                         dx = ds[0].mX - event.getX();
158                         dy = ds[0].mY - event.getY();
159                     }
160                     ds[0].mX = event.getX();
161                     ds[0].mY = event.getY();
162 
163                     int nx = widget.getScrollX() + (int) dx;
164                     int ny = widget.getScrollY() + (int) dy;
165 
166                     int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
167                     Layout layout = widget.getLayout();
168 
169                     ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - padding));
170                     ny = Math.max(ny, 0);
171 
172                     int oldX = widget.getScrollX();
173                     int oldY = widget.getScrollY();
174 
175                     if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
176                         scrollTo(widget, layout, nx, ny);
177                     }
178 
179                     // If we actually scrolled, then cancel the up action.
180                     if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
181                         widget.cancelLongPress();
182                     }
183 
184                     return true;
185                 }
186             }
187         }
188 
189         return false;
190     }
191 
192     /**
193      * @param widget The text view.
194      * @param buffer The text buffer.
195      */
getInitialScrollX(TextView widget, Spannable buffer)196     public static int getInitialScrollX(TextView widget, Spannable buffer) {
197         DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class);
198         return ds.length > 0 ? ds[0].mScrollX : -1;
199     }
200 
201     /**
202      * @param widget The text view.
203      * @param buffer The text buffer.
204      */
getInitialScrollY(TextView widget, Spannable buffer)205     public static int getInitialScrollY(TextView widget, Spannable buffer) {
206         DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class);
207         return ds.length > 0 ? ds[0].mScrollY : -1;
208     }
209 
210     /**
211      * Checks if selection is still active.
212      * This is useful for extending Selection span on buffer.
213      * @param buffer The text buffer.
214      * @return true if buffer has been marked for selection.
215      *
216      * @hide
217      */
isActivelySelecting(Spannable buffer)218     static boolean isActivelySelecting(Spannable buffer) {
219         DragState[] ds;
220         ds = buffer.getSpans(0, buffer.length(), DragState.class);
221 
222         return ds.length > 0 && ds[0].mIsActivelySelecting;
223     }
224 
225     /**
226      * Checks if selection has begun (are we out of slop?).
227      * Note: DragState.mIsSelectionStarted goes back to false with the very next event.
228      * This is useful for starting Selection span on buffer.
229      * @param buffer The text buffer.
230      * @return true if selection has started on the buffer.
231      *
232      * @hide
233      */
isSelectionStarted(Spannable buffer)234     static boolean isSelectionStarted(Spannable buffer) {
235         DragState[] ds;
236         ds = buffer.getSpans(0, buffer.length(), DragState.class);
237 
238         return ds.length > 0 && ds[0].mIsSelectionStarted;
239     }
240 
241     private static class DragState implements NoCopySpan {
242         public float mX;
243         public float mY;
244         public int mScrollX;
245         public int mScrollY;
246         public boolean mFarEnough;
247         public boolean mUsed;
248         public boolean mIsActivelySelecting;
249         public boolean mIsSelectionStarted;
250 
DragState(float x, float y, int scrollX, int scrollY)251         public DragState(float x, float y, int scrollX, int scrollY) {
252             mX = x;
253             mY = y;
254             mScrollX = scrollX;
255             mScrollY = scrollY;
256         }
257     }
258 }
259