手写板原理

本文探讨了手写板的实现,包括笔锋处理的两种方案、消隐策略、手写板大小计算以及PopupWindow的使用。在触摸事件处理中,通过dispatchTouchEvent和onTouchEvent与手写SDK交互,并在onDraw中进行Canvas绘制。GestureController、GestureCanvas、BrushesPlotter和SpotFilter等组件协同工作,完成手写轨迹的记录和绘制。

笔锋有两种方案:自己画(计算复杂度高且实现效果差)、笔锋图片(存在重合区域)

消隐有两种方案:笔画结束时整体重画(过度绘制)、笔画结束时增加PorterDuff蒙层

计算手写板大小(全屏和半屏)、构造视图层次结构(Background、Margin)、扩展手写板大小:

/*--------------------------------SogouIME.java----------------------------------*/

Rect frame = new Rect();
mCandidateViewContainer.getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top; //Rect(0, 50, 720, 1280)

//left : 0.005, top : 0.0125, height : 0.83, width : 0.845
HandWritingRect hwRect = mKeyboardView.getKeyboard().getHandWritingRect();

int[] candidateViewContainerInWindow = new int[2]; //(0, 0)
mCandidateViewContainer.getLocationInWindow(candidateViewContainerInWindow);

Rect rect = new Rect();
if (isFullScreen) {
    rect.left = 0;
    rect.top = getHWWindowTop();
    rect.right = mKeyboardView.getWidth();
    rect.bottom = candidateViewContainerInWindow[1];
} else {
    rect.left = (int)(mKeyboardView.getWidth() * hwRect.left);
    rect.top = (int)(mKeyboardView.getKeyboard().getKeyboardRealHeight() * hwRect.top);
    rect.right = (rect.left + (int)(mKeyboardView.getKeyboard().getMinWidth() * hwRect.width));
    rect.bottom = rect.top + (int)(mKeyboardView.getKeyboard().getKeyboardRealHeight() * hwRect.height);
}

mHWGestureWindow = new HWGestureWindow(getApplicationContext(), mKeyboardView, !isFullScreen, rect, mode);
mHWGestureWindow.setGestureActionListener(mHWGestureListener);
mHWGestureWindow.showGestureWindow();

public int getHWWindowTop() {
    int keyboardHeight = mKeyboardView.getHeight(); //230
    int candidateHeight = mCandidateViewContainer.getHeight(); //88
    Rect frame = calculateTitleBarHeight(); //Rect(0, 50, 720, 1280)
    int totalHeight = keyboardHeight + candidateHeight - frame.height(); // totalHeight : -912
    return totalHeight;
}

/*--------------------------------HWGestureWindow.java----------------------------------*/

public HWGestureWindow(Context context, View parent, boolean withInKeyboard, Rect viewRect, int mode) {
    super(context);
    mContext = context;
    mParent = parent;
    mWithinKeyboard = withInKeyboard;
    mViewRect = viewRect;
    mMode = mode;
    setBackgroundDrawable(null);
    setClippingEnabled(false);
    mGesturePoints = new short[GESTURE_POINTS_NUM];
    initGestureView(mWithinKeyboard);
}

//View hierarchy for fullScreen :
//PopupWindow -- mRootView -- mHwGestureView
//View hierarchy for halfScreen :
//mKeyboardView -- mHandwritingView -- mHwBgLayout -- mModeTextView(下) & mHwGestureView(上)

public void initGestureView(boolean withInKeyboard) {
    mDensity = mContext.getResources().getDisplayMetrics().density;
    mMinPadding = (int)(2 * mDensity);
    mRootView = new RelativeLayout(mContext);
    mHWGestureView = new HandWriteView(mContext, withInKeyboard, mViewRect);
    if (!withInKeyboard) {
        mRootView.addView(mHWGestureView);
        setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.bg_color)));
    } else {
        LayoutInflater inflater = mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mHandWritingView = inflater.inflate(R.layout.hw_half_layout, null);

        mModeTextView = (TextView) mHandWritingView.findViewById(R.id.hw_mode_tip);
        mTextStyle = KeyboardManager.getInstance(mContext).getTextStyle();
        mModeTextView.setTextColor(mTextStyle.color);
        String contentText = mContext.getString(R.string.hw_mode_overlap);
        mModeTextView.setText(contentText);

        mHWBgLayout = mHandWritingView.findViewById(R.id.hw_bg_layout);
        // 将mHWGestureView添加到mHWBgLayout中,并设置mHWBgLayout的背景
        mHWBg = mKeyboardView.getKeyboard().getHandWritingBG();
        mHWBgLayout.addView(mHWGestureView);
        mHWBgLayout.setBackgroundDrawable(mHWBg);
        // mHWGestureView相对mHWBgLayout的布局信息
        mBGPaddingRect = mKeyboardView.getKeyboard().getHandWritingPaddingRect();
    }
    setContentView(mRootView);
}

