笔锋有两种方案:自己画(计算复杂度高且实现效果差)、笔锋图片(存在重合区域)
消隐有两种方案:笔画结束时整体重画(过度绘制)、笔画结束时增加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;
}
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
本文探讨了手写板的实现,包括笔锋处理的两种方案、消隐策略、手写板大小计算以及PopupWindow的使用。在触摸事件处理中,通过dispatchTouchEvent和onTouchEvent与手写SDK交互,并在onDraw中进行Canvas绘制。GestureController、GestureCanvas、BrushesPlotter和SpotFilter等组件协同工作,完成手写轨迹的记录和绘制。
1963

被折叠的 条评论
为什么被折叠?



