package com.android.settings.macy11;/* * Copyright (C) 2010 The Android Open Source Project * * 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. */import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.RectF;import android.graphics.Paint.FontMetricsInt;import android.hardware.input.InputManager;import android.hardware.input.InputManager.InputDeviceListener;import android.os.SystemProperties;import android.util.Log;import android.util.Slog;import android.view.InputDevice;import android.view.KeyEvent;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewConfiguration;import android.view.WindowManagerPolicyConstants.PointerEventListener;import android.view.MotionEvent.PointerCoords;import java.util.ArrayList;import java.util.Arrays;/** * 此类默认情况下会绘制三条曲线 * 第一条是手指触摸屏幕的轨迹 记录down-->move-->up * 第二条是手指离开屏幕那个时间点(对应ACTION_UP)获取到的估计量,在经过一个算法得到前后共7个时间点的估计值绘制出的估计轨迹 * 第三条是手指离开屏幕那个时刻通过最后那个点(对应ACTION_UP)的x,y轴的速度刻画出未来16.67ms的速度矢量直线 * 

* {@link PLView#onPointerEvent(MotionEvent)} 此方法是记录整个TouchEvent的过程 * Exp: * {@link PLView#mPointers} 屏幕上一共有几条触摸轨迹(一个完整的事件流的集合)记录在这个集合里 * 集合中的数据类型是{@link PointerState}(一个完整的事件流) * 几个关键的属性 *

* 以下三个变量用于实际触摸曲线的绘制 * {@link PointerState#mTraceX x轨迹点的坐标} * {@link PointerState#mTraceY y轨迹点的坐标} * {@link PointerState#mTraceCurrent 经过的轨迹点:false 最后的触摸点:true 两种类型的点的坐标可能会有重合} *

* 用以下的两个变量来绘制速度矢量直线 这两个值的获取依赖 {@link PLView#mVelocityTracker} * {@link PointerState#mXVelocity} x分量速度 * {@link PointerState#mYVelocity} y分量速度 *

* 依靠下面的这个变量来绘制估计运动曲线 这个值的获取依赖于 {@link PLView#mVelocityTracker} * {@link PointerState#mEstimator} 位置估计变量 基于多项式模型的指针运动估计器 *

* {@link PLView#mVelocityTracker} 事件的速度追踪 *

* {@link PLView#onDraw(Canvas)} 绘制 * 其余细节代码中有详细的标注 */public class PLView extends View implements InputDeviceListener, PointerEventListener { private static final String TAG = "Macy11-Pointer"; // The system property key used to specify an alternate velocity tracker strategy // to plot alongside the default one. Useful for testing and comparison purposes. // 系统属性键,用于指定另一种速度跟踪策略,以便与默认策略一起绘图。适用于测试和比较目的。 private static final String ALT_STRATEGY_PROPERY_KEY = "debug.velocitytracker.alt"; //--------------------------Pointer State----------------------------// // 指针状态 我理解的是一条完整轨迹曲线的数据状态 // 其中会记录一次运动轨迹曲线上从down--> move --> up 的点的信息 public static class PointerState { // Trace of previous points. // 该数组容量会随着触摸点数的增加一直增长 只要保持在这个View内部 private float[] mTraceX = new float[32];// x轨迹的点的坐标 private float[] mTraceY = new float[32];// y轨迹的点的坐标 // true:current 通过getPointerCoords 获取的点; // false:history 通过getHistoricalPointerCoords获取的点 private boolean[] mTraceCurrent = new boolean[32]; // 记录每个点的估计值 private int mTraceCount;// 一次触摸事件的所有点的数量 private float[] mEstimatorTraceX = new float[32]; private float[] mEstimatorTraceY = new float[32]; private boolean[] mEstimatorTraceCurrent = new boolean[32]; private int mEstimatorTraceCount; // True if the pointer is down. private boolean mCurDown; // Most recent coordinates. // 坐标点的数据(这条轨迹曲线上的点的坐标信息都可以获取到) private PointerCoords mCoords = new PointerCoords(); private int mToolType;// 触摸的工具 // Most recent velocity. private float mXVelocity;// x轴速度 private float mYVelocity;// y轴速度 private float mAltXVelocity; private float mAltYVelocity; // Current bounding box, if any // 测试中发现这些变量一直是默认值 private boolean mHasBoundingBox; private float mBoundingLeft; private float mBoundingTop; private float mBoundingRight; private float mBoundingBottom; // Position estimator. 位置估值器 通过VelocityTracker来获取 private VelocityTracker.Estimator mEstimator = new VelocityTracker.Estimator(); private VelocityTracker.Estimator mAltEstimator = new VelocityTracker.Estimator(); public void clearTrace() { mTraceCount = 0; mEstimatorTraceCount = 0; } //添加一次MotionEvent事件的触摸点的轨迹 (只记录 Move & Up) public void addTrace(float x, float y, boolean current) { Log.d(TAG, " addTrace x= " + x + " y= " + y + " current= " + current + " traceCount= " + mTraceCount); int traceCapacity = mTraceX.length; if (mTraceCount == traceCapacity) {//自动扩容 traceCapacity *= 2; float[] newTraceX = new float[traceCapacity]; System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount); mTraceX = newTraceX; float[] newTraceY = new float[traceCapacity]; System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount); mTraceY = newTraceY; boolean[] newTraceCurrent = new boolean[traceCapacity]; System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount); mTraceCurrent = newTraceCurrent; } mTraceX[mTraceCount] = x; mTraceY[mTraceCount] = y; mTraceCurrent[mTraceCount] = current; mTraceCount += 1; } //添加一次MotionEvent事件的触摸点的轨迹 (只记录 Move & Up) public void addEstimatorTrace(float x, float y, boolean current) { Log.d(TAG, " addTrace Estimator x= " + x + " y= " + y + " current= " + current + " traceCount= " + mEstimatorTraceCount); int traceCapacity = mEstimatorTraceX.length; if (mEstimatorTraceCount == traceCapacity) {//自动扩容 traceCapacity *= 2; // estimator float[] newEstimatorTraceX = new float[traceCapacity]; System.arraycopy(mEstimatorTraceX, 0, newEstimatorTraceX, 0, mEstimatorTraceCount); mEstimatorTraceX = newEstimatorTraceX; float[] newEstimatorTraceY = new float[traceCapacity]; System.arraycopy(mEstimatorTraceY, 0, newEstimatorTraceY, 0, mEstimatorTraceCount); mEstimatorTraceY = newEstimatorTraceY; boolean[] newEstimatorCurrent = new boolean[traceCapacity]; System.arraycopy(mEstimatorTraceCurrent, 0, newEstimatorCurrent, 0, mEstimatorTraceCount); mEstimatorTraceCurrent = newEstimatorCurrent; } mEstimatorTraceX[mEstimatorTraceCount] = x; mEstimatorTraceY[mEstimatorTraceCount] = y; mEstimatorTraceCurrent[mEstimatorTraceCount] = current; mEstimatorTraceCount += 1; } public String getTraceArray() { return "\nmTraceX=" + Arrays.toString(mTraceX) + "\nmTraceY=" + Arrays.toString(mTraceY) + "\nmTraceCurrent=" + Arrays.toString(mTraceCurrent); } public String getTraceArrayLength() { return "\nmTraceCount=" + mTraceCount + "\nmTraceX=" + mTraceX.length + "\nmTraceY=" + mTraceY.length + "\nmTraceCurrent=" + mTraceCurrent.length; } public String getBounding() { return "\nmHasBoundingBox=" + mHasBoundingBox + "\nmBoundingLeft=" + mBoundingLeft + "\nmBoundingTop=" + mBoundingTop + "\nmBoundingRight=" + mBoundingRight + "\nmBoundingBottom=" + mBoundingBottom; } @Override public String toString() { return "PointerState{" + ", mCurDown=" + mCurDown + ", mCoords=" + mCoords + ", mToolType=" + mToolType + ", mXVelocity=" + mXVelocity + ", mYVelocity=" + mYVelocity + ", mAltXVelocity=" + mAltXVelocity + ", mAltYVelocity=" + mAltYVelocity + ", mEstimator=" + mEstimator + ", mAltEstimator=" + mAltEstimator + '}'; } } //--------------------------PointerState----------------------------// //用来计算估计值的常量 private final int ESTIMATE_PAST_POINTS = 4;//过去时间的4个点 private final int ESTIMATE_FUTURE_POINTS = 2;//将来时间的2个点 private final float ESTIMATE_INTERVAL = 0.02f;//间隔 private final InputManager mIm; private final ViewConfiguration mVC;// UI中的常量 private final Paint mTextPaint;//顶部状态栏的绘制字体 private final Paint mTextBackgroundPaint;//状态栏的字体的背景 private final Paint mTextLevelPaint;// private final Paint mPaint; private final Paint mCurrentPointPaint;//当前一次Move MotionEvent的最后一个点 private final Paint mHistoryPointPaint;//当前一次Move MotionEvent的经过的所有点 private final Paint mTargetPaint;// 十字准线 private final Paint mPathPaint; // 触摸轨迹路径 private final FontMetricsInt mTextMetrics = new FontMetricsInt(); private int mHeaderBottom; private boolean mCurDown; private int mCurNumPointers;//当前触碰点的数量 private int mMaxNumPointers;//最大触碰点的数量 private int mActivePointerId;// 当前活跃的id of pointer private final ArrayList mPointers = new ArrayList<>();//轨迹曲线集合 子元素为一条轨迹曲线的状态数据对象 private final PointerCoords mTempCoords = new PointerCoords(); private final VelocityTracker.Estimator mTempEstimator = new VelocityTracker.Estimator(); private final VelocityTracker mVelocityTracker;//速度追踪 private final VelocityTracker mAltVelocityTracker; private final FasterStringBuilder mText = new FasterStringBuilder(); private boolean mPrintCoords = true; public PLView(Context c) { super(c); setEnabled(true); setClickable(true); setFocusable(true); setFocusableInTouchMode(true); mIm = c.getSystemService(InputManager.class); mVC = ViewConfiguration.get(c); mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(10 * getResources().getDisplayMetrics().density); mTextPaint.setARGB(255, 0, 0, 0); mTextBackgroundPaint = new Paint(); mTextBackgroundPaint.setAntiAlias(false); mTextBackgroundPaint.setARGB(128, 255, 255, 255); mTextLevelPaint = new Paint(); mTextLevelPaint.setAntiAlias(false); mTextLevelPaint.setARGB(192, 255, 0, 0); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setARGB(255, 255, 255, 255); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(2); mCurrentPointPaint = new Paint(); mCurrentPointPaint.setAntiAlias(true); mCurrentPointPaint.setARGB(255, 255, 0, 0); mCurrentPointPaint.setStyle(Paint.Style.STROKE); mCurrentPointPaint.setStrokeWidth(6); mHistoryPointPaint = new Paint(); mHistoryPointPaint.setAntiAlias(true); mHistoryPointPaint.setARGB(255, 0, 150, 0); mHistoryPointPaint.setStyle(Paint.Style.STROKE); mHistoryPointPaint.setStrokeWidth(4); mTargetPaint = new Paint(); mTargetPaint.setAntiAlias(true); mTargetPaint.setARGB(255, 0, 0, 192); mPathPaint = new Paint(); mPathPaint.setAntiAlias(true); mPathPaint.setARGB(255, 0, 0, 0); mPathPaint.setStrokeWidth(2); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(1); PointerState ps = new PointerState(); Log.d(TAG, " onPointerLocation create new " + ps.toString()); mPointers.add(ps); mActivePointerId = 0; mVelocityTracker = VelocityTracker.obtain(); String altStrategy = SystemProperties.get(ALT_STRATEGY_PROPERY_KEY); Log.d(TAG, " altStrategy " + (altStrategy != null ? altStrategy + " " + altStrategy.length() : "null")); if (altStrategy.length() != 0) { Log.d(TAG, "Comparing default velocity tracker strategy with " + altStrategy); mAltVelocityTracker = VelocityTracker.obtain(altStrategy); } else { mAltVelocityTracker = null; } } public void setPrintCoords(boolean state) { mPrintCoords = state; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mTextPaint.getFontMetricsInt(mTextMetrics); mHeaderBottom = -mTextMetrics.ascent + mTextMetrics.descent + 2; if (true) { Log.i(TAG, " onPointerLocation Metrics: ascent=" + mTextMetrics.ascent + " descent=" + mTextMetrics.descent + " leading=" + mTextMetrics.leading + " top=" + mTextMetrics.top + " bottom=" + mTextMetrics.bottom); } } // Draw an oval. When angle is 0 radians, orients the major axis vertically, // angles less than or greater than 0 radians rotate the major axis left or right. // 画一个椭圆。当角度为0弧度时,将主轴垂直定向,小于或大于0弧度的角将主轴向左或向右旋转。 private RectF mReusableOvalRect = new RectF();// 可重复使用的 //绘制椭圆 接触点的椭圆 private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle, Paint paint) { canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.rotate((float) (angle * 180 / Math.PI), x, y); mReusableOvalRect.left = x - minor / 2; mReusableOvalRect.right = x + minor / 2; mReusableOvalRect.top = y - major / 2; mReusableOvalRect.bottom = y + major / 2; canvas.drawOval(mReusableOvalRect, paint); canvas.restore(); } //从down-->move--->up(一次完整的运动轨迹曲线) //会向mPointers(运动轨迹曲线数据的集合)添加pointerState(一次运动轨迹曲线的数据)对象 //从而完成对一次完整的运动轨迹曲线的描述 在对应的onDraw中绘制出轨迹 @Override public void onPointerEvent(MotionEvent event) { Log.d(TAG, " view callback onPointerEvent " + getTagName(event)); final int action = event.getAction(); int pointersSize = mPointers.size(); Log.d(TAG, " onPointerEvent NP origin pointers size " + pointersSize); //-----------------------Action Down----------------------------// if (action == MotionEvent.ACTION_DOWN || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { //多点触控判断获取当前触控点索引(依次为手指触碰屏幕的点标记index,一个完整的触摸事件拥有相同的index) final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down Log.d(TAG, " current point index " + index); if (action == MotionEvent.ACTION_DOWN) {// 没必要加入多指触摸的条件是因为用了for循环 //从down事件开始 每次重置 pointers集合 中 pointerState某些状态 for (int p = 0; p < pointersSize; p++) {// 轨迹曲线的数量 final PointerState ps = mPointers.get(p); Log.d(TAG, " trace contain others " + ps.toString()); Log.d(TAG, " trace contain length " + ps.getTraceArrayLength()); Log.d(TAG, " trace contain bounding " + ps.getBounding()); Logger.longLogD(" trace contain array body ", ps.getTraceArray()); ps.clearTrace(); ps.mCurDown = false; } mCurDown = true; mCurNumPointers = 0; mMaxNumPointers = 0; mVelocityTracker.clear(); if (mAltVelocityTracker != null) { mAltVelocityTracker.clear(); } } mCurNumPointers += 1;// 屏幕触摸轨迹曲线数量 if (mMaxNumPointers < mCurNumPointers) { mMaxNumPointers = mCurNumPointers; } // 从屏幕上有接触点到没有接触点 系统会按照触摸的先后为每一个触摸点标记index // 每一个pointer 对应唯一的 index, 而index又对应唯一的pointerId // 可以通过这个唯一的pointerId 获取 data final int id = event.getPointerId(index); Log.d(TAG, " onPointerEvent NP ---- " + pointersSize + " getPointerId " + id); Log.d(TAG, " onPointerEvent mPointers.size before " + mPointers.size()); // 大于一个触摸点的时候会进入while 循环体 while (pointersSize <= id) { PointerState ps = new PointerState(); mPointers.add(ps); pointersSize++; } Log.d(TAG, " onPointerEvent mPointers.size after " + mPointers.size()); Log.d(TAG, " id xxxx total size " + mPointers.size()); Log.d(TAG, " onPointerEvent-->> mActivePointerId before " + mActivePointerId); Log.d(TAG, " onPointerEvent-->> pointerState.mCurDown " + mPointers.get(mActivePointerId).mCurDown); if (mActivePointerId < 0 || !mPointers.get(mActivePointerId).mCurDown) { mActivePointerId = id; } Log.d(TAG, " onPointerEvent mActivePointerId after " + mActivePointerId); final PointerState ps = mPointers.get(id); ps.mCurDown = true; InputDevice device = InputDevice.getDevice(event.getDeviceId()); Log.d(TAG, " device xxxxx " + (device != null ? device.toString() : "null")); ps.mHasBoundingBox = device != null && device.getMotionRange(MotionEvent.AXIS_GENERIC_1) != null; Log.d(TAG, " onPointerEvent action down ps.mHasBoundingBox " + ps.mHasBoundingBox); } //-----------------------Action Down----------------------------// //------中间的Action Move------// Log.d(TAG, " onPointerEvent middle numS mCurNumPointers= " + mCurNumPointers + " mMaxNumPointers= " + mMaxNumPointers); //触摸点的数量 final int pointerCountNI = event.getPointerCount(); while (pointersSize < pointerCountNI){ PointerState ps = new PointerState(); mPointers.add(ps); pointersSize++; } Log.d(TAG, "onPointerEvent event.getPointerCount NI middle " + pointerCountNI); mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1); Log.d(TAG, "onPointerEvent mAltVelocity " + mAltVelocityTracker); if (mAltVelocityTracker != null) { mAltVelocityTracker.addMovement(event); mAltVelocityTracker.computeCurrentVelocity(1); } // 当次Move事件轨迹段中包含的经过点的数量 用来描述轨迹曲线的点 final int N = event.getHistorySize(); Log.d(TAG, "onPointerEvent----->>>>>> event.getHistorySize N " + N); for (int historyPos = 0; historyPos < N; historyPos++) { for (int i = 0; i < pointerCountNI; i++) { final int id = event.getPointerId(i); Log.d(TAG," id xxxx history ----> " + id + " size " + (mPointers.size())); if (id >= mPointers.size()) { continue; } Log.d(TAG, "onPointerEvent Pointer history id " + id); final PointerState ps = mCurDown ? mPointers.get(id) : null; final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords; final VelocityTracker.Estimator estimator = ps != null ? ps.mEstimator : mTempEstimator; //填充历史坐标 event.getHistoricalPointerCoords(i, historyPos, coords);// if (mPrintCoords) {// logCoords("onPointerEvent Pointer history ", action, i, coords, id, event);// }// Logger.longLogD(TAG, "onPointerEvent historyPos & NI " + ps); if (ps != null) { // 历史轨迹的点标记为蓝色 // 把历史轨迹点装箱到pointerState的数组里 mVelocityTracker.getEstimator(id, estimator); float eX = estimator.estimateX(0); float eY = estimator.estimateY(0); Log.d(TAG, " xxxxxxxx history \n实际坐标: " + coords.x + " " + coords.y + "\n估计坐标: " + eX + " " + eY); ps.addTrace(coords.x, coords.y, false); ps.addEstimatorTrace(eX, eY, false); } } } for (int i = 0; i < pointerCountNI; i++) { final int id = event.getPointerId(i); Log.d(TAG," id xxxx current ----> " + id+ " size " + (mPointers.size())); if (id >= mPointers.size()) { continue; } final PointerState ps = mCurDown ? mPointers.get(id) : null; final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords; final VelocityTracker.Estimator estimator = ps != null ? ps.mEstimator : mTempEstimator; //使用指定指针索引的指针坐标数据填充{@link PointerCoords}对象。 event.getPointerCoords(i, coords);// if (mPrintCoords) {// logCoords("onPointerEvent Pointer current ", action, i, coords, id, event);// }// Logger.longLogD(TAG, "onPointerEvent only & NI " + ps); if (ps != null) { Log.d(TAG, "onPointerEvent only & NI addTrace " + coords.x + " " + coords.y + " true");// ps.addTrace(coords.x, coords.y, true); ps.mXVelocity = mVelocityTracker.getXVelocity(id); ps.mYVelocity = mVelocityTracker.getYVelocity(id); Log.d(TAG, " current pointer velocity " + ps.mXVelocity + " " + ps.mYVelocity); //计算估计值 //使用指针过去的运动来预测未来的运动,获得指针运动的估计量 //调用本地方法获取估计值0 //之后用来绘制估计曲线 //每次MoveEvent回调事件后 更新(实际最后绘制的是UP点的前后几个估计点构成的曲线) //其中up点就是对应的最后一次更新的 ps.mEstimato Log.d(TAG, "------------------->" + event.getAction()); mVelocityTracker.getEstimator(id, estimator); float eX = estimator.estimateX(0); float eY = estimator.estimateY(0); Log.d(TAG, " xxxxxxxx current \n实际坐标: " + coords.x + " " + coords.y + "\n估计坐标: " + eX + " " + eY); ps.addTrace(coords.x, coords.y, true); ps.addEstimatorTrace(eX, eY, true); Log.d(TAG, "onPointerEvent action middle mAltVelocity " + mAltVelocityTracker);// if (mAltVelocityTracker != null) {// ps.mAltXVelocity = mAltVelocityTracker.getXVelocity(id);// ps.mAltYVelocity = mAltVelocityTracker.getYVelocity(id);// mAltVelocityTracker.getEstimator(id, ps.mAltEstimator);// }// ps.mToolType = event.getToolType(i);// Log.d(TAG, "onPointerEvent ps.mToolType " + ps.mToolType + " ps.mHasBoundingBox " + ps.mHasBoundingBox);// if (ps.mHasBoundingBox) {// it sames always false// ps.mBoundingLeft = event.getAxisValue(MotionEvent.AXIS_GENERIC_1, i);// ps.mBoundingTop = event.getAxisValue(MotionEvent.AXIS_GENERIC_2, i);// ps.mBoundingRight = event.getAxisValue(MotionEvent.AXIS_GENERIC_3, i);// ps.mBoundingBottom = event.getAxisValue(MotionEvent.AXIS_GENERIC_4, i);// } } } //-------Action Move----//// //---------------------------Action Up----------------------------// if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) { final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP final int id = event.getPointerId(index); Log.d(TAG, "onPointerEvent action up id " + id + " NP " + pointersSize); if (id >= pointersSize) { Slog.wtf(TAG, "Got pointer ID out of bounds: id=" + id + " arraysize=" + pointersSize + " pointerindex=" + index + " action=0x" + Integer.toHexString(action)); return; } Log.d(TAG, "onPointerEvent action up mPointers.size " + mPointers.size()); final PointerState ps = mPointers.get(id); ps.mCurDown = false; if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mCurDown = false; mCurNumPointers = 0; } else { Log.d(TAG, "onPointerEvent not action up \nmCurNumPointers " + mCurNumPointers + "\nmActivePointerId " + mActivePointerId + "\nid " + id); mCurNumPointers -= 1; if (mActivePointerId == id) { mActivePointerId = event.getPointerId(index == 0 ? 1 : 0); } Log.d(TAG, "onPointerEvent not action up addTrace " + Float.NaN + " " + Float.NaN + " false"); //异常状态 ps.addTrace(Float.NaN, Float.NaN, false); ps.addEstimatorTrace(Float.NaN, Float.NaN, false); } } //---------------------------Action Up----------------------------// invalidate();//重新绘制 } @Override protected void onDraw(Canvas canvas) { final int w = getWidth(); final int itemW = w / 7; final int base = -mTextMetrics.ascent + 1; final int bottom = mHeaderBottom; final int NP = mPointers.size(); Log.d(TAG, " onDraw NP " + NP + " mActivePointerId " + mActivePointerId); // Labels if (mActivePointerId >= 0) { final PointerState ps = mPointers.get(mActivePointerId); canvas.drawRect(0, 0, itemW - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() .append("P: ").append(mCurNumPointers) .append(" / ").append(mMaxNumPointers) .toString(), 1, base, mTextPaint); final int N = ps.mTraceCount; Log.d(TAG, "onDraw ps.mTraceCount " + N); if ((mCurDown && ps.mCurDown) || N == 0) { // 手指一直贴着屏幕时 会绘制手指触摸屏幕的点的坐标 canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() .append("X: ").append(ps.mCoords.x, 1) .toString(), 1 + itemW, base, mTextPaint); canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() .append("Y: ").append(ps.mCoords.y, 1) .toString(), 1 + itemW * 2, base, mTextPaint); } else { //手指脱离屏幕后 会打印出起始到终止位置的偏移量 float dx = ps.mTraceX[N - 1] - ps.mTraceX[0]; float dy = ps.mTraceY[N - 1] - ps.mTraceY[0]; canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, Math.abs(dx) < mVC.getScaledTouchSlop() ? mTextBackgroundPaint : mTextLevelPaint); canvas.drawText(mText.clear() .append("dX: ").append(dx, 1) .toString(), 1 + itemW, base, mTextPaint); canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, Math.abs(dy) < mVC.getScaledTouchSlop() ? mTextBackgroundPaint : mTextLevelPaint); canvas.drawText(mText.clear() .append("dY: ").append(dy, 1) .toString(), 1 + itemW * 2, base, mTextPaint); } canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() .append("Xv: ").append(ps.mXVelocity, 3) .toString(), 1 + itemW * 3, base, mTextPaint); canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint); canvas.drawText(mText.clear() .append("Yv: ").append(ps.mYVelocity, 3) .toString(), 1 + itemW * 4, base, mTextPaint); canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint); canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, bottom, mTextLevelPaint); canvas.drawText(mText.clear() .append("Prs: ").append(ps.mCoords.pressure, 2) .toString(), 1 + itemW * 5, base, mTextPaint); canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint); canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1, bottom, mTextLevelPaint); canvas.drawText(mText.clear() .append("Size: ").append(ps.mCoords.size, 2) .toString(), 1 + itemW * 6, base, mTextPaint); } Log.d(TAG, "onDraw draw pointer trace NP " + NP); // Pointer trace. for (int p = 0; p < NP; p++) { final PointerState ps = mPointers.get(p); // Draw path. // 每次都会从最开始的那个点开始绘制到当前手指移动到的位置 一直到手指离开屏幕 final int traceCount = ps.mTraceCount; final int estimatorTraceCount = ps.mEstimatorTraceCount; Log.d(TAG, " onDraw path ps.mTraceCount " + traceCount); float lastX = 0, lastY = 0; boolean haveLast = false; boolean drawn = false; mPaint.setARGB(255, 128, 255, 255); for (int i = 0; i < traceCount; i++) { float x = ps.mTraceX[i]; float y = ps.mTraceY[i]; Log.d(TAG, " ----->>>>>> " + x + " " + y); Log.d(TAG, "onDraw -- isNaN " + i + " isNaN " + (Float.isNaN(x))); if (Float.isNaN(x)) { haveLast = false; continue; } Log.d(TAG, "onDraw -- haveLast " + i + " haveLast " + haveLast); if (haveLast) { //画出运动轨迹线 canvas.drawLine(lastX, lastY, x, y, mPathPaint); //画出运动轨迹的点 final Paint paint = ps.mTraceCurrent[i] ? mCurrentPointPaint : mHistoryPointPaint; canvas.drawPoint(lastX, lastY, paint); drawn = true; Log.d(TAG, " onDraw path ps.mTraceCount v " + i); //绘制 1 ------ 最后一个点 } lastX = x; lastY = y; haveLast = true; } Log.d(TAG, "onDraw drawn " + drawn); if (drawn) { float lx = 0, ly = 0; boolean haveLast_Estimater = false; // Draw movement estimate curve.// mPaint.setARGB(128, 128, 0, 128); mPaint.setARGB(255, 255, 95, 33);// mCurrentPointPaint.setARGB(255,);// mHistoryPointPaint.setARGB(255,); // 橙色的运动轨迹估计曲线 // 绘制了手指离开屏幕前后几个点得到估计运动曲线 // 每段OnMove回调后计算出的运动估计值// float lx = ps.mEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);// float ly = ps.mEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL); //起始绘制time点是 -0.08f //进行[-3,2] 共6段直线的绘制 // (-0.08,-0.06)(-0.06,-0.04)(-0.04,-0.02)(-0.02,0)(0,0.02)(0.02,0.04) 这些区间点都是时刻点 // 0时刻就是对应的 一组事件最后的Up点记录// for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {// float x = ps.mEstimator.estimateX(i * ESTIMATE_INTERVAL);// float y = ps.mEstimator.estimateY(i * ESTIMATE_INTERVAL);// canvas.drawLine(lx, ly, x, y, mPaint);// Log.d(TAG, " 轨迹估计曲线 " + " lx= " + lx + " ly=" + ly + " x= " + x + "y= " + y);// lx = x;// ly = y;// } for (int i = 0; i < estimatorTraceCount; i++) { float x = ps.mEstimatorTraceX[i]; float y = ps.mEstimatorTraceY[i]; if (Float.isNaN(x)) { haveLast_Estimater = false; continue; } if (haveLast_Estimater) { //画出运动轨迹线 canvas.drawLine(lx, ly, x, y, mPaint); Log.d(TAG, " estimator== ly " + ly + " lx " + lx + " x " + x + " y " + y); //画出运动轨迹的点 final Paint paint = ps.mTraceCurrent[i] ? mCurrentPointPaint : mHistoryPointPaint;// canvas.drawPoint(lastX, lastY, paint); //绘制 1 ------ 最后一个点 } lx = x; ly = y; haveLast_Estimater = true; } // Draw velocity vector.// mPaint.setARGB(255, 255, 64, 128); mPaint.setARGB(255, 0, 0, 255); //蓝色的速度矢量直线 float xVel = ps.mXVelocity * (1000 / 60); // pixel/ms * 1000 / 60 --> 未来16.67ms 经过的距离 float yVel = ps.mYVelocity * (1000 / 60); canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint); Log.d(TAG, "xxxxxx xVel " + xVel + " yVel " + yVel);// // Draw alternate estimate. 目前没用 mAltVelocity null// Log.d(TAG,"onDraw Draw alternate estimate mAltVelocity " + mAltVelocity);// if (mAltVelocity != null) {// mPaint.setARGB(128, 0, 128, 128);// lx = ps.mAltEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);// ly = ps.mAltEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);// for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {// float x = ps.mAltEstimator.estimateX(i * ESTIMATE_INTERVAL);// float y = ps.mAltEstimator.estimateY(i * ESTIMATE_INTERVAL);// canvas.drawLine(lx, ly, x, y, mPaint);// lx = x;// ly = y;// }//// mPaint.setARGB(255, 64, 255, 128);// xVel = ps.mAltXVelocity * (1000 / 60);// yVel = ps.mAltYVelocity * (1000 / 60);// canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);// } }// Log.d(TAG, "onDraw mCurDown " + (mCurDown && ps.mCurDown));// if (mCurDown && ps.mCurDown) {// //绘制十字准线 x y 轴// // Draw crosshairs.// //x轴//// canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);// //y轴//// canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);//// // Draw current point. 十字轴准星点//// int pressureLevel = (int) (ps.mCoords.pressure * 255);//// mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);//// canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);//// // Draw current touch ellipse.//// mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);//// drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,//// ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);//// // Draw current tool ellipse.//// mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);//// drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,//// ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);//// // Draw the orientation arrow.//// float arrowSize = ps.mCoords.toolMajor * 0.7f;//// if (arrowSize < 20) {//// arrowSize = 20;//// }//// mPaint.setARGB(255, pressureLevel, 255, 0);//// float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)//// * arrowSize);//// float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)//// * arrowSize);//// if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS//// || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {//// // Show full circle orientation.//// canvas.drawLine(ps.mCoords.x, ps.mCoords.y,//// ps.mCoords.x + orientationVectorX,//// ps.mCoords.y + orientationVectorY,//// mPaint);//// } else {//// // Show half circle orientation.//// canvas.drawLine(//// ps.mCoords.x - orientationVectorX,//// ps.mCoords.y - orientationVectorY,//// ps.mCoords.x + orientationVectorX,//// ps.mCoords.y + orientationVectorY,//// mPaint);//// }//// // Draw the tilt point along the orientation arrow.//// float tiltScale = (float) Math.sin(//// ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));//// canvas.drawCircle(//// ps.mCoords.x + orientationVectorX * tiltScale,//// ps.mCoords.y + orientationVectorY * tiltScale,//// 3.0f, mPaint);//// // Draw the current bounding box//// if (ps.mHasBoundingBox) {//// canvas.drawRect(ps.mBoundingLeft, ps.mBoundingTop,//// ps.mBoundingRight, ps.mBoundingBottom, mPaint);//// }// } } } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onPointerEvent----->>>>>> view callback onTouchEvent " + event.getAction());// printSamples(event); onPointerEvent(event); if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) { requestFocus(); } return true; } private void printSamples(MotionEvent ev) { final int historySize = ev.getHistorySize(); final int pointerCount = ev.getPointerCount(); for (int h = 0; h < historySize; h++) { Log.d("macy777", "At time historyET " + ev.getHistoricalEventTime(h)); for (int p = 0; p < pointerCount; p++) { Log.d("macy777", " pointerId " + ev.getPointerId(p) + " historyX " + ev.getHistoricalX(p, h) + " historyY " + ev.getHistoricalY(p, h)); } } Log.d("macy777", "At time ET " + ev.getEventTime()); for (int p = 0; p < pointerCount; p++) { Log.d("macy777", " pointerId " + ev.getPointerId(p) + " X " + ev.getX(p) + " Y " + ev.getY(p)); } } @Override public boolean onGenericMotionEvent(MotionEvent event) { Log.d(TAG, " view callback onGenericMotionEvent "); final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { onPointerEvent(event); } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { logMotionEvent("Joystick", event); } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) { logMotionEvent("Position", event); } else { logMotionEvent("Generic", event); } return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (shouldLogKey(keyCode)) { final int repeatCount = event.getRepeatCount(); if (repeatCount == 0) { Log.i(TAG, "Key Down: " + event); } else { Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event); } return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (shouldLogKey(keyCode)) { Log.i(TAG, "Key Up: " + event); return true; } return super.onKeyUp(keyCode, event); } private static boolean shouldLogKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_CENTER: return true; default: return KeyEvent.isGamepadButton(keyCode) || KeyEvent.isModifierKey(keyCode); } } @Override public boolean onTrackballEvent(MotionEvent event) { Log.d(TAG, " view callback onTrackballEvent "); logMotionEvent("Trackball", event); return true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mIm.registerInputDeviceListener(this, getHandler()); logInputDevices(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mIm.unregisterInputDeviceListener(this); } @Override public void onInputDeviceAdded(int deviceId) { logInputDeviceState(deviceId, "Device Added"); } @Override public void onInputDeviceChanged(int deviceId) { logInputDeviceState(deviceId, "Device Changed"); } @Override public void onInputDeviceRemoved(int deviceId) { logInputDeviceState(deviceId, "Device Removed"); } private void logInputDevices() { int[] deviceIds = InputDevice.getDeviceIds(); for (int i = 0; i < deviceIds.length; i++) { logInputDeviceState(deviceIds[i], "Device Enumerated"); } } private void logInputDeviceState(int deviceId, String state) { InputDevice device = mIm.getInputDevice(deviceId); if (device != null) { Log.i(TAG, state + ": " + device); } else { Log.i(TAG, state + ": " + deviceId); } } private void logMotionEvent(String type, MotionEvent event) { final int action = event.getAction(); final int N = event.getHistorySize(); final int NI = event.getPointerCount(); for (int historyPos = 0; historyPos < N; historyPos++) { for (int i = 0; i < NI; i++) { final int id = event.getPointerId(i); event.getHistoricalPointerCoords(i, historyPos, mTempCoords); logCoords(type, action, i, mTempCoords, id, event); } } for (int i = 0; i < NI; i++) { final int id = event.getPointerId(i); event.getPointerCoords(i, mTempCoords); logCoords(type, action, i, mTempCoords, id, event); } } private void logCoords(String type, int action, int index, MotionEvent.PointerCoords coords, int id, MotionEvent event) { final int toolType = event.getToolType(index); final int buttonState = event.getButtonState(); final String prefix; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: prefix = "DOWN"; break; case MotionEvent.ACTION_UP: prefix = "UP"; break; case MotionEvent.ACTION_MOVE: prefix = "MOVE"; break; case MotionEvent.ACTION_CANCEL: prefix = "CANCEL"; break; case MotionEvent.ACTION_OUTSIDE: prefix = "OUTSIDE"; break; case MotionEvent.ACTION_POINTER_DOWN: if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) { prefix = "DOWN"; } else { prefix = "MOVE"; } break; case MotionEvent.ACTION_POINTER_UP: if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) { prefix = "UP"; } else { prefix = "MOVE"; } break; case MotionEvent.ACTION_HOVER_MOVE: prefix = "HOVER MOVE"; break; case MotionEvent.ACTION_HOVER_ENTER: prefix = "HOVER ENTER"; break; case MotionEvent.ACTION_HOVER_EXIT: prefix = "HOVER EXIT"; break; case MotionEvent.ACTION_SCROLL: prefix = "SCROLL"; break; default: prefix = Integer.toString(action); break; } Log.i(TAG, mText.clear() .append(type).append(" id ").append(id + 1) .append(": ") .append(prefix) .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3) .append(") Pressure=").append(coords.pressure, 3) .append(" Size=").append(coords.size, 3) .append(" TouchMajor=").append(coords.touchMajor, 3) .append(" TouchMinor=").append(coords.touchMinor, 3) .append(" ToolMajor=").append(coords.toolMajor, 3) .append(" ToolMinor=").append(coords.toolMinor, 3) .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1) .append("deg") .append(" Tilt=").append((float) ( coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1) .append("deg") .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1) .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1) .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1) .append(" BoundingBox=[(") .append(event.getAxisValue(MotionEvent.AXIS_GENERIC_1), 3) .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_2), 3).append(")") .append(", (").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_3), 3) .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_4), 3) .append(")]") .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType)) .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState)) .toString()); } //fdfdfsd private String getTagName(MotionEvent motionEvent) { int action = motionEvent.getAction(); String prefix = "default"; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: prefix = "DOWN"; break; case MotionEvent.ACTION_UP: prefix = "UP"; break; case MotionEvent.ACTION_MOVE: prefix = "MOVE"; break; case MotionEvent.ACTION_CANCEL: prefix = "CANCEL"; break; case MotionEvent.ACTION_OUTSIDE: prefix = "OUTSIDE"; break; case MotionEvent.ACTION_POINTER_DOWN: prefix = "ACTION_POINTER_DOWN"; break; case MotionEvent.ACTION_POINTER_UP: prefix = "ACTION_POINTER_UP"; break; case MotionEvent.ACTION_HOVER_MOVE: prefix = "HOVER MOVE"; break; case MotionEvent.ACTION_HOVER_ENTER: prefix = "HOVER ENTER"; break; case MotionEvent.ACTION_HOVER_EXIT: prefix = "HOVER EXIT"; break; case MotionEvent.ACTION_SCROLL: prefix = "SCROLL"; break; default: prefix = Integer.toString(action); break; } return prefix; } // HACK // A quick and dirty string builder implementation optimized for GC. // Using String.format causes the application grind to a halt when // more than a couple of pointers are down due to the number of // temporary objects allocated while formatting strings for drawing or logging. private static final class FasterStringBuilder { private char[] mChars; private int mLength; public FasterStringBuilder() { mChars = new char[64]; } public FasterStringBuilder clear() { mLength = 0; return this; } public FasterStringBuilder append(String value) { final int valueLength = value.length(); final int index = reserve(valueLength); value.getChars(0, valueLength, mChars, index); mLength += valueLength; return this; } public FasterStringBuilder append(int value) { return append(value, 0); } public FasterStringBuilder append(int value, int zeroPadWidth) { final boolean negative = value < 0; if (negative) { value = -value; if (value < 0) { append("-2147483648"); return this; } } int index = reserve(11); final char[] chars = mChars; if (value == 0) { chars[index++] = '0'; mLength += 1; return this; } if (negative) { chars[index++] = '-'; } int divisor = 1000000000; int numberWidth = 10; while (value < divisor) { divisor /= 10; numberWidth -= 1; if (numberWidth < zeroPadWidth) { chars[index++] = '0'; } } do { int digit = value / divisor; value -= digit * divisor; divisor /= 10; chars[index++] = (char) (digit + '0'); } while (divisor != 0); mLength = index; return this; } public FasterStringBuilder append(float value, int precision) { int scale = 1; for (int i = 0; i < precision; i++) { scale *= 10; } value = (float) (Math.rint(value * scale) / scale); append((int) value); if (precision != 0) { append("."); value = Math.abs(value); value -= Math.floor(value); append((int) (value * scale), precision); } return this; } @Override public String toString() { return new String(mChars, 0, mLength); } private int reserve(int length) { final int oldLength = mLength; final int newLength = mLength + length; final char[] oldChars = mChars; final int oldCapacity = oldChars.length; if (newLength > oldCapacity) { final int newCapacity = oldCapacity * 2; final char[] newChars = new char[newCapacity]; System.arraycopy(oldChars, 0, newChars, 0, oldLength); mChars = newChars; } return oldLength; } }}

 

更多相关文章

  1. Android折线图
  2. Android折线图
  3. Android图形显示系统——下层显示4:图层合成上(合成原理与3D合成)
  4. RecyclerView正确打开分隔符
  5. a64_7.1-v3.1android拉伸绘制矩形区域距离默认平板外边界问题
  6. 在android使用OPENGL总结
  7. Android(安卓)实现 WheelView
  8. Android(安卓)Canvas绘图详解(图文)
  9. Android饼状图的绘制

随机推荐

  1. Android牟利之道(一)--界面嵌入有米广告
  2. Mono for Android 4.2初探
  3. Android 数据库对比
  4. 热点:Android 10(Android Q)发布
  5. android 阻尼效果(图片下拉变大)
  6. Google支付V3.0集成,使用Google play结算
  7. Android学习笔记(一)-Android与3G简介
  8. android-【机型-版本-分辨率】测试点罗列
  9. Android开发之低调的Service
  10. Perl登陆Android