public void showGestureWindow() {
    if (mWithinKeyboard) {
        if (mKeyboardView.indexOfChild(mHandWrtingView) < 0) {
            mKeyboardView.addView(mHandWrtingView);
        }
        // mViewRect(3, 7, 611, 485) 设置layout的大小和margin信息
        // mBGPaddingRect(4, 4, 4, 4) 设置View的大小和margin信息
        final ViewGroup.MarginLayoutParams mlp = mHWBgLayout.getLayoutParams();
        mlp.width = mViewRect.width();
        mlp.height = mViewRect.height();
        mlp.leftMargin = mViewRect.left;
        mlp.topMargin = mViewRect.top;
        mHWBgLayout.setMinimumWidth(mViewRect.width());
        mHWBgLayout.setMinimumHeight(mViewRect.height());
        mHWBgLayout.setVisibility(View.VISIBLE);
        mHWBgLayout.invalidate();
        final ViewGroup.MarginLayoutParams mlp2 = mHWGestureView.getLayoutParams();
        mlp2.width = mViewRect.width() - mBGPaddingRect.left - mBGPaddingRect.right;
        mlp2.height = mViewRect.height() - mBGPaddingRect.top - mBGPaddingRect.bottom;
        mlp2.leftMargin = mBGPaddingRect.left;
        mlp2.topMargin = mBGPaddingRect.top;
        mHWGestureView.setMinimumWidth(mViewRect.width() - mBGPaddingRect.left - mBGPaddingRect.right);
        mHWGestureView.setMinimumHeight(mViewRect.height() - mBGPaddingRect.top - mBGPaddingRect.bottom);
        mHWGestureView.setVisibility(View.VISIBLE);
        mHWGestureView.invalidate();
    } else {
        // mViewRect(0, -912, 720, 0) 设置View的大小
        // mBGPaddingRect(0, 0, 0, 0) 设置View的margin信息
        this.setHeight(mViewRect.height());
        this.setWidth(mViewRect.width());
        final ViewGroup.MarginLayoutParams mlp2 = mHWGestureView.getLayoutParams();
        mlp2.width = mViewRect.width() - mBGPaddingRect.left - mBGPaddingRect.right;
        mlp2.height = mViewRect.height() - mBGPaddingRect.top - mBGPaddingRect.bottom;
        mlp2.leftMargin = mBGPaddingRect.left;
        mlp2.topMargin = mBGPaddingRect.top;
        mHWGestureView.setMinimumWidth(mViewRect.width() - mBGPaddingRect.left - mBGPaddingRect.right);
        mHWGestureView.setMinimumHeight(mViewRect.height() - mBGPaddingRect.top - mBGPaddingRect.bottom);
        mHWGestureView.setVisibility(View.VISIBLE);
        mHWGestureView.invalidate();
        try {
            update();
            int x = mViewRect.left;
            int y = mViewRect.top;
            mLastY = y;
            showAtLocation(mParent, Gravity.NO_GRAVITY, x, y);
        } catch (Exception ignore) {
        }
    }
}

dispatchTouchEvent函数主要有两个功能:计算坐标点路径长度,进而增加手写板大小或者销毁手写板并显示监控窗口

private void touchMove(MotionEvent event) {
  final float x = event.getX();
  final float y = event.getY();
  final float dx = Math.abs(x - mX);
  final float dy = Math.abs(y - mY);
  if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
    mX = x;
    mY = y;
    if (mTotalLength <= mGestureStrokeLengthThreshold) {
      mTotalLength += (float) Math.sqrt(dx * dx + dy * dy);
    }
  }
}

