Skip to content

增加对Emoji的支持. #4

@huangjie21

Description

@huangjie21

/*

  • Copyright 2017 zengp
  • Licensed under the Apache License, Version 2.0 (the "License");
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an "AS IS" BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License.
    */

package com.devilist.advancedtextview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Vibrator;
import android.text.Layout;
import android.text.Selection;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.PopupWindow;
import android.widget.Toast;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static android.content.Context.VIBRATOR_SERVICE;

/**

  • SelectableTextView ————增强版的TextView,具有以下功能:

  • 1:长按文字弹出ActionMenu菜单;菜单menu可以自定义;实现自定义功能(复制,全选,翻译,分享等;默认实现了全选和复制功能)

  • 2:文本两端对齐功能;适用于中文文本,英文文本 以及中英混合文本

  • Created by zengpu on 2016/11/20.
    */
    public class SelectableTextView extends EditText {

    private final int TRIGGER_LONGPRESS_TIME_THRESHOLD = 300; // 触发长按事件的时间阈值
    private final int TRIGGER_LONGPRESS_DISTANCE_THRESHOLD = 10; // 触发长按事件的位移阈值

    private Context mContext;
    private int mScreenHeight; // 屏幕高度
    private int mStatusBarHeight; // 状态栏高度
    private int mActionMenuHeight; // 弹出菜单高度
    private int mTextHighlightColor;// 选中文字背景高亮颜色

    private float mTouchDownX = 0;
    private float mTouchDownY = 0;
    private float mTouchDownRawY = 0;

    private boolean isLongPress = false; // 是否发触了长按事件
    private boolean isLongPressTouchActionUp = false; // 长按事件结束后,标记该次事件
    private boolean isVibrator = false; // 是否触发过长按震动

    private boolean isTextJustify = true; // 是否需要两端对齐 ,默认true
    private boolean isForbiddenActionMenu = false; // 是否需要两端对齐 ,默认false

    private boolean isActionSelectAll = false; // 是否触发全选事件

    private int mStartLine; //action_down触摸事件 起始行
    private int mStartTextOffset; //action_down触摸事件 字符串开始位置的偏移值
    private int mCurrentLine; // action_move触摸事件 当前行
    private int mCurrentTextOffset; //action_move触摸事件 字符串当前位置的偏移值

    private int mViewTextWidth; // SelectableTextView内容的宽度(不包含padding)

    private Vibrator mVibrator;
    private PopupWindow mActionMenuPopupWindow; // 长按弹出菜单
    private ActionMenu mActionMenu = null;

    private OnClickListener mOnClickListener;
    private CustomActionMenuCallBack mCustomActionMenuCallBack;

    public SelectableTextView(Context context) {
    this(context, null);
    }

    public SelectableTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public SelectableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.mContext = context;

     TypedArray mTypedArray = context.obtainStyledAttributes(attrs,
             R.styleable.SelectableTextView);
     isTextJustify = mTypedArray.getBoolean(R.styleable.SelectableTextView_textJustify, true);
     isForbiddenActionMenu = mTypedArray.getBoolean(R.styleable.SelectableTextView_forbiddenActionMenu, false);
     mTextHighlightColor = mTypedArray.getColor(R.styleable.SelectableTextView_textHeightColor, 0x60ffeb3b);
     mTypedArray.recycle();
    
     init();
    

    }

    private void init() {
    WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    mScreenHeight = wm.getDefaultDisplay().getHeight();
    mStatusBarHeight = Utils.getStatusBarHeight(mContext);
    mActionMenuHeight = Utils.dp2px(mContext, 45);

     mVibrator = (Vibrator) mContext.getSystemService(VIBRATOR_SERVICE);
    
     if (isTextJustify)
         setGravity(Gravity.TOP);
    
     setTextIsSelectable(true);
     setCursorVisible(false);
    
     setTextHighlightColor(mTextHighlightColor);
    

    }

    @OverRide
    public boolean getDefaultEditable() {
    // 返回false,屏蔽掉系统自带的ActionMenu
    return false;
    }

    public void setTextJustify(boolean textJustify) {
    isTextJustify = textJustify;
    }

    public void setForbiddenActionMenu(boolean forbiddenActionMenu) {
    isForbiddenActionMenu = forbiddenActionMenu;
    }

    public void setTextHighlightColor(int color) {
    this.mTextHighlightColor = color;
    String color_hex = String.format("%08X", color);
    color_hex = "#40" + color_hex.substring(2);
    setHighlightColor(Color.parseColor(color_hex));
    }

    @OverRide
    public void setOnClickListener(OnClickListener l) {
    super.setOnClickListener(l);
    if (null != l) {
    mOnClickListener = l;
    }
    }

    @OverRide
    public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    Layout layout = getLayout();
    int currentLine; // 当前所在行

     switch (action) {
         case MotionEvent.ACTION_DOWN:
             Log.d("SelectableTextView", "ACTION_DOWN");
    
             // 每次按下时,创建ActionMenu菜单,创建不成功,屏蔽长按事件
             if (null == mActionMenu) {
                 mActionMenu = createActionMenu();
             }
             mTouchDownX = event.getX();
             mTouchDownY = event.getY();
             mTouchDownRawY = event.getRawY();
             isLongPress = false;
             isVibrator = false;
             isLongPressTouchActionUp = false;
             break;
         case MotionEvent.ACTION_MOVE:
             Log.d("SelectableTextView", "ACTION_MOVE");
             // 先判断是否禁用了ActionMenu功能,以及ActionMenu是否创建失败,
             // 二者只要满足了一个条件,退出长按事件
             if (!isForbiddenActionMenu || mActionMenu.getChildCount() == 0) {
                 // 手指移动过程中的字符偏移
                 currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY());
                 int mWordOffset_move = layout.getOffsetForHorizontal(currentLine, (int) event.getX());
                 // 判断是否触发长按事件
                 if (event.getEventTime() - event.getDownTime() >= TRIGGER_LONGPRESS_TIME_THRESHOLD
                         && Math.abs(event.getX() - mTouchDownX) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD
                         && Math.abs(event.getY() - mTouchDownY) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD) {
    
                     Log.d("SelectableTextView", "ACTION_MOVE 长按");
                     isLongPress = true;
                     isLongPressTouchActionUp = false;
                     mStartLine = currentLine;
                     mStartTextOffset = mWordOffset_move;
    
                     // 每次触发长按时,震动提示一次
                     if (!isVibrator) {
                         mVibrator.vibrate(30);
                         isVibrator = true;
                     }
                 }
                 if (isLongPress) {
    
                     if (!isTextJustify)
                         requestFocus();
                     mCurrentLine = currentLine;
                     mCurrentTextOffset = mWordOffset_move;
                     // 通知父布局不要拦截触摸事件
                     getParent().requestDisallowInterceptTouchEvent(true);
                     // 选择字符
                     Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mWordOffset_move),
                             Math.max(mStartTextOffset, mWordOffset_move));
                 }
             }
             break;
         case MotionEvent.ACTION_UP:
             Log.d("SelectableTextView", "ACTION_UP");
             // 处理长按事件
             if (isLongPress) {
                 currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY());
                 int mWordOffsetEnd = layout.getOffsetForHorizontal(currentLine, (int) event.getX());
                 // 至少选中一个字符
                 mCurrentLine = currentLine;
                 mCurrentTextOffset = mWordOffsetEnd;
                 int maxOffset = getEditableText().length() - 1;
                 if (mStartTextOffset > maxOffset)
                     mStartTextOffset = maxOffset;
                 if (mCurrentTextOffset > maxOffset)
                     mCurrentTextOffset = maxOffset;
                 if (mCurrentTextOffset == mStartTextOffset) {
                     if (mCurrentTextOffset == layout.getLineEnd(currentLine) - 1)
                         mStartTextOffset -= 1;
                     else
                         mCurrentTextOffset += 1;
                 }
    
    
                 Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mCurrentTextOffset),
                         Math.max(mStartTextOffset, mCurrentTextOffset));
                 // 计算菜单显示位置
                 int mPopWindowOffsetY = calculatorActionMenuYPosition((int) mTouchDownRawY, (int) event.getRawY());
                 // 弹出菜单
                 showActionMenu(mPopWindowOffsetY, mActionMenu);
                 isLongPressTouchActionUp = true;
                 isLongPress = false;
    
             } else if (event.getEventTime() - event.getDownTime() < TRIGGER_LONGPRESS_TIME_THRESHOLD) {
                 // 由于onTouchEvent最终返回了true,onClick事件会被屏蔽掉,因此在这里处理onClick事件
                 if (null != mOnClickListener)
                     mOnClickListener.onClick(this);
             }
             // 通知父布局继续拦截触摸事件
             getParent().requestDisallowInterceptTouchEvent(false);
             break;
     }
     return true;
    

    }

    /* ***************************************************************************************** */
    // 创建ActionMenu部分

    /**

    • 创建ActionMenu菜单

    • @return
      */
      private ActionMenu createActionMenu() {
      // 创建菜单
      ActionMenu actionMenu = new ActionMenu(mContext);
      // 是否需要移除默认item
      boolean isRemoveDefaultItem = false;
      if (null != mCustomActionMenuCallBack) {
      isRemoveDefaultItem = mCustomActionMenuCallBack.onCreateCustomActionMenu(actionMenu);
      }
      if (!isRemoveDefaultItem)
      actionMenu.addDefaultMenuItem(); // 添加默认item

      actionMenu.addCustomItem(); // 添加自定义item
      actionMenu.setFocusable(true); // 获取焦点
      actionMenu.setFocusableInTouchMode(true);

      if (actionMenu.getChildCount() != 0) {
      // item监听
      for (int i = 0; i < actionMenu.getChildCount(); i++) {
      actionMenu.getChildAt(i).setOnClickListener(mMenuClickListener);
      }
      }
      return actionMenu;
      }

    /**

    • 长按弹出菜单

    • @param offsetY

    • @param actionMenu

    • @return 菜单创建成功,返回true
      */
      private void showActionMenu(int offsetY, ActionMenu actionMenu) {

      mActionMenuPopupWindow = new PopupWindow(actionMenu, WindowManager.LayoutParams.WRAP_CONTENT,
      mActionMenuHeight, true);
      mActionMenuPopupWindow.setFocusable(true);
      mActionMenuPopupWindow.setOutsideTouchable(false);
      mActionMenuPopupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
      mActionMenuPopupWindow.showAtLocation(this, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY);

      mActionMenuPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
      @OverRide
      public void onDismiss() {
      Selection.removeSelection(getEditableText());
      // 如果设置了分散对齐,ActionMenu销毁后,强制刷新一次,防止出现文字背景未消失的情况
      if (isTextJustify)
      SelectableTextView.this.postInvalidate();
      }
      });
      }

    /**

    • 隐藏菜单
      */
      private void hideActionMenu() {
      if (null != mActionMenuPopupWindow) {
      mActionMenuPopupWindow.dismiss();
      mActionMenuPopupWindow = null;
      }
      }

    /**

    • 菜单点击事件监听
      */
      private OnClickListener mMenuClickListener = new OnClickListener() {
      @OverRide
      public void onClick(View v) {

       String menuItemTitle = (String) v.getTag();
      
       // 选中的字符的开始和结束位置
       int start = getSelectionStart();
       int end = getSelectionEnd();
       // 获得选中的字符
       String selected_str;
       if (start < 0 || end < 0 || end <= start) {
           selected_str = "";
       } else
           selected_str = getText().toString().substring(start, end);
      
       if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_SELECT_ALL)) {
           //全选事件
           if (isTextJustify) {
               mStartLine = 0;
               mCurrentLine = getLayout().getLineCount() - 1;
               mStartTextOffset = 0;
               mCurrentTextOffset = getLayout().getLineEnd(mCurrentLine);
               isActionSelectAll = true;
               SelectableTextView.this.invalidate();
           }
           Selection.selectAll(getEditableText());
      
       } else if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_COPY)) {
           // 复制事件
           Utils.copyText(mContext, selected_str);
           Toast.makeText(mContext, "复制成功!", Toast.LENGTH_SHORT).show();
           hideActionMenu();
      
       } else {
           // 自定义事件
           if (null != mCustomActionMenuCallBack) {
               mCustomActionMenuCallBack.onCustomActionItemClicked(menuItemTitle, selected_str);
           }
           hideActionMenu();
       }
      

      }
      };

    /**

    • 计算弹出菜单相对于父布局的Y向偏移

    • @param yOffsetStart 所选字符的起始位置相对屏幕的Y向偏移

    • @param yOffsetEnd 所选字符的结束位置相对屏幕的Y向偏移

    • @return
      */
      private int calculatorActionMenuYPosition(int yOffsetStart, int yOffsetEnd) {
      if (yOffsetStart > yOffsetEnd) {
      int temp = yOffsetStart;
      yOffsetStart = yOffsetEnd;
      yOffsetEnd = temp;
      }
      int actionMenuOffsetY;

      if (yOffsetStart < mActionMenuHeight * 3 / 2 + mStatusBarHeight) {
      if (yOffsetEnd > mScreenHeight - mActionMenuHeight * 3 / 2) {
      // 菜单显示在屏幕中间
      actionMenuOffsetY = mScreenHeight / 2 - mActionMenuHeight / 2;
      } else {
      // 菜单显示所选文字下方
      actionMenuOffsetY = yOffsetEnd + mActionMenuHeight / 2;
      }
      } else {
      // 菜单显示所选文字上方
      actionMenuOffsetY = yOffsetStart - mActionMenuHeight * 3 / 2;
      }
      return actionMenuOffsetY;
      }

    /* ***************************************************************************************** */
    // 两端对齐部分

    @OverRide
    protected void onDraw(Canvas canvas) {
    Log.d("SelectableTextView", "onDraw");
    if (!isTextJustify) {
    // 不需要两端对齐
    super.onDraw(canvas);

     } else {
         //textview内容的实际宽度
         mViewTextWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
         // 重绘文字,两端对齐
         drawTextWithJustify(canvas);
         // 绘制选中文字的背景,触发以下事件时需要绘制背景:
         // 1.长按事件 2.全选事件 3.手指滑动过快时,进入ACTION_UP事件后,
         // 可能会出现背景未绘制的情况
         if (isLongPress | isActionSelectAll | isLongPressTouchActionUp) {
             drawSelectedTextBackground(canvas);
             isActionSelectAll = false;
             isLongPressTouchActionUp = false;
         }
     }
    

    }

    /**

    • 重绘文字,两端对齐

    • @param canvas
      */
      private void drawTextWithJustify(Canvas canvas) {
      // 文字画笔
      TextPaint textPaint = getPaint();
      textPaint.setColor(getCurrentTextColor());
      textPaint.drawableState = getDrawableState();

      String text_str = getText().toString();
      // 当前所在行的Y向偏移
      int currentLineOffsetY = getPaddingTop();
      currentLineOffsetY += getTextSize();

      Layout layout = getLayout();

      //循环每一行,绘制文字
      for (int i = 0; i < layout.getLineCount(); i++) {
      int lineStart = layout.getLineStart(i);
      int lineEnd = layout.getLineEnd(i);
      //获取到TextView每行中的内容
      String line_str = text_str.substring(lineStart, lineEnd);
      // 获取每行字符串的宽度(不包括字符间距)
      float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint());

       if (isLineNeedJustify(line_str)) {
           //最后一行不需要重绘
           if (i == layout.getLineCount() - 1) {
               canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint);
           } else {
               drawJustifyTextForLine(canvas, line_str, desiredWidth, currentLineOffsetY);
           }
       } else {
           canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint);
       }
       //更新行Y向偏移
       currentLineOffsetY += getLineHeight();
      

      }
      }

    /**

    • 绘制选中的文字的背景

    • @param canvas
      */
      private void drawSelectedTextBackground(Canvas canvas) {
      if (mStartTextOffset == mCurrentTextOffset)
      return;

      // 文字背景高亮画笔
      Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      highlightPaint.setStyle(Paint.Style.FILL);
      highlightPaint.setColor(mTextHighlightColor);
      highlightPaint.setAlpha(60);

      // 计算开始位置和结束位置的字符相对view最左侧的x偏移
      float startToLeftPosition = calculatorCharPositionToLeft(mStartLine, mStartTextOffset);
      float currentToLeftPosition = calculatorCharPositionToLeft(mCurrentLine, mCurrentTextOffset);

      // 行高
      int h = getLineHeight();
      int paddingTop = getPaddingTop();
      int paddingLeft = getPaddingLeft();

      // 创建三个矩形,分别对应:
      // 所有选中的行对应的矩形,起始行左侧未选中文字的对应的矩形,结束行右侧未选中的文字对应的矩形
      RectF rect_all, rect_lt, rect_rb;
      // sdk版本控制
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      if (mStartTextOffset < mCurrentTextOffset) {
      rect_all = new RectF(paddingLeft, mStartLine * h + paddingTop,
      mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop);
      rect_lt = new RectF(paddingLeft, mStartLine * h + paddingTop,
      startToLeftPosition, (mStartLine + 1) * h + paddingTop);
      rect_rb = new RectF(currentToLeftPosition, mCurrentLine * h + paddingTop,
      mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop);
      } else {
      rect_all = new RectF(paddingLeft, mCurrentLine * h + paddingTop,
      mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop);
      rect_lt = new RectF(paddingLeft, mCurrentLine * h + paddingTop,
      currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop);
      rect_rb = new RectF(startToLeftPosition, mStartLine * h + paddingTop,
      mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop);
      }

       // 创建三个路径,分别对应上面三个矩形
       Path path_all = new Path();
       Path path_lt = new Path();
       Path path_rb = new Path();
       path_all.addRect(rect_all, Path.Direction.CCW);
       path_lt.addRect(rect_lt, Path.Direction.CCW);
       path_rb.addRect(rect_rb, Path.Direction.CCW);
       // 将左上角和右下角的矩形从path_all中减去
       path_all.addRect(rect_all, Path.Direction.CCW);
       path_all.op(path_lt, Path.Op.DIFFERENCE);
       path_all.op(path_rb, Path.Op.DIFFERENCE);
      
       canvas.drawPath(path_all, highlightPaint);
      

      } else {
      Path path_all = new Path();
      path_all.moveTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop);
      path_all.lineTo(startToLeftPosition, mStartLine * h + paddingTop);
      path_all.lineTo(mViewTextWidth + paddingLeft, mStartLine * h + paddingTop);
      path_all.lineTo(mViewTextWidth + paddingLeft, mCurrentLine * h + paddingTop);
      path_all.lineTo(currentToLeftPosition, mCurrentLine * h + paddingTop);
      path_all.lineTo(currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop);
      path_all.lineTo(paddingLeft, (mCurrentLine + 1) * h + paddingTop);
      path_all.lineTo(paddingLeft, (mStartLine + 1) * h + paddingTop);
      path_all.lineTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop);

       canvas.drawPath(path_all, highlightPaint);
      

      }
      // canvas.restore();
      }

    /**

    • 重绘此行,两端对齐

    • @param canvas

    • @param line_str 该行所有的文字

    • @param desiredWidth 该行每个文字的宽度的总和

    • @param currentLineOffsetY 该行的Y向偏移
      */
      private void drawJustifyTextForLine(Canvas canvas, String line_str, float desiredWidth, int currentLineOffsetY) {

      // 画笔X方向的偏移
      float lineTextOffsetX = getPaddingLeft();
      // 判断是否是首行
      if (isFirstLineOfParagraph(line_str)) {
      String blanks = " ";
      // 画出缩进空格
      canvas.drawText(blanks, lineTextOffsetX, currentLineOffsetY, getPaint());
      // 空格需要的宽度
      float blank_width = StaticLayout.getDesiredWidth(blanks, getPaint());
      // 更新画笔X方向的偏移
      lineTextOffsetX += blank_width;
      line_str = line_str.substring(3);
      }

      // 计算相邻字符(或单词)之间需要填充的宽度,英文按单词处理,中文按字符处理
      // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符或单词个数-1)
      if (isContentABC(line_str)) {
      // 该行包含英文,以空格分割单词
      String[] line_words = line_str.split(" ");
      // 计算相邻单词间需要插入的空白
      float insert_blank = mViewTextWidth - desiredWidth;
      if (line_words.length > 1)
      insert_blank = (mViewTextWidth - desiredWidth) / (line_words.length - 1);
      // 遍历单词
      for (int i = 0; i < line_words.length; i++) {
      // 判断分割后的每一个单词;如果是纯英文,按照纯英文单词处理,直接在画布上画出单词;
      // 如果包括汉字,则按照汉字字符处理,逐个字符绘画
      // 如果只有一个单词,按中文处理
      // 最后一个单词按照纯英文单词处理
      String word_i = line_words[i] + " ";
      if (line_words.length == 1 || (isContentHanZi(word_i) && i < line_words.length - 1)) {
      // 单词按照汉字字符处理
      // 计算单词中相邻字符间需要插入的空白
      float insert_blank_word_i = insert_blank;
      if (word_i.length() > 1)
      insert_blank_word_i = insert_blank / (word_i.length() - 1);
      // 遍历单词中字符,依次绘画
      for (int j = 0; j < word_i.length(); j++) {
      String word_i_char_j = String.valueOf(word_i.charAt(j));
      //是否emoji
      if(isEmojiCharacter(line_str.charAt(j))){
      j++;
      word_i_char_j=word_i_char_j+word_i.charAt(j);
      }
      float word_i_char_j_width = StaticLayout.getDesiredWidth(word_i_char_j, getPaint());
      canvas.drawText(word_i_char_j, lineTextOffsetX, currentLineOffsetY, getPaint());
      // 更新画笔X方向的偏移
      lineTextOffsetX += word_i_char_j_width + insert_blank_word_i;
      }
      } else {
      //单词按照纯英文处理
      float word_i_width = StaticLayout.getDesiredWidth(word_i, getPaint());
      canvas.drawText(word_i, lineTextOffsetX, currentLineOffsetY, getPaint());
      // 更新画笔X方向的偏移
      lineTextOffsetX += word_i_width + insert_blank;
      }
      }
      } else {
      // 该行按照中文处理
      float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1);
      for (int i = 0; i < line_str.length(); i++) {
      String char_i = String.valueOf(line_str.charAt(i));
      //是否emoji
      if(isEmojiCharacter(line_str.charAt(i))){
      i++;
      char_i=char_i+line_str.charAt(i);
      }
      float char_i_width = StaticLayout.getDesiredWidth(char_i, getPaint());
      canvas.drawText(char_i, lineTextOffsetX, currentLineOffsetY, getPaint());
      // 更新画笔X方向的偏移
      lineTextOffsetX += char_i_width + insert_blank;
      }
      }
      }

    /**

    • 计算字符距离控件左侧的位移

    • @param line 字符所在行

    • @param charOffset 字符偏移量
      */
      private float calculatorCharPositionToLeft(int line, int charOffset) {

      String text_str = getText().toString();

      Layout layout = getLayout();
      int lineStart = layout.getLineStart(line);
      int lineEnd = layout.getLineEnd(line);

      String line_str = text_str.substring(lineStart, lineEnd);

      if (line_str.equals("\n"))
      return getPaddingLeft();
      // 最左侧
      if (lineStart == charOffset)
      return getPaddingLeft();
      // 最右侧
      if (charOffset == lineEnd - 1)
      return mViewTextWidth + getPaddingLeft();

      float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint());

      // 中间位置
      // 计算相邻字符之间需要填充的宽度
      // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符个数-1)
      float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1);
      // 计算当前字符左侧所有字符的宽度
      float allLeftCharWidth = StaticLayout.getDesiredWidth(text_str.substring(lineStart, charOffset), getPaint());

      // 相邻字符之间需要填充的宽度 + 当前字符左侧所有字符的宽度
      return insert_blank * (charOffset - lineStart) + allLeftCharWidth + getPaddingLeft();

    }

    /**

    • 判断是不是段落的第一行。一个汉字相当于一个字符,此处判断是否为第一行的依据是:
    • 字符长度大于3且前两个字符为空格
    • @param line
    • @return
      */
      private boolean isFirstLineOfParagraph(String line) {
      return line.length() > 3 && line.charAt(0) == ' ' && line.charAt(1) == ' ';
      }

    /**

    • 判断该行需不需要缩放;该行最后一个字符不是换行符的时候返回true,
    • 该行最后一个字符是换行符的时候返回false
    • @param line_str 该行的文字
    • @return
      */
      private boolean isLineNeedJustify(String line_str) {
      if (line_str.length() == 0) {
      return false;
      } else {
      return line_str.charAt(line_str.length() - 1) != '\n';
      }
      }

    /**

    • 判断是否包含英文
    • @param line_str
    • @return
      /
      private boolean isContentABC(String line_str) {
      String regex = ".
      [a-zA-Z]+.*";
      Matcher m = Pattern.compile(regex).matcher(line_str);
      return m.matches();
      }

    /**

    • 判断是否包含中文
    • @param word_str
    • @return
      /
      private boolean isContentHanZi(String word_str) {
      // String E1 = "[\u4e00-\u9fa5]";// 中文
      String regex = ".
      [\u4e00-\u9fa5]+.*";
      Matcher m = Pattern.compile(regex).matcher(word_str);
      return m.matches();
      }

    /**

    • 判断是否是中文标点符号
    • @param str
    • @return
      /
      private boolean isUnicodeSymbol(String str) {
      String regex = ".
      [`!@#$^&*()=|{}':;',\[\].<>/?!@#¥……&()——|{}【】‘;:”“'。,、?]$+.";
      Matcher m = Pattern.compile(regex).matcher(str);
      return m.matches();
      }

    public void setCustomActionMenuCallBack(CustomActionMenuCallBack callBack) {
    this.mCustomActionMenuCallBack = callBack;
    }

    //是否是emoji
    private boolean isEmojiCharacter(char codePoint) {

     return !((codePoint == 0x0) ||
             (codePoint == 0x9) ||
             (codePoint == 0xA) ||
             (codePoint == 0xD) ||
             ((codePoint >= 0x20) && (codePoint <= 0xD7FF)) ||
             ((codePoint >= 0xE000) && (codePoint <= 0xFFFD)) ||
             ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF)));
    

    }
    }

我修改了一下,对emoji表情的支持。谢谢楼主的类库,很好很强大

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions