壬戌之秋,七月既望,苏子与客泛舟游于赤壁之下。清风徐来,水波不兴。举酒属客,诵明月之诗,歌窈窕之章。少焉,月出于东山之上,徘徊于斗牛之间。白露横江,水光接天。纵一苇之所如,凌万顷之茫然。浩浩乎如冯虚御风,而不知其所止;飘飘乎如遗世独立,羽化而登仙。︵冯 通:凭︶
于是饮酒乐甚,扣舷而歌之。歌曰:﹃桂棹兮兰桨,击空明兮溯流光。渺渺兮予怀,望美人兮天一方。﹄客有吹洞箫者,倚歌而和之。其声呜呜然,如怨如慕,如泣如诉;余音袅袅,不绝如缕。舞幽壑之潜蛟,泣孤舟之嫠妇。
苏子愀然,正襟危坐,而问客曰:﹃何为其然也?﹄客曰:﹃﹁月明星稀,乌鹊南飞。﹂此非曹孟德之诗乎?西望夏口,东望武昌,山川相缪,郁乎苍苍,此非孟德之困于周郎者乎?方其破荆州,下江陵,顺流而东也,舳舻千里,旌旗蔽空,酾酒临江,横槊赋诗,固一世之雄也,而今安在哉?况吾与子渔樵于江渚之上,侣鱼虾而友麋鹿,驾一叶之扁舟,举匏樽以相属。寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长 江之无穷。挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。﹄
苏子曰:﹃客亦知夫水与月乎?逝者如斯,而未尝往也;盈虚者如彼,而卒莫消长也。盖将自其变者而观之,则天地曾不能以一瞬;自其不变者而观之,则物与我皆无尽也,而又何羡乎!且夫天地之间,物各有主,苟非吾之所有,虽一毫而莫取。惟江上之清风,与山间之明月,耳得之而为声,目遇之而成色,取之无禁,用之不竭。是造物者之无尽藏也,而吾与子之所共适。﹄︵共适 一作:共食︶
客喜而笑,洗盏更酌。肴核既尽,杯盘狼籍。相与枕藉乎舟中,不知东方之既白。
"; 10 | public static String str_cbf = "壬戌之秋,七月既望,蘇子與客泛舟遊於赤壁之下。清風徐來,水波不興。舉酒屬客,誦明月之詩,歌窈窕之章。少焉,月出於東山之上,徘徊於鬥牛之間。白露橫江,水光接天。縱壹葦之所如,淩萬頃之茫然。浩浩乎如馮虛禦風,而不知其所止;飄飄乎如遺世獨立,羽化而登仙。︵馮 通:憑︶
於是飲酒樂甚,扣舷而歌之。歌曰:﹃桂桌兮蘭槳,擊空明兮溯流光。渺渺兮予懷,望美人兮天壹方。﹄客有吹洞簫者,倚歌而和之。其聲嗚嗚然,如怨如慕,如泣如訴;余音裊裊,不絕如縷。舞幽壑之潛蛟,泣孤舟之嫠婦。
蘇子愀然,正襟危坐,而問客曰:﹃何為其然也?﹄客曰:﹃﹁月明星稀,烏鵲南飛。﹂此非曹孟德之詩乎?西望夏口,東望武昌,山川相繆,郁乎蒼蒼,此非孟德之困於周郎者乎?方其破荊州,下江陵,順流而東也,舳艫千裏,旌旗蔽空,釃酒臨江,橫槊賦詩,固壹世之雄也,而今安在哉?況吾與子漁樵於江諸之上,侶魚蝦而友麋鹿,駕壹葉之扁舟,舉匏樽以相屬。寄蜉蝣於天地,渺滄海之壹粟。哀吾生之須臾,羨長 江之無窮。挾飛仙以遨遊,抱明月而長終。知不可乎驟得,托遺響於悲風。﹄
蘇子曰:﹃客亦知夫水與月乎?逝者如斯,而未嘗往也;盈虛者如彼,而卒莫消長也。蓋將自其變者而觀之,則天地曾不能以壹瞬;自其不變者而觀之,則物與我皆無盡也,而又何羨乎!且夫天地之間,物各有主,茍非吾之所有,雖壹毫而莫取。惟江上之清風,與山間之明月,耳得之而為聲,目遇之而成色,取之無禁,用之不竭。是造物者之無盡藏也,而吾與子之所共適。﹄︵共適 壹作:共食︶
客喜而笑,洗盞更酌。肴核既盡,杯盤狼籍。相與枕藉乎舟中,不知東方之既白。
\n"; 11 | public static String str_en = "renxu autumn July, Jiwang, Suzi and guest boating tours under Chibi. Xu breeze, rippleless. Wine is off, chanting the moon poetry, song slim chapter. A little while, months of Dongshan, wandering in the bullfight. Dew Yokoe, Shuiguang next day. Even a reed like, Ling Wan Qing loss. The vast Feng seems such as virtual Yufeng, and not to stop; waving as aloof, emergence and immortal. (Feng Tong)
and drinking music, I and song. The Song said: \"Zhao Gui Xi Lan paddle upstream come blow out light and air. You come to my arms, at beauty come one day.\" A blowing flute, and the Yi song. The hum of course, such as resentment, such as mu of bamboo, linger on faintly, as if weeping and complaining. Dance Youhe the cry of a hidden dragon boat.
Suzi stern, sat, and asked the guests said: \"what is it?\" The guests said: \"yuemingxingxi, Ukraine magpie flying south.\" This is not a poem of Cao Mengde? Seomang Xiakou, looking east Wuchang mountains, Miao Yu, between green and the non shuro Meng moral trapped in between? The broken Jingzhou, Jiangling, East River, a convoy of ships thousands of miles, empty word flags, pour wine, having a lance sideward and poetizing Linjiang, a hero of the age and where is also solid,? Kuangwu and son, in Jiang Nagisa, Lu fish and shrimp and the friends of the elk, riding a leaf boat, belonging to lift bottle gourd. Send ephemera in the world, a boundless sea. Sad moment of my life, the infinite envy of Yangtze river. With the fly to roam, hold the moon and long end. Don't know will suddenly left, supporting ring in Beifeng.\"
"; 12 | public static String str_muti = "壬戌之秋,renxu autumn July, 七月既望,Jiwang, 苏子与客泛舟游于赤壁之下。Suzi and guest boating tours under Chibi.清风徐来,水波不兴。 Xu breeze, rippleless. 举酒属客,Wine is off, 诵明月之诗,chanting the moon poetry, 歌窈窕之章。song slim chapter. 少焉,A little while, 月出于东山之上,months of Dongshan, 徘徊于斗牛之间。wandering in the bullfight. 白露横江,Dew Yokoe, 水光接天。Shuiguang next day. 纵一苇之所如,Even a reed like, 凌万顷之茫然。Ling Wan Qing loss. 浩浩乎如冯虚御风,The vast Feng seems such as virtual Yufeng, 而不知其所止;and not to stop; 飘飘乎如遗世独立,waving as aloof, 羽化而登仙。emergence and immortal. (冯 通:凭)(Feng Tong)
于是饮酒乐甚,then drinking music very, 扣舷而歌之。 I and song. 歌曰:“桂棹兮兰桨,击空明兮溯流光。渺渺兮予怀,望美人兮天一方。”。The Song said: \"Zhao Gui Xi Lan paddle upstream come blow out light and air. You come to my arms, at beauty come one day.\"。客有吹洞箫者,A blowing flute, 倚歌而和之。 and the Yi song. 其声呜呜然,The hum of course, 如怨如慕,such as resentment, 如泣如诉;such as mu of bamboo, 余音袅袅,linger on faintly, 不绝如缕。as if weeping and complaining. 舞幽壑之潜蛟,泣孤舟之嫠妇。Dance Youhe the cry of a hidden dragon boat.
"; 13 | public static String str_hhl = "登黄鹤楼\n昔人已乘黄鹤去\n此地空余黄鹤楼\n黄鹤一去不复返\n白云千载空悠悠\n晴川历历汉阳树\n芳草萋萋鹦鹉洲\n日暮乡关何处是\n烟波江上使人愁"; 14 | public static String str_juaner = "采采卷耳,不盈頃筐。\n嗟我懷人,寘彼周行。\n︵寘 通:置︶\n陟彼崔嵬,我馬虺隤。\n我姑酌彼金罍,\n維以不永懷。\n陟彼高岡,我馬玄黃。\n我姑酌彼兕觥,\n維以不永傷。\n陟彼砠矣,我馬瘏矣,\n我仆痡矣,雲何籲矣。"; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/zp/smaple/VertActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 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 com.zp.smaple; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.text.Html; 23 | import android.view.View; 24 | import android.widget.HorizontalScrollView; 25 | import android.widget.RadioGroup; 26 | import android.widget.Toast; 27 | 28 | import androidx.appcompat.app.AppCompatActivity; 29 | 30 | import com.devilist.advancedtextview.ActionMenu; 31 | import com.devilist.advancedtextview.CustomActionMenuCallBack; 32 | import com.devilist.advancedtextview.VerticalTextView; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | /** 38 | * Created by zengp on 2017/12/2. 39 | */ 40 | 41 | public class VertActivity extends AppCompatActivity implements 42 | RadioGroup.OnCheckedChangeListener, CustomActionMenuCallBack { 43 | 44 | private RadioGroup rg_text_orient, rg_text_underline; 45 | 46 | private VerticalTextView vtv_text_ltr; 47 | private HorizontalScrollView scroll_rtl; 48 | 49 | public static void start(Context context) { 50 | Intent starter = new Intent(context, VertActivity.class); 51 | context.startActivity(starter); 52 | } 53 | 54 | @Override 55 | protected void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | setContentView(R.layout.activity_vert); 58 | init(); 59 | } 60 | 61 | private void init() { 62 | scroll_rtl = (HorizontalScrollView) findViewById(R.id.scroll_rtl); 63 | vtv_text_ltr = (VerticalTextView) findViewById(R.id.vtv_text_ltr); 64 | vtv_text_ltr.setText(Html.fromHtml(StringContentUtil.str_cbf).toString()); 65 | 66 | vtv_text_ltr.setLeftToRight(true) 67 | .setLineSpacingExtra(10) 68 | .setCharSpacingExtra(2) 69 | .setUnderLineText(true) 70 | .setShowActionMenu(true) 71 | .setUnderLineColor(0xffCEAD53) 72 | .setUnderLineWidth(1.0f) 73 | .setUnderLineOffset(3) 74 | .setTextHighlightColor(0xffCEAD53) 75 | .setCustomActionMenuCallBack(this); 76 | 77 | vtv_text_ltr.setOnClickListener(new View.OnClickListener() { 78 | @Override 79 | public void onClick(View v) { 80 | Toast.makeText(VertActivity.this, "onClick事件", Toast.LENGTH_SHORT).show(); 81 | } 82 | }); 83 | 84 | rg_text_orient = findViewById(R.id.rg_text_orient); 85 | rg_text_underline = findViewById(R.id.rg_text_underline); 86 | rg_text_orient.setOnCheckedChangeListener(this); 87 | rg_text_underline.setOnCheckedChangeListener(this); 88 | } 89 | 90 | @Override 91 | public void onCheckedChanged(RadioGroup group, int checkedId) { 92 | switch (checkedId) { 93 | case R.id.rb_ltr: 94 | vtv_text_ltr.setLeftToRight(true); 95 | vtv_text_ltr.requestLayout(); 96 | scroll_rtl.fullScroll(View.FOCUS_LEFT); 97 | break; 98 | case R.id.rb_rtl: 99 | vtv_text_ltr.setLeftToRight(false); 100 | vtv_text_ltr.requestLayout(); 101 | scroll_rtl.fullScroll(View.FOCUS_RIGHT); 102 | break; 103 | case R.id.rb_show: 104 | vtv_text_ltr.setUnderLineText(true); 105 | vtv_text_ltr.requestLayout(); 106 | break; 107 | case R.id.rb_hidden: 108 | vtv_text_ltr.setUnderLineText(false); 109 | vtv_text_ltr.requestLayout(); 110 | break; 111 | } 112 | } 113 | 114 | @Override 115 | public boolean onCreateCustomActionMenu(ActionMenu menu) { 116 | List1:长按文字弹出ActionMenu菜单;菜单menu可以自定义;实现自定义功能(复制,全选,翻译,分享等;默认实现了全选和复制功能) 52 | *
2:文本两端对齐功能;适用于中文文本,英文文本 以及中英混合文本 53 | * Created by zengpu on 2016/11/20. 54 | */ 55 | public class SelectableTextView extends EditText { 56 | 57 | private final int TRIGGER_LONGPRESS_TIME_THRESHOLD = 300; // 触发长按事件的时间阈值 58 | private final int TRIGGER_LONGPRESS_DISTANCE_THRESHOLD = 10; // 触发长按事件的位移阈值 59 | 60 | private Context mContext; 61 | private int mScreenHeight; // 屏幕高度 62 | private int mStatusBarHeight; // 状态栏高度 63 | private int mActionMenuHeight; // 弹出菜单高度 64 | private int mTextHighlightColor;// 选中文字背景高亮颜色 65 | 66 | private float mTouchDownX = 0; 67 | private float mTouchDownY = 0; 68 | private float mTouchDownRawY = 0; 69 | 70 | private boolean isLongPress = false; // 是否发触了长按事件 71 | private boolean isLongPressTouchActionUp = false; // 长按事件结束后,标记该次事件 72 | private boolean isVibrator = false; // 是否触发过长按震动 73 | 74 | private boolean isTextJustify = true; // 是否需要两端对齐 ,默认true 75 | private boolean isForbiddenActionMenu = false; // 是否需要两端对齐 ,默认false 76 | 77 | private boolean isActionSelectAll = false; // 是否触发全选事件 78 | 79 | private int mStartLine; //action_down触摸事件 起始行 80 | private int mStartTextOffset; //action_down触摸事件 字符串开始位置的偏移值 81 | private int mCurrentLine; // action_move触摸事件 当前行 82 | private int mCurrentTextOffset; //action_move触摸事件 字符串当前位置的偏移值 83 | 84 | private int mViewTextWidth; // SelectableTextView内容的宽度(不包含padding) 85 | 86 | private Vibrator mVibrator; 87 | private PopupWindow mActionMenuPopupWindow; // 长按弹出菜单 88 | private ActionMenu mActionMenu = null; 89 | 90 | private OnClickListener mOnClickListener; 91 | private CustomActionMenuCallBack mCustomActionMenuCallBack; 92 | 93 | public SelectableTextView(Context context) { 94 | this(context, null); 95 | } 96 | 97 | public SelectableTextView(Context context, AttributeSet attrs) { 98 | this(context, attrs, 0); 99 | } 100 | 101 | public SelectableTextView(Context context, AttributeSet attrs, int defStyleAttr) { 102 | super(context, attrs, defStyleAttr); 103 | this.mContext = context; 104 | 105 | TypedArray mTypedArray = context.obtainStyledAttributes(attrs, 106 | R.styleable.SelectableTextView); 107 | isTextJustify = mTypedArray.getBoolean(R.styleable.SelectableTextView_textJustify, true); 108 | isForbiddenActionMenu = mTypedArray.getBoolean(R.styleable.SelectableTextView_forbiddenActionMenu, false); 109 | mTextHighlightColor = mTypedArray.getColor(R.styleable.SelectableTextView_textHeightColor, 0x60ffeb3b); 110 | mTypedArray.recycle(); 111 | 112 | init(); 113 | } 114 | 115 | private void init() { 116 | WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 117 | mScreenHeight = wm.getDefaultDisplay().getHeight(); 118 | mStatusBarHeight = Utils.getStatusBarHeight(mContext); 119 | mActionMenuHeight = Utils.dp2px(mContext, 45); 120 | 121 | mVibrator = (Vibrator) mContext.getSystemService(VIBRATOR_SERVICE); 122 | 123 | if (isTextJustify) 124 | setGravity(Gravity.TOP); 125 | 126 | setTextIsSelectable(true); 127 | setCursorVisible(false); 128 | 129 | setTextHighlightColor(mTextHighlightColor); 130 | } 131 | 132 | @Override 133 | public boolean getDefaultEditable() { 134 | // 返回false,屏蔽掉系统自带的ActionMenu 135 | return false; 136 | } 137 | 138 | public void setTextJustify(boolean textJustify) { 139 | isTextJustify = textJustify; 140 | } 141 | 142 | public void setForbiddenActionMenu(boolean forbiddenActionMenu) { 143 | isForbiddenActionMenu = forbiddenActionMenu; 144 | } 145 | 146 | public void setTextHighlightColor(int color) { 147 | this.mTextHighlightColor = color; 148 | String color_hex = String.format("%08X", color); 149 | color_hex = "#40" + color_hex.substring(2); 150 | setHighlightColor(Color.parseColor(color_hex)); 151 | } 152 | 153 | @Override 154 | public void setOnClickListener(OnClickListener l) { 155 | super.setOnClickListener(l); 156 | if (null != l) { 157 | mOnClickListener = l; 158 | } 159 | } 160 | 161 | @Override 162 | public boolean onTouchEvent(MotionEvent event) { 163 | int action = event.getAction(); 164 | Layout layout = getLayout(); 165 | int currentLine; // 当前所在行 166 | 167 | switch (action) { 168 | case MotionEvent.ACTION_DOWN: 169 | Log.d("SelectableTextView", "ACTION_DOWN"); 170 | 171 | // 每次按下时,创建ActionMenu菜单,创建不成功,屏蔽长按事件 172 | if (null == mActionMenu) { 173 | mActionMenu = createActionMenu(); 174 | } 175 | mTouchDownX = event.getX(); 176 | mTouchDownY = event.getY(); 177 | mTouchDownRawY = event.getRawY(); 178 | isLongPress = false; 179 | isVibrator = false; 180 | isLongPressTouchActionUp = false; 181 | break; 182 | case MotionEvent.ACTION_MOVE: 183 | Log.d("SelectableTextView", "ACTION_MOVE"); 184 | // 先判断是否禁用了ActionMenu功能,以及ActionMenu是否创建失败, 185 | // 二者只要满足了一个条件,退出长按事件 186 | if (!isForbiddenActionMenu || mActionMenu.getChildCount() == 0) { 187 | // 手指移动过程中的字符偏移 188 | currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY()); 189 | int mWordOffset_move = layout.getOffsetForHorizontal(currentLine, (int) event.getX()); 190 | // 判断是否触发长按事件 191 | if (event.getEventTime() - event.getDownTime() >= TRIGGER_LONGPRESS_TIME_THRESHOLD 192 | && Math.abs(event.getX() - mTouchDownX) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD 193 | && Math.abs(event.getY() - mTouchDownY) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD) { 194 | 195 | Log.d("SelectableTextView", "ACTION_MOVE 长按"); 196 | isLongPress = true; 197 | isLongPressTouchActionUp = false; 198 | mStartLine = currentLine; 199 | mStartTextOffset = mWordOffset_move; 200 | 201 | // 每次触发长按时,震动提示一次 202 | if (!isVibrator) { 203 | mVibrator.vibrate(30); 204 | isVibrator = true; 205 | } 206 | } 207 | if (isLongPress) { 208 | 209 | if (!isTextJustify) 210 | requestFocus(); 211 | mCurrentLine = currentLine; 212 | mCurrentTextOffset = mWordOffset_move; 213 | // 通知父布局不要拦截触摸事件 214 | getParent().requestDisallowInterceptTouchEvent(true); 215 | // 选择字符 216 | Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mWordOffset_move), 217 | Math.max(mStartTextOffset, mWordOffset_move)); 218 | } 219 | } 220 | break; 221 | case MotionEvent.ACTION_UP: 222 | Log.d("SelectableTextView", "ACTION_UP"); 223 | // 处理长按事件 224 | if (isLongPress) { 225 | currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY()); 226 | int mWordOffsetEnd = layout.getOffsetForHorizontal(currentLine, (int) event.getX()); 227 | // 至少选中一个字符 228 | mCurrentLine = currentLine; 229 | mCurrentTextOffset = mWordOffsetEnd; 230 | int maxOffset = getEditableText().length() - 1; 231 | if (mStartTextOffset > maxOffset) 232 | mStartTextOffset = maxOffset; 233 | if (mCurrentTextOffset > maxOffset) 234 | mCurrentTextOffset = maxOffset; 235 | if (mCurrentTextOffset == mStartTextOffset) { 236 | if (mCurrentTextOffset == layout.getLineEnd(currentLine) - 1) 237 | mStartTextOffset -= 1; 238 | else 239 | mCurrentTextOffset += 1; 240 | } 241 | 242 | 243 | Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mCurrentTextOffset), 244 | Math.max(mStartTextOffset, mCurrentTextOffset)); 245 | // 计算菜单显示位置 246 | int mPopWindowOffsetY = calculatorActionMenuYPosition((int) mTouchDownRawY, (int) event.getRawY()); 247 | // 弹出菜单 248 | showActionMenu(mPopWindowOffsetY, mActionMenu); 249 | isLongPressTouchActionUp = true; 250 | isLongPress = false; 251 | 252 | } else if (event.getEventTime() - event.getDownTime() < TRIGGER_LONGPRESS_TIME_THRESHOLD) { 253 | // 由于onTouchEvent最终返回了true,onClick事件会被屏蔽掉,因此在这里处理onClick事件 254 | if (null != mOnClickListener) 255 | mOnClickListener.onClick(this); 256 | } 257 | // 通知父布局继续拦截触摸事件 258 | getParent().requestDisallowInterceptTouchEvent(false); 259 | break; 260 | } 261 | return true; 262 | } 263 | 264 | /* ***************************************************************************************** */ 265 | // 创建ActionMenu部分 266 | 267 | /** 268 | * 创建ActionMenu菜单 269 | * 270 | * @return 271 | */ 272 | private ActionMenu createActionMenu() { 273 | // 创建菜单 274 | ActionMenu actionMenu = new ActionMenu(mContext); 275 | // 是否需要移除默认item 276 | boolean isRemoveDefaultItem = false; 277 | if (null != mCustomActionMenuCallBack) { 278 | isRemoveDefaultItem = mCustomActionMenuCallBack.onCreateCustomActionMenu(actionMenu); 279 | } 280 | if (!isRemoveDefaultItem) 281 | actionMenu.addDefaultMenuItem(); // 添加默认item 282 | 283 | actionMenu.addCustomItem(); // 添加自定义item 284 | actionMenu.setFocusable(true); // 获取焦点 285 | actionMenu.setFocusableInTouchMode(true); 286 | 287 | if (actionMenu.getChildCount() != 0) { 288 | // item监听 289 | for (int i = 0; i < actionMenu.getChildCount(); i++) { 290 | actionMenu.getChildAt(i).setOnClickListener(mMenuClickListener); 291 | } 292 | } 293 | return actionMenu; 294 | } 295 | 296 | /** 297 | * 长按弹出菜单 298 | * 299 | * @param offsetY 300 | * @param actionMenu 301 | * @return 菜单创建成功,返回true 302 | */ 303 | private void showActionMenu(int offsetY, ActionMenu actionMenu) { 304 | 305 | mActionMenuPopupWindow = new PopupWindow(actionMenu, WindowManager.LayoutParams.WRAP_CONTENT, 306 | mActionMenuHeight, true); 307 | mActionMenuPopupWindow.setFocusable(true); 308 | mActionMenuPopupWindow.setOutsideTouchable(false); 309 | mActionMenuPopupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); 310 | mActionMenuPopupWindow.showAtLocation(this, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY); 311 | 312 | mActionMenuPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { 313 | @Override 314 | public void onDismiss() { 315 | Selection.removeSelection(getEditableText()); 316 | // 如果设置了分散对齐,ActionMenu销毁后,强制刷新一次,防止出现文字背景未消失的情况 317 | if (isTextJustify) 318 | SelectableTextView.this.postInvalidate(); 319 | } 320 | }); 321 | } 322 | 323 | /** 324 | * 隐藏菜单 325 | */ 326 | private void hideActionMenu() { 327 | if (null != mActionMenuPopupWindow) { 328 | mActionMenuPopupWindow.dismiss(); 329 | mActionMenuPopupWindow = null; 330 | } 331 | } 332 | 333 | /** 334 | * 菜单点击事件监听 335 | */ 336 | private OnClickListener mMenuClickListener = new OnClickListener() { 337 | @Override 338 | public void onClick(View v) { 339 | 340 | String menuItemTitle = (String) v.getTag(); 341 | 342 | // 选中的字符的开始和结束位置 343 | int start = getSelectionStart(); 344 | int end = getSelectionEnd(); 345 | // 获得选中的字符 346 | String selected_str; 347 | if (start < 0 || end < 0 || end <= start) { 348 | selected_str = ""; 349 | } else 350 | selected_str = getText().toString().substring(start, end); 351 | 352 | if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_SELECT_ALL)) { 353 | //全选事件 354 | if (isTextJustify) { 355 | mStartLine = 0; 356 | mCurrentLine = getLayout().getLineCount() - 1; 357 | mStartTextOffset = 0; 358 | mCurrentTextOffset = getLayout().getLineEnd(mCurrentLine); 359 | isActionSelectAll = true; 360 | SelectableTextView.this.invalidate(); 361 | } 362 | Selection.selectAll(getEditableText()); 363 | 364 | } else if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_COPY)) { 365 | // 复制事件 366 | Utils.copyText(mContext, selected_str); 367 | Toast.makeText(mContext, "复制成功!", Toast.LENGTH_SHORT).show(); 368 | hideActionMenu(); 369 | 370 | } else { 371 | // 自定义事件 372 | if (null != mCustomActionMenuCallBack) { 373 | mCustomActionMenuCallBack.onCustomActionItemClicked(menuItemTitle, selected_str); 374 | } 375 | hideActionMenu(); 376 | } 377 | } 378 | }; 379 | 380 | /** 381 | * 计算弹出菜单相对于父布局的Y向偏移 382 | * 383 | * @param yOffsetStart 所选字符的起始位置相对屏幕的Y向偏移 384 | * @param yOffsetEnd 所选字符的结束位置相对屏幕的Y向偏移 385 | * @return 386 | */ 387 | private int calculatorActionMenuYPosition(int yOffsetStart, int yOffsetEnd) { 388 | if (yOffsetStart > yOffsetEnd) { 389 | int temp = yOffsetStart; 390 | yOffsetStart = yOffsetEnd; 391 | yOffsetEnd = temp; 392 | } 393 | int actionMenuOffsetY; 394 | 395 | if (yOffsetStart < mActionMenuHeight * 3 / 2 + mStatusBarHeight) { 396 | if (yOffsetEnd > mScreenHeight - mActionMenuHeight * 3 / 2) { 397 | // 菜单显示在屏幕中间 398 | actionMenuOffsetY = mScreenHeight / 2 - mActionMenuHeight / 2; 399 | } else { 400 | // 菜单显示所选文字下方 401 | actionMenuOffsetY = yOffsetEnd + mActionMenuHeight / 2; 402 | } 403 | } else { 404 | // 菜单显示所选文字上方 405 | actionMenuOffsetY = yOffsetStart - mActionMenuHeight * 3 / 2; 406 | } 407 | return actionMenuOffsetY; 408 | } 409 | 410 | /* ***************************************************************************************** */ 411 | // 两端对齐部分 412 | 413 | @Override 414 | protected void onDraw(Canvas canvas) { 415 | Log.d("SelectableTextView", "onDraw"); 416 | if (!isTextJustify) { 417 | // 不需要两端对齐 418 | super.onDraw(canvas); 419 | 420 | } else { 421 | //textview内容的实际宽度 422 | mViewTextWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 423 | // 重绘文字,两端对齐 424 | drawTextWithJustify(canvas); 425 | // 绘制选中文字的背景,触发以下事件时需要绘制背景: 426 | // 1.长按事件 2.全选事件 3.手指滑动过快时,进入ACTION_UP事件后, 427 | // 可能会出现背景未绘制的情况 428 | if (isLongPress | isActionSelectAll | isLongPressTouchActionUp) { 429 | drawSelectedTextBackground(canvas); 430 | isActionSelectAll = false; 431 | isLongPressTouchActionUp = false; 432 | } 433 | } 434 | } 435 | 436 | /** 437 | * 重绘文字,两端对齐 438 | * 439 | * @param canvas 440 | */ 441 | private void drawTextWithJustify(Canvas canvas) { 442 | // 文字画笔 443 | TextPaint textPaint = getPaint(); 444 | textPaint.setColor(getCurrentTextColor()); 445 | textPaint.drawableState = getDrawableState(); 446 | 447 | String text_str = getText().toString(); 448 | // 当前所在行的Y向偏移 449 | int currentLineOffsetY = getPaddingTop(); 450 | currentLineOffsetY += getTextSize(); 451 | 452 | Layout layout = getLayout(); 453 | 454 | //循环每一行,绘制文字 455 | for (int i = 0; i < layout.getLineCount(); i++) { 456 | int lineStart = layout.getLineStart(i); 457 | int lineEnd = layout.getLineEnd(i); 458 | //获取到TextView每行中的内容 459 | String line_str = text_str.substring(lineStart, lineEnd); 460 | // 获取每行字符串的宽度(不包括字符间距) 461 | float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint()); 462 | 463 | if (isLineNeedJustify(line_str)) { 464 | //最后一行不需要重绘 465 | if (i == layout.getLineCount() - 1) { 466 | canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint); 467 | } else { 468 | drawJustifyTextForLine(canvas, line_str, desiredWidth, currentLineOffsetY); 469 | } 470 | } else { 471 | canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint); 472 | } 473 | //更新行Y向偏移 474 | currentLineOffsetY += getLineHeight(); 475 | } 476 | } 477 | 478 | /** 479 | * 绘制选中的文字的背景 480 | * 481 | * @param canvas 482 | */ 483 | private void drawSelectedTextBackground(Canvas canvas) { 484 | if (mStartTextOffset == mCurrentTextOffset) 485 | return; 486 | 487 | // 文字背景高亮画笔 488 | Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 489 | highlightPaint.setStyle(Paint.Style.FILL); 490 | highlightPaint.setColor(mTextHighlightColor); 491 | highlightPaint.setAlpha(60); 492 | 493 | // 计算开始位置和结束位置的字符相对view最左侧的x偏移 494 | float startToLeftPosition = calculatorCharPositionToLeft(mStartLine, mStartTextOffset); 495 | float currentToLeftPosition = calculatorCharPositionToLeft(mCurrentLine, mCurrentTextOffset); 496 | 497 | // 行高 498 | int h = getLineHeight(); 499 | int paddingTop = getPaddingTop(); 500 | int paddingLeft = getPaddingLeft(); 501 | 502 | // 创建三个矩形,分别对应: 503 | // 所有选中的行对应的矩形,起始行左侧未选中文字的对应的矩形,结束行右侧未选中的文字对应的矩形 504 | RectF rect_all, rect_lt, rect_rb; 505 | // sdk版本控制 506 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 507 | if (mStartTextOffset < mCurrentTextOffset) { 508 | rect_all = new RectF(paddingLeft, mStartLine * h + paddingTop, 509 | mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop); 510 | rect_lt = new RectF(paddingLeft, mStartLine * h + paddingTop, 511 | startToLeftPosition, (mStartLine + 1) * h + paddingTop); 512 | rect_rb = new RectF(currentToLeftPosition, mCurrentLine * h + paddingTop, 513 | mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop); 514 | } else { 515 | rect_all = new RectF(paddingLeft, mCurrentLine * h + paddingTop, 516 | mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop); 517 | rect_lt = new RectF(paddingLeft, mCurrentLine * h + paddingTop, 518 | currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop); 519 | rect_rb = new RectF(startToLeftPosition, mStartLine * h + paddingTop, 520 | mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop); 521 | } 522 | 523 | // 创建三个路径,分别对应上面三个矩形 524 | Path path_all = new Path(); 525 | Path path_lt = new Path(); 526 | Path path_rb = new Path(); 527 | path_all.addRect(rect_all, Path.Direction.CCW); 528 | path_lt.addRect(rect_lt, Path.Direction.CCW); 529 | path_rb.addRect(rect_rb, Path.Direction.CCW); 530 | // 将左上角和右下角的矩形从path_all中减去 531 | path_all.addRect(rect_all, Path.Direction.CCW); 532 | path_all.op(path_lt, Path.Op.DIFFERENCE); 533 | path_all.op(path_rb, Path.Op.DIFFERENCE); 534 | 535 | canvas.drawPath(path_all, highlightPaint); 536 | 537 | } else { 538 | Path path_all = new Path(); 539 | path_all.moveTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop); 540 | path_all.lineTo(startToLeftPosition, mStartLine * h + paddingTop); 541 | path_all.lineTo(mViewTextWidth + paddingLeft, mStartLine * h + paddingTop); 542 | path_all.lineTo(mViewTextWidth + paddingLeft, mCurrentLine * h + paddingTop); 543 | path_all.lineTo(currentToLeftPosition, mCurrentLine * h + paddingTop); 544 | path_all.lineTo(currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop); 545 | path_all.lineTo(paddingLeft, (mCurrentLine + 1) * h + paddingTop); 546 | path_all.lineTo(paddingLeft, (mStartLine + 1) * h + paddingTop); 547 | path_all.lineTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop); 548 | 549 | canvas.drawPath(path_all, highlightPaint); 550 | } 551 | // canvas.restore(); 552 | } 553 | 554 | /** 555 | * 重绘此行,两端对齐 556 | * 557 | * @param canvas 558 | * @param line_str 该行所有的文字 559 | * @param desiredWidth 该行每个文字的宽度的总和 560 | * @param currentLineOffsetY 该行的Y向偏移 561 | */ 562 | private void drawJustifyTextForLine(Canvas canvas, String line_str, float desiredWidth, int currentLineOffsetY) { 563 | 564 | // 画笔X方向的偏移 565 | float lineTextOffsetX = getPaddingLeft(); 566 | // 判断是否是首行 567 | if (isFirstLineOfParagraph(line_str)) { 568 | String blanks = " "; 569 | // 画出缩进空格 570 | canvas.drawText(blanks, lineTextOffsetX, currentLineOffsetY, getPaint()); 571 | // 空格需要的宽度 572 | float blank_width = StaticLayout.getDesiredWidth(blanks, getPaint()); 573 | // 更新画笔X方向的偏移 574 | lineTextOffsetX += blank_width; 575 | line_str = line_str.substring(3); 576 | } 577 | 578 | // 计算相邻字符(或单词)之间需要填充的宽度,英文按单词处理,中文按字符处理 579 | // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符或单词个数-1) 580 | if (isContentABC(line_str)) { 581 | // 该行包含英文,以空格分割单词 582 | String[] line_words = line_str.split(" "); 583 | // 计算相邻单词间需要插入的空白 584 | float insert_blank = mViewTextWidth - desiredWidth; 585 | if (line_words.length > 1) 586 | insert_blank = (mViewTextWidth - desiredWidth) / (line_words.length - 1); 587 | // 遍历单词 588 | for (int i = 0; i < line_words.length; i++) { 589 | // 判断分割后的每一个单词;如果是纯英文,按照纯英文单词处理,直接在画布上画出单词; 590 | // 如果包括汉字,则按照汉字字符处理,逐个字符绘画 591 | // 如果只有一个单词,按中文处理 592 | // 最后一个单词按照纯英文单词处理 593 | String word_i = line_words[i] + " "; 594 | if (line_words.length == 1 || (isContentHanZi(word_i) && i < line_words.length - 1)) { 595 | // 单词按照汉字字符处理 596 | // 计算单词中相邻字符间需要插入的空白 597 | float insert_blank_word_i = insert_blank; 598 | if (word_i.length() > 1) 599 | insert_blank_word_i = insert_blank / (word_i.length() - 1); 600 | // 遍历单词中字符,依次绘画 601 | for (int j = 0; j < word_i.length(); j++) { 602 | String word_i_char_j = String.valueOf(word_i.charAt(j)); 603 | float word_i_char_j_width = StaticLayout.getDesiredWidth(word_i_char_j, getPaint()); 604 | canvas.drawText(word_i_char_j, lineTextOffsetX, currentLineOffsetY, getPaint()); 605 | // 更新画笔X方向的偏移 606 | lineTextOffsetX += word_i_char_j_width + insert_blank_word_i; 607 | } 608 | } else { 609 | //单词按照纯英文处理 610 | float word_i_width = StaticLayout.getDesiredWidth(word_i, getPaint()); 611 | canvas.drawText(word_i, lineTextOffsetX, currentLineOffsetY, getPaint()); 612 | // 更新画笔X方向的偏移 613 | lineTextOffsetX += word_i_width + insert_blank; 614 | } 615 | } 616 | } else { 617 | // 该行按照中文处理 618 | float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1); 619 | for (int i = 0; i < line_str.length(); i++) { 620 | String char_i = String.valueOf(line_str.charAt(i)); 621 | float char_i_width = StaticLayout.getDesiredWidth(char_i, getPaint()); 622 | canvas.drawText(char_i, lineTextOffsetX, currentLineOffsetY, getPaint()); 623 | // 更新画笔X方向的偏移 624 | lineTextOffsetX += char_i_width + insert_blank; 625 | } 626 | } 627 | } 628 | 629 | /** 630 | * 计算字符距离控件左侧的位移 631 | * 632 | * @param line 字符所在行 633 | * @param charOffset 字符偏移量 634 | */ 635 | private float calculatorCharPositionToLeft(int line, int charOffset) { 636 | 637 | String text_str = getText().toString(); 638 | 639 | 640 | Layout layout = getLayout(); 641 | int lineStart = layout.getLineStart(line); 642 | int lineEnd = layout.getLineEnd(line); 643 | 644 | String line_str = text_str.substring(lineStart, lineEnd); 645 | 646 | if (line_str.equals("\n")) 647 | return getPaddingLeft(); 648 | // 最左侧 649 | if (lineStart == charOffset) 650 | return getPaddingLeft(); 651 | // 最右侧 652 | if (charOffset == lineEnd - 1) 653 | return mViewTextWidth + getPaddingLeft(); 654 | 655 | float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint()); 656 | 657 | // 中间位置 658 | // 计算相邻字符之间需要填充的宽度 659 | // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符个数-1) 660 | float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1); 661 | // 计算当前字符左侧所有字符的宽度 662 | float allLeftCharWidth = StaticLayout.getDesiredWidth(text_str.substring(lineStart, charOffset), getPaint()); 663 | 664 | // 相邻字符之间需要填充的宽度 + 当前字符左侧所有字符的宽度 665 | return insert_blank * (charOffset - lineStart) + allLeftCharWidth + getPaddingLeft(); 666 | 667 | } 668 | 669 | /** 670 | * 判断是不是段落的第一行。一个汉字相当于一个字符,此处判断是否为第一行的依据是: 671 | * 字符长度大于3且前两个字符为空格 672 | * 673 | * @param line 674 | * @return 675 | */ 676 | private boolean isFirstLineOfParagraph(String line) { 677 | return line.length() > 3 && line.charAt(0) == ' ' && line.charAt(1) == ' '; 678 | } 679 | 680 | /** 681 | * 判断该行需不需要缩放;该行最后一个字符不是换行符的时候返回true, 682 | * 该行最后一个字符是换行符的时候返回false 683 | * 684 | * @param line_str 该行的文字 685 | * @return 686 | */ 687 | private boolean isLineNeedJustify(String line_str) { 688 | if (line_str.length() == 0) { 689 | return false; 690 | } else { 691 | return line_str.charAt(line_str.length() - 1) != '\n'; 692 | } 693 | } 694 | 695 | /** 696 | * 判断是否包含英文 697 | * 698 | * @param line_str 699 | * @return 700 | */ 701 | private boolean isContentABC(String line_str) { 702 | String regex = ".*[a-zA-Z]+.*"; 703 | Matcher m = Pattern.compile(regex).matcher(line_str); 704 | return m.matches(); 705 | } 706 | 707 | /** 708 | * 判断是否包含中文 709 | * 710 | * @param word_str 711 | * @return 712 | */ 713 | private boolean isContentHanZi(String word_str) { 714 | // String E1 = "[\u4e00-\u9fa5]";// 中文 715 | String regex = ".*[\\u4e00-\\u9fa5]+.*"; 716 | Matcher m = Pattern.compile(regex).matcher(word_str); 717 | return m.matches(); 718 | } 719 | 720 | /** 721 | * 判断是否是中文标点符号 722 | * 723 | * @param str 724 | * @return 725 | */ 726 | private boolean isUnicodeSymbol(String str) { 727 | String regex = ".*[`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“'。,、?]$+.*"; 728 | Matcher m = Pattern.compile(regex).matcher(str); 729 | return m.matches(); 730 | } 731 | 732 | public void setCustomActionMenuCallBack(CustomActionMenuCallBack callBack) { 733 | this.mCustomActionMenuCallBack = callBack; 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/Utils.java: -------------------------------------------------------------------------------- 1 | package com.devilist.advancedtextview; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * Created by zengp on 2017/12/2. 10 | */ 11 | 12 | public class Utils { 13 | 14 | 15 | /** 16 | * dp2px 17 | */ 18 | public static int dp2px(Context context, float dpValue) { 19 | final float scale = context.getResources().getDisplayMetrics().density; 20 | return (int) (dpValue * scale + 0.5f); 21 | } 22 | 23 | /** 24 | * 实现文本复制功能 25 | * 26 | * @param text 27 | */ 28 | public static void copyText(Context context, String text) { 29 | // 得到剪贴板管理器 30 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { 31 | android.text.ClipboardManager cmb = (android.text.ClipboardManager) context 32 | .getSystemService(Context.CLIPBOARD_SERVICE); 33 | cmb.setText(text.trim()); 34 | } else { 35 | android.content.ClipboardManager cmb = (android.content.ClipboardManager) context 36 | .getSystemService(Context.CLIPBOARD_SERVICE); 37 | cmb.setText(text.trim()); 38 | } 39 | } 40 | 41 | /** 42 | * 状态栏高度 43 | * 44 | * @param context 45 | * @return 46 | */ 47 | public static int getStatusBarHeight(Context context) { 48 | Class> c = null; 49 | Object obj = null; 50 | Field field = null; 51 | int x = 0, statusBarHeight = 0; 52 | try { 53 | c = Class.forName("com.android.internal.R$dimen"); 54 | obj = c.newInstance(); 55 | field = c.getField("status_bar_height"); 56 | x = Integer.parseInt(field.get(obj).toString()); 57 | statusBarHeight = context.getResources().getDimensionPixelSize(x); 58 | 59 | } catch (Exception e1) { 60 | statusBarHeight = 0; 61 | e1.printStackTrace(); 62 | } 63 | return statusBarHeight; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/VerticalTextView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 zengp 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 com.devilist.advancedtextview; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.Paint; 24 | import android.graphics.Path; 25 | import android.graphics.Rect; 26 | import android.graphics.drawable.ColorDrawable; 27 | import android.os.Vibrator; 28 | import android.text.TextPaint; 29 | import android.text.TextUtils; 30 | import android.util.AttributeSet; 31 | import android.util.Log; 32 | import android.util.SparseArray; 33 | import android.view.Gravity; 34 | import android.view.MotionEvent; 35 | import android.view.View; 36 | import android.view.WindowManager; 37 | import android.widget.PopupWindow; 38 | import android.widget.TextView; 39 | import android.widget.Toast; 40 | 41 | import java.lang.reflect.Field; 42 | import java.util.regex.Matcher; 43 | import java.util.regex.Pattern; 44 | 45 | 46 | import static android.content.Context.VIBRATOR_SERVICE; 47 | import static android.view.MotionEvent.ACTION_MOVE; 48 | 49 | /** 50 | * VerticalTextView ———— 实现文字竖排的TextView。 51 | *
功能: 52 | *
1.文字从上到下竖排。 53 | *
2.文字阅读方向可选择 从右向左 和 从左向右。 54 | *
3.长按可选择文本,并弹出自定义的可定制化的菜单ActionMenu 55 | *
56 | * Created by zengpu on 2017/1/20.
57 | */
58 | public class VerticalTextView extends TextView {
59 |
60 | private static String TAG = VerticalTextView.class.getSimpleName();
61 |
62 | private final int TRIGGER_LONGPRESS_TIME_THRESHOLD = 300; // 触发长按事件的时间阈值
63 | private final int TRIGGER_LONGPRESS_DISTANCE_THRESHOLD = 10; // 触发长按事件的位移阈值
64 |
65 | private Context mContext;
66 | private int mScreenWidth; // 屏幕宽度
67 | private int mScreenHeight; // 屏幕高度
68 |
69 | // attrs
70 | private boolean isLeftToRight; // 竖排方向,是否从左到右;默认从右到左
71 | private float mLineSpacingExtra; // 行距 默认 6px
72 | private float mCharSpacingExtra; // 字符间距 默认 6px
73 | private boolean isUnderLineText; // 是否需要下划线,默认false
74 | private int mUnderLineColor; // 下划线颜色 默认 Color.RED
75 | private float mUnderLineWidth; // 下划线线宽 默认 1.5f
76 | private float mUnderLineOffset; // 下划线偏移 默认 3px
77 | private boolean isShowActionMenu; // 是否显示ActionMenu,默认true
78 | private int mTextHighlightColor; // 选中文字背景高亮颜色 默认0x60ffeb3b
79 |
80 | // onMeasure相关
81 | private int[] mTextAreaRoughBound; // 粗略计算的文本最大显示区域(包含padding),用于view的测量和不同Gravity情况下文本的绘制
82 | private int[] mMeasureMode; // 宽高的测量模式
83 |
84 | private SparseArray