case MotionEvent.ACTION_MOVE:
  if (mTotalLength > mGestureStrokeLengthThreshold) {
    mOnGestureThroughListener.onGestureThroughEnded(this, event);
    mOnGestureEnable = false;
  }
  break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
  mOnGestureThroughListener.onGestureThroughEnded(this, event);
  mOnGestureEnable = false;
  break;


public boolean onGestureThroughEnded(HandWriteView overlay, MotionEvent event) {
  if (event.getAction() == MotionEvent.ACTION_UP) {
    if (mIMEStatus.isFullScreenHW() && !mHWIsWaitForEnd) {
      if (!mComposer.isEmpty()) {
        pickSuggestionHandWriting();
      }
      showHWMonitorWindow();
      mHWIsWaitForEnd = false;
    }
  } else {
    if (mIMEStatus.isFullScreenHW() && !mHWIsWaitForEnd) {
      int candidateViewContainerLocation[] = new int[2];
      mCandidateViewContainer.getLocationOnScreen(candidateViewContainerLocation);
      int candidateViewContainerInWindow[] = new int[2];
      mCandidateViewContainer.getLocationInWindow(candidateViewContainerInWindow);
      mHWGestureWindow.updateGestureWindow(0,
          candidateViewContainerInWindow[1] - candidateViewContainerLocation[1] + mTitleBarHeight, 
          mKeyboardView.getWidth(), 
          mCandidateViewContainer.getHeight() + mKeyboardView.getHeight() +
          candidateViewContainerLocation[1] - mTitleBarHeight);
      mHWIsWaitForEnd = true;
    }
  }
  return true;
}

private void showHWMonitorWindow() {
  if (mHWMonitorWindow == null) {
    mHWMonitorView = new View(getApplicationContext());
    mHWMonitorView.setBackgroundDrawable(null);
    mHWMonitorView.setFocusable(false);
    mHWMonitorWindow = 
                   new SPopupWindow(mHWMonitorView, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
    mHWMonitorWindow.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.transparent));
    mHWMonitorWindow.setClippingEnabled(false);
    mHWMonitorWindow.setOutsideTouchable(true);
    mHWMonitorWindow.setTouchable(true);
    mHWMonitorWindow.setFocusable(false);
    mHWMonitorWindow.setHeight(1);
    mHWMonitorWindow.setWidth(1);
    mHWMonitorWindow.setTouchInterceptor(new View.OnTouchListener() {
      public boolean onTouch(View v, MotionEvent event) {
        mHWMonitorWindow.dismiss();
        if (mIMEStatus.isFullScreenHW()) {
          mHandler.sendEmptyMessage(MSG_SHOW_HW_WINDOW);
        }
        return true;
      }
    });
  }
  mHWMonitorWindow.showAtLocation(mCandidateViewContainer, Gravity.NO_GRAVITY, 0, 0);
}

PopupWindow:showAtLocation相对于窗口显示、showAsDropDown相对于View显示。另外,需要判断当前View的WindowToken是否存在

    @Override
    public void showAtLocation(View parent, int gravity, int x, int y) {
        try {
            if (parent != null && parent.getWindowToken() != null
                                       && parent.getWindowToken().isBinderAlive()){
                super.showAtLocation(parent, gravity, x, y);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

onTouchEvent函数有三个作用:向手写SDK传递数据、在ACTION_UP中发送延时消息(重置标记),在ACTION_DOWN中清除延时消息、Canvas绘制

case MotionEvent.ACTION_DOWN:
    mHandler.removeMessages(MSG_END_POINT);
    pushPoint(mClickX, mClickY);
    mGestureController.handleTouch(event);
    break;
case MotionEvent.ACTION_UP:
    pushPoint(mClickX, mClickY);
    mHandler.sendEmptyMessageDelayed(MSG_END_POINT, mEndWaitTime);
    mGestureController.handleTouch(event);
    break;

onDraw函数用于绘制手写板

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mGestureController.draw(canvas);
}

下面主要介绍Canvas绘制及笔锋消隐效果

在GestureController的构造方法中会初始化GestureCanvas对象,用于在onDraw中直接绘制Canvas

//宽高信息
public HandWriteView(Context context, boolean withInKeyboard, Rect viewRect) {
  DisplayMetrics dm = context.getResources().getDisplayMetrics();
  int width = dm.widthPixels;   //720
  int height = dm.heightPixels; //1080
  if (withInKeyboard && viewRect != null) {
    width = viewRect.width();   //608
    height = viewRect.height(); //478
  }
  mGestureController = new GestureController(mContext, width, height);
}

public GestureController(Context mContext, int width, int height) {
  this.mContext = mContext;
  mGestureCanvas = new GestureCanvas(width, height, Bitmap.Config.ARGB_4444);
  mSpotFilter = new SpotFilter(SMOOTHING_FILTER_LEN, this);
  mGestureStroker = new GestureStroker(context);
  mGestureStroker.setInvalidateListener(mInvalidateListener);
}

//直接绘制Canvas
public void draw(Canvas canvas) {
  if (mAllowAlpha && !mIsSingleCharMode) {
    mGestureCanvas.drawAlphaTo(canvas, 0, 0, null, mIsSplitWord, false);
    mAllowAlpha = false;
    mIsSplitWord = false;
  } else {
    mGestureCanvas.drawTo(canvas, 0, 0, null, false);
  }
}

HandWriteView中的点击事件会传给GestureController处理

//HandWriteView.java

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    float mClickX = event.getX();
    float mClickY = event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        mGestureController.handleTouch(event);
        break;
    }
    invalidate();
    return true;
  }

