├── .gitignore
├── AndroidManifest.xml
├── README.md
├── project.properties
├── res
├── drawable-mdpi
│ └── cursor.png
├── layout
│ └── main.xml
└── values
│ └── strings.xml
└── src
└── com
└── zyz
└── mobile
└── example
├── MainActivity.java
├── ObservableScrollView.java
├── OnScrollChangedListener.java
├── SelectableTextView.java
└── SelectionInfo.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 |
15 | # Local configuration file (sdk path, etc)
16 | local.properties
17 |
18 | # Eclipse project files
19 | .classpath
20 | .project
21 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SelectableTextView
2 | ==================
3 |
4 | Android - My implementation of a selectable TextView for Android 2.3.3 and up.
5 |
6 | This is NOT an EditText with selection enabled pretending to be a TextView.
7 | I've implemented cursor controller on top of the TextView. (The source code of Android was a great help for me in making my own implementation.)
8 |
9 | You should be able to build the source and run the example on your Android device without any modifications.
10 | Long click to start the text selection.
11 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-10
15 |
--------------------------------------------------------------------------------
/res/drawable-mdpi/cursor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouray/SelectableTextView/cba2aac6327c6fdd1626062dd099708c43f2dce5/res/drawable-mdpi/cursor.png
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Selectable TextView
4 | 木村項(きむらこう)の発見者木村(きむら)博士の名は驚くべき速力を以て旬日(じゅんじつ)を出ないうちに日本全国に広がった。博士の功績を表彰(ひょうしょう)した学士会院(がくしかいいん)とその表彰をあくまで緊張して報道する事を忘れなかった都下の各新聞は、久しぶりにといわんよりはむしろ初めて、純粋の科学者に対して、政客、軍人、及び実業家に譲らぬ注意を一般社会から要求した。学問のためにも賀すべき事で、博士のためにも喜ばしき事に違(ちがい)ない。
5 | けれども今より一カ月前に、この木村博士が何処に何をしているかを知っていたものは、全国を通じて僅か百人を出ぬ位であったろう。博士が忽然(こつぜん)と著名になったのは、今までまるで人の眼に触れないで経過した科学界という暗黒な人世(じんせい)の象面(しょうめん)に、一点急に輝やく場所が出来たと同じ事である。其所(そこ)が明るくなったのは仕合せである。しかし其所だけが明るくなったのは不都合である。
6 | 一般の社会はつい二、三週間前まで博士の存在について全く神経を使わなかった。一般の社会は今日といえども科学という世界の存在については殆んど不関心(ふかんしん)に打ち過ぎつつある。彼らから見て闇(やみ)に等しい科学界が、一様の程度で彼らの眼に暗く映る間は、彼らが根柢(こんてい)ある人生の活力の或物に対して公平に無感覚であったと非難されるだけで済むが、いやしくもこの暗い中の一点が木村項の名で輝やき渡る以上、また他が依然として暗がりに静まり返る以上、彼らが今まで所有していた公平の無感覚は、俄然(がぜん)として不公平な感覚と変性(へんせい)しなければならない。これまではただ無知で済んでいたのである。それが急に不徳義に転換するのである。問題は単(ひとえ)に智愚を界(さかい)する理性一遍の墻(かき)を乗り超えて、道義の圏内(けんない)に落ち込んで来るのである。
7 | 木村項だけが炳(へい)として俗人の眸(ひとみ)を焼くに至った変化につれて、木村項の周囲にある暗黒面は依然として、木村項の知られざる前と同じように人からその存在を忘れられるならば、日本の科学は木村博士一人の科学で、他の物理学者、数学者、化学者、乃至(ないし)動植物学者に至っては、単位をすら充たす事の出来ない出来損(できそこ)ないでなければならない。貧弱なる日本ではあるが、余(よ)にはこれほどまでに愚図(ぐず)が揃(そろ)って科学を研究しているとは思えない。その方面の知識に疎(うと)い寡聞(かぶん)なる余の頭にさえ、この断見(だんけん)を否定すべき材料は充分あると思う。
8 | 社会は今まで科学界をただ漫然と暗く眺めていた。そうしてその科学界を組織する学者の研究と発見とに対しては、その比較的価値所(どころ)か、全く自家の着衣喫飯(ちゃくいきっぱん)と交渉のない、徒事(いたずらごと)の如く見傚(みな)して来た。そうして学士会院の表彰に驚ろいて、急に木村氏をえらく吹聴(ふいちょう)し始めた。吹聴の程度が木村氏の偉さと比例するとしても、木村氏と他の学者とを合せて、一様に坑中(こうちゅう)に葬り去った一カ月前の無知なる公平は、全然破れてしまった訳になる。一旦(いったん)木村博士を賞揚(しょうよう)するならば、木村博士の功績に応じて、他の学者もまた適当の名誉を荷(にな)うのが正当であるのに、他の学者は木村博士の表彰前と同じ暗黒な平面に取り残されて、ただ一の木村博士のみが、今日まで学者間に維持せられた比較的位地を飛び離れて、衆目の前に独り偉大に見えるようになったのは少なくとも道義的の不公平を敢てして、一般の社会に妙な誤解を与うる好意的な悪結果である。
9 | 社会はただ新聞紙の記事を信じている。新聞紙はただ学士会院の所置(しょち)を信じている。学士会院は固(もと)より己(おの)れを信じているのだろう。余といえども木村項の名誉ある発見たるを疑うものではない。けれども学士会院がその発見者に比較的の位置を与える工夫(くふう)を講じないで、徒(いたず)らに表彰の儀式を祭典の如く見せしむるため被賞者に絶対の優越権を与えるかの如き挙に出でたのは、思慮の周密(しゅうみつ)と弁別(べんべつ)の細緻(さいち)を標榜(ひょうぼう)する学者の所置としては、余の提供にかかる不公平の非難を甘んじて受ける資格があると思う。
10 | 学士会院が栄誉ある多数の学者中より今年はまず木村氏だけを選んで、他は年々順次に表彰するという意を当初から持っているのだと弁解するならば、木村氏を表彰すると同時に、その主意が一般に知れ渡るように取り計(はから)うのが学者の用意というものであろう。木村氏が五百円の賞金と直径三寸大の賞牌(しょうはい)に相当するのに、他の学者はただの一銭の賞金にも直径一分の賞牌にも値せぬように俗衆に思わせるのは、木村氏の功績を表するがために、他の学者に屈辱を与えたと同じ事に帰着する。\n\n
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/com/zyz/mobile/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.zyz.mobile.example;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 | import android.view.MotionEvent;
7 | import android.view.View;
8 |
9 | public class MainActivity extends Activity {
10 |
11 | private SelectableTextView mTextView;
12 | private int mTouchX;
13 | private int mTouchY;
14 | private final static int DEFAULT_SELECTION_LEN = 5;
15 |
16 | @Override
17 | public void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | setContentView(R.layout.main);
20 |
21 | // make sure the TextView's BufferType is Spannable, see the main.xml
22 | mTextView = (SelectableTextView) findViewById(R.id.main_text);
23 | mTextView.setDefaultSelectionColor(0x40FF00FF);
24 |
25 |
26 | mTextView.setOnLongClickListener(new View.OnLongClickListener() {
27 | @Override
28 | public boolean onLongClick(View v) {
29 | showSelectionCursors(mTouchX, mTouchY);
30 | return true;
31 | }
32 | });
33 | mTextView.setOnClickListener(new View.OnClickListener() {
34 | @Override
35 | public void onClick(View v) {
36 | mTextView.hideCursor();
37 | }
38 | });
39 | mTextView.setOnTouchListener(new View.OnTouchListener() {
40 | @Override
41 | public boolean onTouch(View v, MotionEvent event) {
42 | mTouchX = (int) event.getX();
43 | mTouchY = (int) event.getY();
44 | return false;
45 | }
46 | });
47 | }
48 |
49 | private void showSelectionCursors(int x, int y) {
50 | int start = mTextView.getPreciseOffset(x, y);
51 |
52 | if (start > -1) {
53 | int end = start + DEFAULT_SELECTION_LEN;
54 | if (end >= mTextView.getText().length()) {
55 | end = mTextView.getText().length() - 1;
56 | }
57 | mTextView.showSelectionControls(start, end);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/com/zyz/mobile/example/ObservableScrollView.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2013 Ray Zhou
3 |
4 | JadeRead is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | JadeRead is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with JadeRead. If not, see
16 |
17 | Author: Ray Zhou
18 | Date: 2013 04 26
19 |
20 | */
21 | package com.zyz.mobile.example;
22 |
23 | import android.content.Context;
24 | import android.util.AttributeSet;
25 | import android.widget.ScrollView;
26 |
27 | import java.util.ArrayList;
28 |
29 | /**
30 | * ScrollView with a onScrollChangedListener
31 | *
32 | * http://stackoverflow.com/questions/3948934/synchronise-scrollview-scroll-positions-android
33 | *
34 | */
35 | public class ObservableScrollView extends ScrollView{
36 |
37 | private ArrayList mOnScrollChangedListeners;
38 |
39 | @SuppressWarnings("unused")
40 | public ObservableScrollView(Context context) {
41 | super(context);
42 | init();
43 | }
44 |
45 | @SuppressWarnings("unused")
46 | public ObservableScrollView(Context context, AttributeSet attrs, int defStyle) {
47 | super(context, attrs, defStyle);
48 | init();
49 | }
50 |
51 | @SuppressWarnings("unused")
52 | public ObservableScrollView(Context context, AttributeSet attrs) {
53 | super(context, attrs);
54 | init();
55 | }
56 |
57 | private void init() {
58 | mOnScrollChangedListeners = new ArrayList(2);
59 | }
60 |
61 | public void addOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) {
62 | mOnScrollChangedListeners.add(onScrollChangedListener);
63 | }
64 |
65 | @SuppressWarnings("unused")
66 | public void removeOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) {
67 | mOnScrollChangedListeners.remove(onScrollChangedListener);
68 | }
69 |
70 | /**
71 | * google should make this method public and add a setOnScrollChangedListener
72 | * override to allow listener
73 | */
74 | @Override
75 | protected void onScrollChanged(int x, int y, int oldx, int oldy) {
76 | super.onScrollChanged(x, y, oldx, oldy);
77 | for (OnScrollChangedListener listener : mOnScrollChangedListeners) {
78 | listener.onScrollChanged(this, x, y, oldx, oldy);
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/com/zyz/mobile/example/OnScrollChangedListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2013 Ray Zhou
3 |
4 | JadeRead is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | JadeRead is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with JadeRead. If not, see
16 |
17 | Author: Ray Zhou
18 | Date: 2013 04 26
19 |
20 | */
21 | package com.zyz.mobile.example;
22 |
23 |
24 | public interface OnScrollChangedListener {
25 | public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);
26 | }
27 |
--------------------------------------------------------------------------------
/src/com/zyz/mobile/example/SelectableTextView.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2013 Ray Zhou
3 |
4 | JadeRead is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | JadeRead is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with JadeRead. If not, see
16 |
17 | Author: Ray Zhou
18 | Date: 2013 04 26
19 |
20 | */
21 |
22 | package com.zyz.mobile.example;
23 |
24 | import android.content.Context;
25 | import android.graphics.Canvas;
26 | import android.graphics.drawable.Drawable;
27 | import android.text.Layout;
28 | import android.text.style.BackgroundColorSpan;
29 | import android.util.AttributeSet;
30 | import android.view.Gravity;
31 | import android.view.MotionEvent;
32 | import android.view.View;
33 | import android.view.ViewTreeObserver;
34 | import android.widget.PopupWindow;
35 | import android.widget.ScrollView;
36 | import android.widget.TextView;
37 |
38 | /**
39 | * User: ray Date: 2013-02-01
40 | *
41 | * WARNING: SelectableTextView is designed to be used inside a ScrollView. The selection
42 | * functionality have only been tested under the assumption that the parent of the view is a
43 | * ScrollView.
44 | *
45 | * The functionality WILL PROBABLY breaks if the TextView is not inside a ScrollView.
46 | */
47 | public class SelectableTextView extends TextView {
48 |
49 | private int mDefaultSelectionColor;
50 |
51 | /**
52 | * the selection information used by the cursor
53 | */
54 | private SelectionInfo mCursorSelection;
55 |
56 |
57 | private final int[] mTempCoords = new int[2];
58 |
59 |
60 | private OnCursorStateChangedListener mOnCursorStateChangedListener;
61 |
62 | /**
63 | * DONT ACCESS DIRECTLY, use getSelectionController() instead
64 | */
65 | private SelectionCursorController mSelectionController;
66 |
67 | @SuppressWarnings("unused")
68 | public SelectableTextView(Context context) {
69 | super(context);
70 | init();
71 | }
72 |
73 | @SuppressWarnings("unused")
74 | public SelectableTextView(Context context, AttributeSet attrs) {
75 | super(context, attrs);
76 | init();
77 | }
78 |
79 |
80 | @SuppressWarnings("unused")
81 | public SelectableTextView(Context context, AttributeSet attrs, int defStyle) {
82 | super(context, attrs, defStyle);
83 | init();
84 | }
85 |
86 | private void init() {
87 | mCursorSelection = new SelectionInfo();
88 |
89 |
90 | mSelectionController = new SelectionCursorController();
91 |
92 | final ViewTreeObserver observer = getViewTreeObserver();
93 | if (observer != null) {
94 | observer.addOnTouchModeChangeListener(mSelectionController);
95 | }
96 |
97 | }
98 |
99 | @Override
100 | protected void onAttachedToWindow() {
101 | super.onAttachedToWindow();
102 |
103 | // TextView will take care of the color span of the text when it's being scroll by
104 | // its parent ScrollView. But the cursors position is not handled. Calling snapToSelection
105 | // will move the cursors along with the selection.
106 | if (getParent() instanceof ObservableScrollView) {
107 | ((ObservableScrollView) getParent()).addOnScrollChangedListener(new OnScrollChangedListener() {
108 | @Override
109 | public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
110 | mSelectionController.snapToSelection();
111 | }
112 | });
113 | }
114 | }
115 |
116 | public void setDefaultSelectionColor(int color) {
117 | mDefaultSelectionColor = color;
118 | }
119 |
120 | /**
121 | * set the selection beginning at {@code start} with the specified {@code length} for the given
122 | * {@code duration}
123 | *
124 | * @param color the color of the selection
125 | * @param start the start offset
126 | * @param length the length of the selection
127 | * @param duration the duration the selection, in second, to be last, -1 to indicate forever
128 | */
129 | public void setSelection(int color, int start, int length, int duration) {
130 |
131 | // make sure we are not selecting beyond the text
132 | int end = start + length >= getText().length() ? getText().length() - 1 : start + length;
133 |
134 | if (start + length > getText().length() || end <= start) {
135 | return ;
136 | }
137 |
138 | /* My Note
139 | have to create a new one each time instead of using the {@code set} method.
140 | if I use set, will mess up the removeSelection function as the content of the
141 | reference has changed
142 | */
143 | mCursorSelection = new SelectionInfo(getText(), new BackgroundColorSpan(color), start, end);
144 | mCursorSelection.select();
145 | removeSelection(duration);
146 | }
147 |
148 | /**
149 | * set the selection beginning at {@code start} with the specified {@code length} for the given
150 | * {@code duration} with the default color
151 | *
152 | * @param start the start offset
153 | * @param length the length of the selection
154 | * @param duration the duration of the selection to be last, -1 to indicate forever
155 | */
156 | public void setSelection(int start, int length, int duration) {
157 | setSelection(mDefaultSelectionColor, start, length, duration);
158 | }
159 |
160 | /**
161 | * set the selection beginning at {@code start} with the spcified {@code length}
162 | *
163 | * @param start the starting offset
164 | * @param length the length of the selection
165 | */
166 | public void setSelection(int start, int length) {
167 | setSelection(start, length, -1);
168 | }
169 |
170 | /**
171 | * removes the current selection immediately
172 | */
173 | public void removeSelection() {
174 | mCursorSelection.remove();
175 | }
176 |
177 | public void removeSelection(int delay) {
178 | removeSelection(mCursorSelection, delay);
179 | }
180 |
181 | /**
182 | * remove the given span style from the main text after the specified time
183 | *
184 | * @param selection the span style to be removed
185 | * @param delay the milliseconds to wait before remove the style
186 | */
187 | public void removeSelection(final SelectionInfo selection, int delay) {
188 | if (delay >= 0) {
189 | Runnable runnable = new Runnable() {
190 | public void run() {
191 | selection.remove();
192 | }
193 | };
194 | postDelayed(runnable, delay);
195 | }
196 | }
197 |
198 |
199 | /**
200 | * show the selection cursors and select the text between the specified offset
201 | *
202 | * @param start the start offset
203 | * @param end the end offset
204 | */
205 | public void showSelectionControls(int start, int end) {
206 | assert (start >= 0);
207 | assert (end >= 0);
208 | assert (start < getText().length());
209 | assert (end < getText().length());
210 |
211 | mSelectionController.show(start, end);
212 | }
213 |
214 | /**
215 | * @return the current selection information
216 | */
217 | public SelectionInfo getCursorSelection() {
218 | return mCursorSelection;
219 | }
220 |
221 |
222 | /**
223 | * set the OnCursorHiddenListner when then the cursors hide
224 | *
225 | * @param onCursorStateChangedListener the OnCursorVisibilityChangedListener
226 | */
227 | public void setOnCursorStateChangedListener(OnCursorStateChangedListener onCursorStateChangedListener) {
228 | mOnCursorStateChangedListener = onCursorStateChangedListener;
229 | }
230 |
231 | /**
232 | * @return the y position
233 | */
234 | private int getScrollYInternal() {
235 | int y = this.getScrollY();
236 |
237 | // a TextView inside a ScrollView is not scrolled, so getScrollY() returns 0.
238 | // We must use getScrollY() from the ScrollView instead
239 | if (this.getParent() instanceof ScrollView) {
240 | y += ((ScrollView) this.getParent()).getScrollY();
241 |
242 | // do this to compensate for the height of the status bar
243 | final int[] coords = mTempCoords;
244 | ((ScrollView) this.getParent()).getLocationInWindow(coords);
245 | y -= coords[1];
246 |
247 | /* My Note
248 | getLocationInWindow(coords) for the TextView will return a negative
249 | value if we scroll down as the top the TextView is beyong the top
250 | of the screen.
251 |
252 | So maybe we could replace SrollView.getScrollY() with getWindowsInLocation(coords)
253 | */
254 | }
255 |
256 | return y;
257 | }
258 |
259 | /**
260 | * the current x position of the TextView taking into account the scroll (if it's inside a
261 | * ScrollView) and the padding of the scrollview
262 | *
263 | * @return the x position
264 | */
265 | private int getScrollXInternal() {
266 | int x = this.getScrollX();
267 |
268 | // a TextView inside a ScrollView is not scrolled, so getScrollX() returns 0.
269 | // We must use getScrollX() from the ScrollView instead
270 | if (this.getParent() instanceof ScrollView) {
271 | ScrollView scrollView = (ScrollView) this.getParent();
272 |
273 | x += scrollView.getScrollX();
274 |
275 | final int[] coords = mTempCoords;
276 | scrollView.getLocationInWindow(coords);
277 | x -= coords[0];
278 | x -= scrollView.getPaddingLeft();
279 | }
280 | return x;
281 | }
282 |
283 | /**
284 | * Gets the character offset of (x, y). If (x, y) lies on the right half of the character, it
285 | * returns the offset of the next character. If (x, y) lies on the left half of the character, it
286 | * returns the offset of this character.
287 | *
288 | * @param x x coordinate relative to this TextView
289 | * @param y y coordinate relative to this TextView
290 | * @return the offset at (x,y), -1 if error occurs
291 | */
292 | public int getOffset(int x, int y) {
293 | Layout layout = getLayout();
294 | int offset = -1;
295 |
296 | if (layout != null) {
297 | int topVisibleLine = layout.getLineForVertical(y);
298 | offset = layout.getOffsetForHorizontal(topVisibleLine, x);
299 | }
300 |
301 | return offset;
302 | }
303 |
304 | /**
305 | * Gets the character offset where (x, y) is pointing to.
306 | *
307 | * @param x x coordinate relative to this TextView
308 | * @param y y coordinate relative to this TextView
309 | * @return the offset at (x, y), -1 if error occurs
310 | *
311 | * @see {@link #getOffset(int, int)}
312 | */
313 | public int getPreciseOffset(int x, int y) {
314 | Layout layout = getLayout();
315 |
316 | if (layout != null) {
317 | int topVisibleLine = layout.getLineForVertical(y);
318 | int offset = layout.getOffsetForHorizontal(topVisibleLine, x);
319 |
320 | int offset_x = (int) layout.getPrimaryHorizontal(offset);
321 | if (offset_x > x) {
322 | return layout.getOffsetToLeftOf(offset);
323 | }
324 | }
325 | return getOffset(x, y);
326 | }
327 |
328 | ////////////////////////////////////////////////
329 | // copied & modified from Android source code //
330 | ////////////////////////////////////////////////
331 |
332 | /**
333 | * Get the offset character closest to the specified absolute position.
334 | *
335 | * @param x The horizontal absolute position of a point on screen
336 | * @param y The vertical absolute position of a point on screen
337 | * @return the character offset for the character whose position is closest to the specified
338 | * position. Returns -1 if there is no layout.
339 | */
340 | // @SuppressWarnings("unused")
341 | // public int getOffsetFromRaw(int x, int y) {
342 | // if (getLayout() == null) return -1;
343 | //
344 | // y -= getTotalPaddingTop();
345 | // // Clamp the position to inside of the view.
346 | // y = Math.max(0, y);
347 | // y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
348 | // y += getScrollY();
349 | //
350 | // // if the textview is inside a scrollview, the above getScrollY() will return 0
351 | // // even if it's scrolled. Must do getScrollY() from the ScrollView instead.
352 | // if (this.getParent() instanceof ScrollView) {
353 | // ScrollView scrollView = (ScrollView) this.getParent();
354 | // y += scrollView.getScrollY();
355 | //
356 | // // do this to compensate for the height of the status bar
357 | // final int[] coords = mTempCoords;
358 | // scrollView.getLocationInWindow(coords);
359 | // y -= coords[1];
360 | // }
361 | //
362 | // final int line = getLayout().getLineForVertical(y);
363 | // final int offset = getOffsetForHorizontal(line, x);
364 | // return offset;
365 | // }
366 |
367 | ////////////////////////////////////////////////
368 | // copied & modified from Android source code //
369 | ////////////////////////////////////////////////
370 |
371 | /**
372 | * Get the offset character closest to the specified absolute position. If the resulting offset is
373 | * too close to the previous offset, return the previous offset instead.
374 | *
375 | * @param x raw x
376 | * @param y raw y
377 | * @param previousOffset previous offset
378 | * @return offset of the specified (x,y)
379 | */
380 | private int getHysteresisOffset(int x, int y, int previousOffset) {
381 | final Layout layout = getLayout();
382 | if (layout == null) return -1;
383 |
384 |
385 | y += getScrollYInternal();
386 | x += getScrollXInternal();
387 |
388 | int line = getLayout().getLineForVertical(y);
389 |
390 | // The "HACK BLOCK"S in this function is required because of how Android Layout for
391 | // TextView works - if 'offset' equals to the last character of a line, then
392 | //
393 | // * getLineForOffset(offset) will result the NEXT line
394 | // * getPrimaryHorizontal(offset) will return 0 because the next insertion point is on the next line
395 | // * getOffsetForHorizontal(line, x) will not return the last offset of a line no matter where x is
396 | // These are highly undesired and is worked around with the HACK BLOCK
397 | //
398 | // @see Moon+ Reader/Color Note - see how it can't select the last character of a line unless you move
399 | // the cursor to the beginning of the next line.
400 | //
401 | ////////////////////HACK BLOCK////////////////////////////////////////////////////
402 | if (isEndOfLineOffset(previousOffset)) {
403 | // we have to minus one from the offset so that the code below to find
404 | // the previous line can work correctly.
405 | int left = (int) layout.getPrimaryHorizontal(previousOffset - 1);
406 | int right = (int) layout.getLineRight(line);
407 | int threshold = (right - left) / 2; // half the width of the last character
408 | if (x > right - threshold) {
409 | previousOffset -= 1;
410 | }
411 | }
412 | ///////////////////////////////////////////////////////////////////////////////////
413 |
414 | final int previousLine = layout.getLineForOffset(previousOffset);
415 | final int previousLineTop = layout.getLineTop(previousLine);
416 | final int previousLineBottom = layout.getLineBottom(previousLine);
417 | final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2;
418 |
419 | // If new line is just before or after previous line and y position is less than
420 | // hysteresisThreshold away from previous line, keep cursor on previous line.
421 | if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) ||
422 | ((line == previousLine - 1) && ((previousLineTop - y) < hysteresisThreshold)))
423 | {
424 | line = previousLine;
425 | }
426 |
427 | int offset = layout.getOffsetForHorizontal(line, x);
428 |
429 |
430 | // This allow the user to select the last character of a line without moving the
431 | // cursor to the next line. (As Layout.getOffsetForHorizontal does not return the
432 | // offset of the last character of the specified line)
433 | //
434 | // But this function will probably get called again immediately, must decrement the offset
435 | // by 1 to compensate for the change made below. (see previous HACK BLOCK)
436 | /////////////////////HACK BLOCK///////////////////////////////////////////////////
437 | if (offset < getText().length() - 1) {
438 | if (isEndOfLineOffset(offset + 1)) {
439 | int left = (int) layout.getPrimaryHorizontal(offset);
440 | int right = (int) layout.getLineRight(line);
441 | int threshold = (right - left) / 2; // half the width of the last character
442 | if (x > right - threshold) {
443 | offset += 1;
444 | }
445 | }
446 | }
447 | //////////////////////////////////////////////////////////////////////////////////
448 |
449 | return offset;
450 | }
451 |
452 |
453 | /**
454 | * Checks whether the specified offset is at the end of a line.
455 | *
456 | * PRECONDITION: assumes layout exists and is valid
457 | *
458 | * @param offset the offset to check
459 | * @return true if the offset is at the end of a line, false otherwise.
460 | */
461 | private boolean isEndOfLineOffset(int offset) {
462 | if (offset > 0) {
463 | return getLayout().getLineForOffset(offset) ==
464 | getLayout().getLineForOffset(offset - 1) + 1;
465 | }
466 | return false;
467 | }
468 |
469 | ////////////////////////////////////////////////
470 | // copied & modified from Android source code //
471 | ////////////////////////////////////////////////
472 |
473 | /**
474 | * get the offset given the line and the raw x
475 | *
476 | * @param line the line
477 | * @param x raw x
478 | * @return the offset
479 | */
480 | private int getOffsetForHorizontal(int line, int x) {
481 | x -= getTotalPaddingLeft();
482 | // Clamp the position to inside of the view.
483 | x = Math.max(0, x);
484 | x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
485 | x += getScrollXInternal();
486 |
487 | return getLayout().getOffsetForHorizontal(line, x);
488 | }
489 |
490 | /**
491 | * get the (x,y) screen coordinates from the specified offset.
492 | *
493 | * @param offset the offset
494 | * @param scroll_x the horizontal scroll distance to take away
495 | * @param scroll_y the horizontal scroll distance to take away
496 | * @param coords the returned x, y coordinate array, must have a length of 2
497 | */
498 | private void getXY(int offset, int scroll_x, int scroll_y, int[] coords) {
499 | assert (coords.length >= 2);
500 |
501 | coords[0] = coords[1] = -1;
502 | Layout layout = getLayout();
503 |
504 | if (layout != null) {
505 | int line = layout.getLineForOffset(offset);
506 | int base = layout.getLineBottom(line);
507 |
508 | coords[0] = (int) layout.getPrimaryHorizontal(offset) - scroll_x; // x
509 | coords[1] = base - scroll_y; // y
510 | }
511 | }
512 |
513 | /**
514 | * Get the (x,y) screen coordinate from the specified offset. If the specified offset is beyond the
515 | * end of the line, move the offset to the beginning of the next line.
516 | *
517 | * @param offset the offset
518 | * @param scroll_x the horizontal scroll distance to take away
519 | * @param scroll_y the horizontal scroll distance to take away
520 | * @param coords the returned x, y coordinate array, muust have a length of 2
521 | */
522 | private void getAdjusteStartXY(int offset, int scroll_x, int scroll_y, int[] coords) {
523 | if (offset < getText().length()) {
524 | final Layout layout = getLayout();
525 | if (layout != null) {
526 | if (isEndOfLineOffset(offset + 1)) {
527 | float a = layout.getPrimaryHorizontal(offset);
528 | float b = layout.getLineRight(layout.getLineForOffset(offset));
529 | if (a == b) {
530 | // this means the we encounter a new line character, i think.
531 | offset += 1;
532 | }
533 | }
534 | }
535 | }
536 | getXY(offset, scroll_x, scroll_y, coords);
537 | }
538 |
539 | /**
540 | * Get the (x,y) screen coordinate from the specified offset. If the offset is the at the end of a
541 | * wrapped line, return the (x,y) at the end of that line instead of the (x, y) at the beginning of
542 | * the next line (which is the default behaviour for Android)
543 | *
544 | * @param offset the offset
545 | * @param scroll_x the horizontal scroll distance to take away
546 | * @param scroll_y the horizontal scroll distance to take away
547 | * @param coords the returned x, y coordinate array, must have a length of 2
548 | */
549 | private void getAdjustedEndXY(int offset, int scroll_x, int scroll_y, int[] coords) {
550 | if (offset > 0) {
551 | final Layout layout = getLayout();
552 | if (layout != null) {
553 | //if (this_line > prev_line) {
554 | if (isEndOfLineOffset(offset)) {
555 | // if we are at the end of a line, calculate the X using getLineRight instead of
556 | // getPrimaryHorizontal.
557 | // (Because getPrimaryHorizontal returns 0 for offset sitting at the end of a line.
558 | // getPrimaryHorizontal returns the next insertion point, which will be the next line)
559 | int prev_line = layout.getLineForOffset(offset - 1);
560 | float right = layout.getLineRight(prev_line);
561 | int y = layout.getLineBottom(prev_line);
562 | coords[0] = (int) right - scroll_x;
563 | coords[1] = y - scroll_y;
564 | return;
565 | }
566 | }
567 | }
568 | getXY(offset, scroll_x, scroll_y, coords);
569 | }
570 |
571 | public void hideCursor() {
572 | mSelectionController.hide();
573 | }
574 |
575 |
576 | ////////////////////////////////////////////////////////////////////////////
577 | ////////////////////////////////////////////////////////////////////////////
578 | /////////////////////////////INNER CLASSES//////////////////////////////////
579 | ///////// copied, heavily modified and simplified from TextView ////////////
580 | ////////////////////////////////////////////////////////////////////////////
581 |
582 | /**
583 | * Manages the two cursors that control the selection. Internal used only. For outsider, please use
584 | * SelectionModifier
585 | */
586 | private class SelectionCursorController implements ViewTreeObserver.OnTouchModeChangeListener {
587 |
588 | /**
589 | * The two cursor handle that controls the selection. Note that the two cursors are allowed to
590 | * swap positions and thus the name of the handle has no bearing on the relative position of the
591 | * handle to each other. (e.g. mStartHandle can be positioned leagally at an offset greater than
592 | * the offset mEndHandle is resting on)
593 | */
594 | private CursorHandle mStartHandle;
595 | private CursorHandle mEndHandle;
596 |
597 |
598 | /**
599 | * whether the selection controller is displaying on the screen
600 | */
601 | private boolean mIsShowing;
602 |
603 |
604 | public SelectionCursorController() {
605 | mStartHandle = new CursorHandle(this);
606 | mEndHandle = new CursorHandle(this);
607 | }
608 |
609 |
610 | /**
611 | * snap the cursors to the current selection
612 | */
613 | public void snapToSelection() {
614 |
615 | if (mIsShowing) {
616 | int a = SelectableTextView.this.getCursorSelection().getStart();
617 | int b = SelectableTextView.this.getCursorSelection().getEnd();
618 |
619 | int start = Math.min(a, b);
620 | int end = Math.max(a, b);
621 |
622 | // find the corresponding handle for the start/end calculated above
623 | CursorHandle startHandle = start == a ? mStartHandle : mEndHandle;
624 | CursorHandle endHandle = end == b ? mEndHandle : mStartHandle;
625 |
626 | final int[] coords = mTempCoords;
627 | int scroll_y = SelectableTextView.this.getScrollYInternal();
628 | int scroll_x = SelectableTextView.this.getScrollXInternal();
629 |
630 | SelectableTextView.this.getAdjusteStartXY(start, scroll_x, scroll_y, coords);
631 | startHandle.pointTo(coords[0], coords[1]);
632 |
633 | SelectableTextView.this.getAdjustedEndXY(end, scroll_x, scroll_y, coords);
634 | endHandle.pointTo(coords[0], coords[1]);
635 | }
636 | }
637 |
638 | /**
639 | * show the selection cursor at the specified offsets and select the text between the specified
640 | * offsets.
641 | *
642 | * @param start the offset of the first cursor
643 | * @param end the offset of the second cursor
644 | */
645 | public void show(int start, int end) {
646 |
647 | int a = Math.min(start, end);
648 | int b = Math.max(start, end);
649 |
650 | final int[] coords = mTempCoords;
651 | int scroll_y = SelectableTextView.this.getScrollY();
652 | int scroll_x = SelectableTextView.this.getScrollX();
653 |
654 | SelectableTextView.this.getAdjusteStartXY(a, scroll_x, scroll_y, coords);
655 | mStartHandle.show(coords[0], coords[1]);
656 |
657 | SelectableTextView.this.getAdjustedEndXY(b, scroll_x, scroll_y, coords);
658 | mEndHandle.show(coords[0], coords[1]);
659 |
660 | mIsShowing = true;
661 | select(a, b);
662 |
663 | if (mOnCursorStateChangedListener != null) {
664 | mOnCursorStateChangedListener.onShowCursors(SelectableTextView.this);
665 | }
666 | }
667 |
668 |
669 | /**
670 | * hide the cursor and selection
671 | */
672 | public void hide() {
673 | if (mIsShowing) {
674 | SelectableTextView.this.removeSelection();
675 | mStartHandle.hide();
676 | mEndHandle.hide();
677 | mIsShowing = false;
678 |
679 | if (mOnCursorStateChangedListener != null) {
680 | mOnCursorStateChangedListener.onHideCursors(SelectableTextView.this);
681 | }
682 | }
683 | }
684 |
685 |
686 | /**
687 | * redraw the moving cursor and update the selection if required
688 | *
689 | * @param cursorHandle the CursorHandle (LEFT or RIGHT)
690 | * @param x the x coordinate the cursor is pointing to on the screen (raw)
691 | * @param y the y coordinate the cursor is pointing to on the screen (raw)
692 | * @param oldx the previous x position the cursor pointed to
693 | * @param oldy the previous y position the cursor pointed to
694 | */
695 | public void updatePosition(CursorHandle cursorHandle, int x, int y, int oldx, int oldy) {
696 | if (!mIsShowing) {
697 | return;
698 | }
699 |
700 | int old_offset =
701 | cursorHandle == mStartHandle ?
702 | SelectableTextView.this.getCursorSelection().getStart() :
703 | SelectableTextView.this.getCursorSelection().getEnd();
704 |
705 | int offset = SelectableTextView.this.getHysteresisOffset(x, y, old_offset);
706 |
707 | if (offset != old_offset) {
708 |
709 | if (cursorHandle == mStartHandle) {
710 | SelectableTextView.this.getCursorSelection().setStart(offset);
711 | }
712 | else {
713 | SelectableTextView.this.getCursorSelection().setEnd(offset);
714 | }
715 | SelectableTextView.this.getCursorSelection().select();
716 | }
717 |
718 | cursorHandle.pointTo(x, y);
719 |
720 | if (mOnCursorStateChangedListener != null) {
721 | mOnCursorStateChangedListener.onPositionChanged(SelectableTextView.this, x, y, oldx, oldy);
722 | }
723 | }
724 |
725 | /**
726 | * Select the textview between start and end.
727 | *
728 | * Precondition: start, end must be valid
729 | *
730 | * @param start the starting offset
731 | * @param end the ending offset
732 | */
733 | private void select(int start, int end) {
734 | SelectableTextView.this.setSelection(Math.min(start, end), Math.abs(end - start));
735 | }
736 |
737 | @SuppressWarnings("unused")
738 | public boolean isShowing() {
739 | return mIsShowing;
740 | }
741 |
742 | /**
743 | * Called when the view is detached from window. Perform house keeping task, such as stopping
744 | * Runnable thread that would otherwise keep a reference on the context, thus preventing the
745 | * activity from being recycled.
746 | */
747 | @SuppressWarnings("unused")
748 | public void onDetached() {
749 | // don't know what to do here
750 | }
751 |
752 |
753 | @Override
754 | public void onTouchModeChanged(boolean isInTouchMode) {
755 | if (!isInTouchMode) {
756 | hide();
757 | }
758 | }
759 | }
760 |
761 |
762 | /**
763 | * represents a single cursor
764 | */
765 | private class CursorHandle extends View {
766 |
767 | /**
768 | * the {@link PopupWindow} containing the cursor drawable
769 | */
770 | private final PopupWindow mContainer;
771 |
772 | /**
773 | * the drawble of the cursor
774 | */
775 | private Drawable mDrawable;
776 |
777 | /**
778 | * whether the user is dragging the cursor
779 | */
780 | @SuppressWarnings("unused")
781 | private boolean mIsDragging;
782 |
783 | /**
784 | * the controller that's controlling the cursor
785 | */
786 | private SelectionCursorController mController;
787 |
788 | /**
789 | * the height of the cursor
790 | */
791 | private int mHeight;
792 |
793 | /**
794 | * the width of the cursor
795 | */
796 | private int mWidth;
797 |
798 | /**
799 | * the x coordinate of the "pointer" of the cursor
800 | */
801 | private int mHotspotX;
802 |
803 | /**
804 | * the y coordinate of the "pointer" of the cursor which is usually the top, so it's zero.
805 | */
806 | private int mHotspotY;
807 |
808 |
809 | /**
810 | * Adjustment to add to the Raw x, y coordinate of the touch position to get the location of where
811 | * the cursor is pointing to
812 | */
813 | private int mAdjustX;
814 | private int mAdjustY;
815 |
816 | private int mOldX;
817 | private int mOldY;
818 |
819 | public CursorHandle(SelectionCursorController controller) {
820 | super(SelectableTextView.this.getContext());
821 |
822 | mController = controller;
823 |
824 | mDrawable = getResources().getDrawable(R.drawable.cursor);
825 |
826 | /* My Note
827 | At first I tried using mContainer = new PopupWindow(SelectableTextView.this.getContext())
828 | and mContainer.setContentView(this) in the show() method AND FAILED to draw the
829 | PopupWindow properly (e.g. PopupWindow can't contain the whole drawable, background
830 | of the PopupWindow is not transparent. I think it's because calling
831 | new PopupWindow(context) uses the internal's default style which messes up
832 | */
833 | mContainer = new PopupWindow(this);
834 | // mContainer.setSplitTouchEnabled(true);
835 | mContainer.setClippingEnabled(false);
836 |
837 | /* My Note
838 | getIntrinsicWidth() returns the width of the drawable after it has been
839 | scaled to the current device's density
840 | e.g. if the drawable is a 15 x 20 image and we load the image on a Nexus 4 (which
841 | has a density of 2.0), getIntrinsicWidth() shall return 15 * 2 = 30
842 | */
843 | mHeight = mDrawable.getIntrinsicHeight();
844 | mWidth = mDrawable.getIntrinsicWidth();
845 |
846 | // the PopupWindow has an initial dimension of (0, 0)
847 | // must set the width/height of the popupwindow in order for it to be drawn
848 | mContainer.setWidth(mWidth);
849 | mContainer.setHeight(mHeight);
850 |
851 | // this is the location of where the pointer is relative to the cursor itself
852 | // if the left and right cursor are different, mHotspotX will need to be calculated
853 | // differently for each cursor. Currently, I'm using the same left and right cursor
854 | mHotspotX = mWidth / 2;
855 | mHotspotY = 0;
856 |
857 | invalidate();
858 | }
859 |
860 |
861 | @Override
862 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
863 | setMeasuredDimension(mWidth, mHeight);
864 | }
865 |
866 | @Override
867 | protected void onDraw(Canvas canvas) {
868 | mDrawable.setBounds(0, 0, mWidth, mHeight);
869 | mDrawable.draw(canvas);
870 | }
871 |
872 | @Override
873 | public boolean /*CursorHandle::*/onTouchEvent(MotionEvent event) {
874 |
875 | int rawX = (int) event.getRawX();
876 | int rawY = (int) event.getRawY();
877 |
878 | switch (event.getAction()) {
879 | case MotionEvent.ACTION_DOWN: {
880 | // calculate distance from the (x,y) of the finger to where the cursor
881 | // points to
882 | mAdjustX = mHotspotX - (int) event.getX();
883 | mAdjustY = mHotspotY - (int) event.getY();
884 | mOldX = mAdjustX + rawX;
885 | mOldY = mAdjustY + rawY;
886 |
887 | mIsDragging = true;
888 | if (SelectableTextView.this.mOnCursorStateChangedListener != null) {
889 | SelectableTextView.this.mOnCursorStateChangedListener.onDragStarts(SelectableTextView.this);
890 | }
891 | break;
892 | }
893 | case MotionEvent.ACTION_UP:
894 | case MotionEvent.ACTION_CANCEL: {
895 | mIsDragging = false;
896 | mController.snapToSelection();
897 | break;
898 | }
899 | case MotionEvent.ACTION_MOVE: {
900 | // calculate the raw (x, y) the cursor is POINTING TO
901 | int x = mAdjustX + rawX;
902 | int y = mAdjustY + rawY;
903 |
904 | mController.updatePosition(this, x, y, mOldX, mOldY);
905 |
906 | mOldX = x;
907 | mOldY = y;
908 | break;
909 | }
910 | }
911 | return true; // consume the event
912 | }
913 |
914 |
915 | public boolean isShowing() {
916 | return mContainer.isShowing();
917 | }
918 |
919 | /**
920 | * Show the cursor pointing to the specified point.
921 | *
922 | * @param x the x coordinate of the point relative to the TextView
923 | * @param y the y coordinate of the point relative to the TextView
924 | */
925 | public void show(int x, int y) {
926 | final int[] coords = mTempCoords;
927 | SelectableTextView.this.getLocationInWindow(coords);
928 |
929 | coords[0] += x - mHotspotX;
930 | coords[1] += y - mHotspotY;
931 | mContainer.showAtLocation(SelectableTextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
932 | }
933 |
934 | /**
935 | * move the cursor to point the (x,y) location on the screen.
936 | *
937 | * @param x the x coordinate on the screen
938 | * @param y the y coordinate on the screen
939 | */
940 | private void pointTo(int x, int y) {
941 | if (isShowing()) {
942 | mContainer.update(x - mHotspotX, y - mHotspotY, -1, -1);
943 | }
944 | }
945 |
946 |
947 | /**
948 | * hide this cursor
949 | */
950 | public void hide() {
951 | mIsDragging = false;
952 | mContainer.dismiss();
953 | }
954 |
955 | @SuppressWarnings("unused")
956 | public void dismiss() {
957 | mContainer.dismiss();
958 | onDetached();
959 | }
960 |
961 | private void onDetached() {
962 | // hide action windows
963 |
964 | // This is not used but kept for future reference.
965 | // Since no action windows is associate with the cursor, nothing is removed.
966 | // I've made the action window a task of the View using SelectableTextView instead of
967 | // embedding it in here.
968 | }
969 |
970 | }
971 |
972 | public interface OnCursorStateChangedListener {
973 | /**
974 | * What to do when the cursors is hidden from the view
975 | *
976 | * @param v the view the cursor belongs to
977 | */
978 | public void onHideCursors(View v);
979 |
980 | /**
981 | * What to do when the cursors show
982 | *
983 | * @param v the view the cursor belongs to
984 | */
985 | public void onShowCursors(View v);
986 |
987 | /**
988 | * What to do when the drag begins
989 | *
990 | * @param v the view the cursor belongs to
991 | */
992 | public void onDragStarts(View v);
993 |
994 | public void onPositionChanged(View v, int x, int y, int oldx, int oldy);
995 | }
996 |
997 | }
998 |
999 | /*
1000 | Note for future me:
1001 | The "LINE" for layout uses 0 based index e.g. getLineForVertical
1002 | */
1003 |
--------------------------------------------------------------------------------
/src/com/zyz/mobile/example/SelectionInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2013 Ray Zhou
3 |
4 | JadeRead is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | JadeRead is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with JadeRead. If not, see
16 |
17 | Author: Ray Zhou
18 | Date: 2013 06 03
19 |
20 | */
21 | package com.zyz.mobile.example;
22 |
23 |
24 | import android.text.Spannable;
25 | import android.text.Spanned;
26 | import android.text.style.CharacterStyle;
27 |
28 | /**
29 | * class to hold the selection information
30 | */
31 | public class SelectionInfo {
32 |
33 | private Object mSpan;
34 | private int mStart;
35 | private int mEnd;
36 | private Spannable mSpannable;
37 |
38 | public SelectionInfo() {
39 | clear();
40 | }
41 |
42 | public SelectionInfo(CharSequence text, Object span, int start, int end) {
43 | set(text, span, start, end);
44 | }
45 |
46 | /**
47 | * select the {@link #getSpannable()} between the offsets {@link this.getStart()} and
48 | * {@link #getEnd()}
49 | */
50 | public void select() {
51 | select(mSpannable);
52 | }
53 |
54 | public void select(Spannable text) {
55 | if (text != null) {
56 | text.removeSpan(mSpan);
57 | text.setSpan(mSpan, Math.min(mStart, mEnd), Math.max(mStart, mEnd), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
58 | }
59 | }
60 |
61 | /**
62 | * remove the the selection
63 | */
64 | public void remove() {
65 | remove(mSpannable);
66 | }
67 |
68 | public void remove(Spannable text) {
69 | if (text != null) {
70 | text.removeSpan(mSpan);
71 | }
72 | }
73 |
74 | public void clear() {
75 | mSpan = null;
76 | mSpannable = null;
77 | mStart = 0;
78 | mEnd = 0;
79 | }
80 |
81 | public void set(Object span, int start, int end) {
82 | mSpan = span;
83 | mStart = start;
84 | mEnd = end;
85 | }
86 |
87 | public void set(CharSequence text, Object span, int start, int end) {
88 | if (text instanceof Spannable) {
89 | mSpannable = (Spannable) text;
90 | }
91 | set(span, start, end);
92 | }
93 |
94 | public CharSequence getSelectedText() {
95 | if (mSpannable != null) {
96 | int start = Math.min(mStart, mEnd);
97 | int end = Math.max(mStart, mEnd);
98 |
99 | if (start >= 0 && end < mSpannable.length()) {
100 | return mSpannable.subSequence(start, end);
101 | }
102 | }
103 | return "";
104 | }
105 |
106 | public Object getSpan() {
107 | return mSpan;
108 | }
109 |
110 | public void setSpan(CharacterStyle span) {
111 | mSpan = span;
112 | }
113 |
114 |
115 | /**
116 | * get the starting offset of the selection. Note the the starting offset is
117 | * not necessarily smaller than the ending offset
118 | * @return the starting offset of the selection
119 | */
120 | public int getStart() {
121 | return mStart;
122 | }
123 |
124 | /**
125 | * set the starting offset of the selection (inclusive)
126 | * @param start the starting offset. It can be larger than {@link #getEnd()}
127 | */
128 | public void setStart(int start) {
129 | assert (start >= 0);
130 | mStart = start;
131 | }
132 |
133 | /**
134 | * get the ending offset of the selection. Note the the ending offset is
135 | * not necessarily larger than the starting offset
136 | * @return the ending offset of the selection
137 | */
138 | public int getEnd() {
139 | return mEnd;
140 | }
141 |
142 | /**
143 | * set the ending offset of the selection (exclusive)
144 | * @param end the ending offset. It can be smaller than {@link #getStart()}
145 | */
146 | public void setEnd(int end) {
147 | assert (end >= 0);
148 | mEnd = end;
149 | }
150 |
151 | public Spannable getSpannable() {
152 | return mSpannable;
153 | }
154 |
155 | public void setSpannable(Spannable spannable) {
156 | mSpannable = spannable;
157 | }
158 |
159 | /**
160 | * Checks the weather the specified offset is within the range of the selection
161 | * @param offset the offset to check
162 | * @return true if the offset is within the range of the selection, false otherwise.
163 | */
164 | public boolean offsetInSelection(int offset) {
165 | return (offset >= mStart && offset <= mEnd) ||
166 | (offset >= mEnd && offset <= mStart);
167 | }
168 | }
--------------------------------------------------------------------------------