在GestureController的handleTouch中会调用BrushesPlotter(绘图机)的add方法,进而调用SpotFilter的add方法,如果没有过滤掉该节点,则继续调用BrushesPlotter的plot方法

//GestureController.java

  public void plot(Spot s) {
    mGestureStroker.strokeTo(mEffectCanvas, s);
  }

  public void add(GestureController mGestureController, Spot s) {
    mSpotFilter.add(mGestureController, s);
  }

  void handleTouch(MotionEvent event) {
    int action = event.getAction();
    long time = event.getEventTime();
    int N = event.getHistorySize();
    switch (action) {
      case MotionEvent.ACTION_DOWN:
        mTmpSpot.update(event.getX(), event.getY(),
                        event.getSize(), event.getSize(),
                        time, MotionEvent.ACTION_DOWN, mFirstPointWidth, action);
        add(this, mTmpSpot);
        break;
      case MotionEvent.ACTION_MOVE:
        for (int i = 0; i < N; i++) {
          mTmpSpot.update(event.getHistoricalX(i), event.getHistoricalY(i),
                          event.getHistoricalSize(i), event.getHistoricalSize(i),
                          event.getHistoricalEventTime(i), MotionEvent.ACTION_MOVE,
                          mFirstPointWidth, action);
          add(this, mTmpSpot);
        }
        mTmpSpot.update(event.getX(), event.getY(),
                        event.getSize(), event.getSize(), 
                        time, MotionEvent.ACTION_MOVE, mFirstPointWidth, action);
        add(this, mTmpSpot);
        break;
      case MotionEvent.ACTION_UP:
        mTmpSpot.update(event.getX(), event.getY(),
                        event.getSize(), event.getSize(),
                        time, MotionEvent.ACTION_UP, mFirstPointWidth, action);
        add(this, mTmpSpot);
        break;
    }
  }

//SpotFilter.java

  public void add(GestureController mGestureController, Spot c) {
    addNoCopy(mGestureController, new Spot(c));
  }

  protected void addNoCopy(GestureController mGestureController, Spot c) {
    if (mSpots.size() == mBufSize) {
      mSpots.remove(mBufSize - 2);
    }

    Spot cc = new Spot();
    tmpSpot = filtered(cc, c);

    if (tmpSpot != null) {
      mSpots.add(0, cc);
      mGestureController.plot(tmpSpot);
    }
  }

plot方法会调用GestureStroker的strokeTo方法,GestureStroker主要完成了线程封装,其中,strokeTo方法会把当前节点添加到mSpotList中,如果线程没启动,则启动线程,另外,strokeTo方法会传入在GestureController中初始化好的Canvas对象,Paint对象在GestureStroker中已经初始化了,代码如下:

//GestureStroker.java

  RectF strokeTo(GestureCanvas c, Spot s) {
    s.lastX = mLastSpot.x;
    s.lastY = mLastSpot.y;
    s.lastR = mLastSpot.width;
    mLastSpot = s;
    mCanvas = c;
    synchronized (LOCK) {
      mSpotList.add(s);
      LOCK.notify();
    }
    drawThread();
    return tmpDirtyRectF;
  }

  private void drawThread() {
    if (!mRunning) {
      mRunning = true;
      mThreadPool.execute(mThread);
    }
  }

  private void initThread() {
    mThreadPool = Executors.newSingleThreadExecutor();
    mThread = new Thread(new Runnable() {
      @Override
      public void run() {
        while (mRunning) {
          try {
            synchronized (LOCK) {
              if (!mSpotList.isEmpty()) {
                activeList.clear();
                activeList.addAll(mSpotList);
                mSpotList.clear();
              }
            }
            if (!activeList.isEmpty()) {
              for (Spot spot : activeList) {
                execute(spot);
              }
              activeList.clear();
            }
            synchronized (LOCK) {
              if (mSpotList.isEmpty()) {
                LOCK.wait();
              }
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
        mRunning = false;
      }
    });
  }

  private void execute(Spot s) {
    if (s.lastR < 0) {
      drawStrokePoint(mCanvas, s.x, s.y, s.width, tmpDirtyRectF);
    } else {
      if (s.x == s.lastX && s.y == s.lastY) {
        if (mListener != null) {
          mListener.invalidateAll(s.action == MotionEvent.ACTION_UP);
        }
        return;
      }
      float mLastLen = dist(s.lastX, s.lastY, s.x, s.y);
      if (mLastLen == 0) {
        if (mListener != null) {
          mListener.invalidateAll(s.action == MotionEvent.ACTION_UP);
        }
        return;
      }

      float xi, yi, ri, frac;
      float d = 0;

      while (true) {
        if (d > mLastLen) {
          break;
        }
        frac = d == 0 ? 0 : (d / mLastLen);
        ri = lerp(s.lastR, s.width, frac);
        xi = lerp(s.lastX, s.x, frac);
        yi = lerp(s.lastY, s.y, frac);
        drawStrokePoint(mCanvas, xi, yi, ri, tmpDirtyRectF);
        if (ri <= THRESH) {
          d += mOffset;
        } else {
          d += Math.sqrt(SLOPE * Math.pow(ri - THRESH, 2) + mOffset);
        }
      }
      if (mListener != null && mHasActionUp) {
        mListener.invalidateAll(s.action == MotionEvent.ACTION_UP);
      }
    }
  }

  private void drawStrokePoint(GestureCanvas c, float x, float y, float r, RectF dirty) {
    tmpRF.set(x - r, y - r, x + r, y + r);
    c.drawBitmap(mBiFengPenBits, mBiFengPenBitsFrame, tmpRF, mPaint);
  }

//GestureCanvas.java

  @Override
  public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
      if (mDrawUnits == null || mDrawUnits.mIsRecycled) return;
      getDrawingCanvas(mDrawUnits).drawBitmap(bitmap, src, dst, paint);
      mDrawUnits.dirty = true;
  }

PotterDuff效果:

mDrawUnits.getCanvas().drawColor(Color.parseColor(mFadeColor), PorterDuff.Mode.DST_OUT);
if (mDrawUnits.getBitmap() != null) {
   drawCanvas.drawBitmap(mDrawUnits.getBitmap(), 0, 0, paint);
}
//DST_OUT:[Da * (1 - Sa), Dc * (1 - Sa)]

画笔设置:

mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
mPaint.setColor(mColor);
mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
//SRC_OVER:在目标图像上层绘制源图像
//SRC_ATOP:[Da, Sc * Da + (1 - Sa) * Dc]

全屏情况下,Canvas中Bitmap大小为屏幕的大小,这里大小只要比原来的Bitmap大就行(和坐标无关),在真正绘制的时候,我们是把event的坐标(getX、getY)直接画在Bitmap上

04-21 11:11:26.699 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 22.944572
04-21 11:11:26.728 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 22.944572
04-21 11:11:26.742 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 18.36341
04-21 11:11:26.759 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 15.950039
04-21 11:11:26.776 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 10.048706
04-21 11:11:26.809 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 0.27072144
04-21 11:11:26.830 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -7.797901
04-21 11:11:26.848 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -12.466526
04-21 11:11:26.861 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -19.900517
04-21 11:11:26.875 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -23.855247
04-21 11:11:26.884 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -27.016394
04-21 11:11:26.886 6416-6416/com.sohu.inputmethod.sogou E/zzzz: aaa 5.991678 -27.016394

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值