Android实现触摸校正功能

当我第一次听到这个需求的时候,内心是迷茫的。触摸校正是个什么鬼,这个术语好像好多年之前才会有。想了想应该是电阻屏时代吧。然后又查资料知道了Android不是电容屏吗,是不需要触摸校正的,而只有当年的电阻屏才需要触摸校正。这下我懵了,因为网上各种关于android触摸校正的资料都很少,还有很多是需要在源码环境改源码的,或者是通过aidl方式去做。(当时为了弄出来一个IWindowManager可是各种入坑啊!)好了,废话不多说,进入正题:

进入正题之前先给效果图吧,因为我为人比较厚道。Android实现触摸校正功能

Android实现触摸校正功能


这一版触摸校正功能只用到了一个CalibrationActivity,onCreate里首先获取到了当前屏幕的宽高,

dpy = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
X_RES = dpy.getWidth();
Y_RES = dpy.getHeight();

然后会有一个初始化要触摸点的坐标的方法:

// TopLeft-->TopRight-->BottomRight-->BottomLeft-->Center
// For 240 * 320 resolution, we use 50 pixel as edge gap
private boolean initScreenPoints() {
    cal.xfb[TOP_LEFT] = EDGE_GAP;                // TopLeft
    cal.yfb[TOP_LEFT] = EDGE_GAP;

    cal.xfb[TOP_RIGHT] = X_RES - EDGE_GAP;        // TopRight
    cal.yfb[TOP_RIGHT] = EDGE_GAP;

    cal.xfb[BOTTOM_RIGHT] = X_RES - EDGE_GAP;    // BottomRight
    cal.yfb[BOTTOM_RIGHT] = Y_RES - EDGE_GAP;

    cal.xfb[BOTTOM_LEFT] = EDGE_GAP;            // BottomLeft
    cal.yfb[BOTTOM_LEFT] = Y_RES - EDGE_GAP;

    cal.xfb[CENTER] = X_RES / 2;                // Center
    cal.yfb[CENTER] = Y_RES / 2;
    return true;
}

接着就开始布局了,是new的一个自定义View:

setContentView(new MyView(this));
构造函数如下:

public MyView(Context c) {
    super(c);
    // set full screen and no title
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN);

    mContext = c;
    paint = new Paint();
    paint.setDither(true);
    paint.setAntiAlias(true); ////抗锯齿,如果没有调用这个方法,写上去的字不饱满,不美观,看地不太清楚
    paint.setStrokeWidth(2); //设置空心线宽
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE); //设置画笔风格,空心或者实心。
    bmp = Bitmap.createBitmap(X_RES, Y_RES, Bitmap.Config.ARGB_8888);
    cv = new Canvas(bmp);
    screen_pos = 0;
    drawCalibrationCross(screen_pos);
}
接下来我要说的重点:是在onDraw方法里,因为原本这个触摸校正是没有界面的,只有5个触摸点,还是自己绘制的,所以我想在原有基础上不妨碍正常功能的同时,又能增加一个界面,之前试过几次,界面是弄出来了,但是会把原来的触摸点给遮住,就是空有其表,功能完全不能实现了,因此尝试过不少方法后,还是选择了canvas来绘制出我想要的效果:

前方高能预警,修改界面核心onDraw方法如下:

canvas.drawColor(Color.BLACK);
canvas.drawBitmap(bmp, 0, 0, null);

//“欢迎字体大小
float txt_welcome_size = 60;
//“欢迎字数
float txt_welcome_count = 2;
//"请按住光标……"字体大小
float txt_content_size = 36;
//"请按住十字光标以校准"字数
float txt_content1_count = 10;
//"你的屏幕"字数
float txt_content2_count = 4;

//"欢迎"
Paint p = new Paint();
p.setTextSize(txt_welcome_size);
p.setFakeBoldText(true);
p.setColor(getResources().getColor(R.color.text_Welcome));
canvas.drawText("欢迎",
        (X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size / 2,
        Y_RES / 2 - txt_welcome_size - 30,
        p);

//"请按住光标中央以校准"
p.setFakeBoldText(false);
p.setColor(getResources().getColor(R.color.text_content1));
p.setTextSize(txt_content_size);
//参数2X_RES / 2 - (txt_content_size / 2 * txt_content1_count)):当前屏幕宽度的一半减去字数
canvas.drawText("请按住十字光标以校准",
        X_RES / 2 - (txt_content_size / 2 * txt_content1_count),
        Y_RES / 2 + 150,
        p);

//"你的屏幕"
p.setColor(getResources().getColor(R.color.text_content1));
p.setTextSize(txt_content_size);
canvas.drawText("你的屏幕",
        X_RES / 2 - txt_content_size / 2 * txt_content2_count,
        Y_RES / 2 + 200,
        p);

//线,渐变效果!!!
Shader shader = new LinearGradient((X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size * 2,
        (Y_RES / 2) - txt_welcome_size,
        X_RES / 2,
        (Y_RES / 2) - txt_welcome_size,
        new int[]{Color.WHITE, Color.GREEN},
        null,
        Shader.TileMode.MIRROR);
p.setShader(shader);

canvas.drawLine((X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size * 2,
        (Y_RES / 2) - txt_welcome_size,
        (X_RES / 2) + (txt_welcome_size / 2) + txt_welcome_size * 2,
        (Y_RES / 2) - txt_welcome_size,
        p);

此处主要是结合了坐标系,才能把你想要的效果摆放到正确的位置中去,因为居中显示其实在xml是很容易实现的,一个gravity=center就行,但是在代码里,特别现在还是在onDraw里,就不能这么干了,因为试了好多次都不能居中显示。

首先确定好要显示的字数,然后setTextSize,确定好字体大小,接着就调用canvas.drawText进行绘制了,

canvas.drawText("你的屏幕",
        X_RES / 2 - txt_content_size / 2 * txt_content2_count,
        Y_RES / 2 + 200,
        p);
参数1为x轴坐标点,参数2位y轴坐标点,参数3为画笔的实例p。

如果想要居中,就是要自己算字数和字体大小,然后用屏幕宽度一减就ok了,算法不难,可以自己试试。


然后因为原本xml中是一个横线的图片作为分隔,很好看,但是这里onDraw里面不好弄图片(也许是我不知道),我就自己画了一条线,并且还弄出了渐变的效果,虽不及原图,但也六七不离八了。


CalibrationActivity.Java 代码如下:

[java] view plain copy
  1. public class CalibrationActivity extends BaseActivity {  
  2.   
  3.     static final int SAMPLE_COUNTS = 5;  
  4.     static final int POINT_DEGREE = 2;  
  5.     static final int FACTOR_COUNTS = 7;  
  6.     static final int TOP_LEFT = 0;  
  7.     static final int TOP_RIGHT = 1;  
  8.     static final int BOTTOM_RIGHT = 2;  
  9.     static final int BOTTOM_LEFT = 3;  
  10.     static final int CENTER = 4;  
  11.     static final int X_AXIS = 0;  
  12.     static final int Y_AXIS = 1;  
  13.     static final int EDGE_GAP = 50;  
  14.   
  15.     static final String CALIBRATION_FILE = "/data/pointercal";  
  16.     static final String TAG = "CalibrationActivity";  
  17.     static final boolean DEBUG = true;  
  18.   
  19.     private int X_RES;  
  20.     private int Y_RES;  
  21.     private Display dpy;  
  22.   
  23.     private calibration cal;  
  24.   
  25.     @Override  
  26.     protected void onCreate(Bundle savedInstanceState) {  
  27.         super.onCreate(savedInstanceState);  
  28.         cal = new calibration();  
  29.   
  30.         dpy = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();  
  31.         X_RES = dpy.getWidth();  
  32.         Y_RES = dpy.getHeight();  
  33.   
  34.         initScreenPoints();  
  35.   
  36.         setContentView(new CalibrationView(this));  
  37.     }  
  38.   
  39.     class calibration {  
  40.         int x[] = new int[5];  
  41.         int y[] = new int[5];  
  42.         int xfb[] = new int[5];  
  43.         int yfb[] = new int[5];  
  44.         int a[] = new int[7];  
  45.     }  
  46.   
  47.     // TopLeft-->TopRight-->BottomRight-->BottomLeft-->Center  
  48.     // For 240 * 320 resolution, we use 50 pixel as edge gap  
  49.     private boolean initScreenPoints() {  
  50.         cal.xfb[TOP_LEFT] = EDGE_GAP;                // TopLeft  
  51.         cal.yfb[TOP_LEFT] = EDGE_GAP;  
  52.   
  53.         cal.xfb[TOP_RIGHT] = X_RES - EDGE_GAP;        // TopRight  
  54.         cal.yfb[TOP_RIGHT] = EDGE_GAP;  
  55.   
  56.         cal.xfb[BOTTOM_RIGHT] = X_RES - EDGE_GAP;    // BottomRight  
  57.         cal.yfb[BOTTOM_RIGHT] = Y_RES - EDGE_GAP;  
  58.   
  59.         cal.xfb[BOTTOM_LEFT] = EDGE_GAP;            // BottomLeft  
  60.         cal.yfb[BOTTOM_LEFT] = Y_RES - EDGE_GAP;  
  61.   
  62.         cal.xfb[CENTER] = X_RES / 2;                // Center  
  63.         cal.yfb[CENTER] = Y_RES / 2;  
  64.         return true;  
  65.     }  
  66.   
  67.     private boolean perform_calibration() {  
  68.         float n, x, y, x2, y2, xy, z, zx, zy;  
  69.         float det, a, b, c, e, f, g;  
  70.         float scaling = (float65536.0;  
  71.   
  72.         n = x = y = x2 = y2 = xy = 0;  
  73.         for (int i = 0; i < SAMPLE_COUNTS; i++) {  
  74.             n += 1.0;  
  75.             x += (float) cal.x[i];  
  76.             y += (float) cal.y[i];  
  77.             x2 += (float) (cal.x[i] * cal.x[i]);  
  78.             y2 += (float) (cal.y[i] * cal.y[i]);  
  79.             xy += (float) (cal.x[i] * cal.y[i]);  
  80.         }  
  81.   
  82.         det = n * (x2 * y2 - xy * xy) + x * (xy * y - x * y2) + y * (x * xy - y * x2);  
  83.         if (det < 0.1 && det > -0.1) {  
  84.             Log.w(TAG, "determinant is too small, det =" + det);  
  85.             return false;  
  86.         }  
  87.   
  88.         if (DEBUG) {  
  89.             Log.i(TAG, "(n,x,y,x2,y2,xy,det)=("  
  90.                     + n + ","  
  91.                     + x + ","  
  92.                     + y + ","  
  93.                     + x2 + ","  
  94.                     + y2 + ","  
  95.                     + xy + ","  
  96.                     + det + ")");  
  97.         }  
  98.   
  99.         a = (x2 * y2 - xy * xy) / det;  
  100.         b = (xy * y - x * y2) / det;  
  101.         c = (x * xy - y * x2) / det;  
  102.         e = (n * y2 - y * y) / det;  
  103.         f = (x * y - n * xy) / det;  
  104.         g = (n * x2 - x * x) / det;  
  105.   
  106.         Log.i(TAG, "(a,b,c,e,f,g)=("  
  107.                 + a + ","  
  108.                 + b + ","  
  109.                 + c + ","  
  110.                 + e + ","  
  111.                 + f + ","  
  112.                 + g + ")");  
  113.   
  114.         // Get sums for x calibration  
  115.         z = zx = zy = 0;  
  116.         for (int i = 0; i < SAMPLE_COUNTS; i++) {  
  117.             z += (float) cal.xfb[i];  
  118.             zx += (float) (cal.xfb[i] * cal.x[i]);  
  119.             zy += (float) (cal.xfb[i] * cal.y[i]);  
  120.         }  
  121.         // Now multiply out to get the calibration for X coordination  
  122.         cal.a[0] = (int) ((a * z + b * zx + c * zy) * (scaling));  
  123.         cal.a[1] = (int) ((b * z + e * zx + f * zy) * (scaling));  
  124.         cal.a[2] = (int) ((c * z + f * zx + g * zy) * (scaling));  
  125.         // Get sums for y calibration  
  126.         z = zx = zy = 0;  
  127.         for (int i = 0; i < SAMPLE_COUNTS; i++) {  
  128.             z += (float) cal.yfb[i];  
  129.             zx += (float) (cal.yfb[i] * cal.x[i]);  
  130.             zy += (float) (cal.yfb[i] * cal.y[i]);  
  131.         }  
  132.         // Now multiply out to get the calibration for Y coordination  
  133.         cal.a[3] = (int) ((a * z + b * zx + c * zy) * (scaling));  
  134.         cal.a[4] = (int) ((b * z + e * zx + f * zy) * (scaling));  
  135.         cal.a[5] = (int) ((c * z + f * zx + g * zy) * (scaling));  
  136.   
  137.         cal.a[6] = (int) scaling;  
  138.   
  139.         return true;  
  140.     }  
  141.   
  142.     private boolean saveCalibrationResult() {  
  143.         FileOutputStream fos;  
  144.         String res = "";  
  145.   
  146.         // save the calibration factor in file system for InputDevice  
  147.         try {  
  148.             fos = openFileOutput("pointercal.txt", Context.MODE_PRIVATE);  
  149.   
  150.             res = String.format("%d %d %d %d %d %d %d", cal.a[1], cal.a[2], cal.a[0], cal.a[4], cal.a[5], cal.a[3], cal.a[6]);  
  151.   
  152.             if (DEBUG) {  
  153.                 Log.i(TAG, "calibration result=" + res);  
  154.             }  
  155.             fos.write(res.getBytes());  
  156.             fos.close();  
  157.         } catch (FileNotFoundException e1) {  
  158.             // TODO Auto-generated catch block  
  159.             e1.printStackTrace();  
  160.             Log.w(TAG, "open calibration file write error: " + CALIBRATION_FILE);  
  161.         } catch (IOException e) {  
  162.             // TODO Auto-generated catch block  
  163.             e.printStackTrace();  
  164.         }  
  165.         return true;  
  166.     }  
  167.   
  168.     public class CalibrationView extends View {  
  169.         private Canvas cv;  
  170.         private Paint paint;  
  171.         private Bitmap bmp;  
  172.         private int screen_pos;  
  173.         private Context mContext;  
  174.   
  175.         public CalibrationView(Context c) {  
  176.             super(c);  
  177.             // set full screen and no title  
  178.             requestWindowFeature(Window.FEATURE_NO_TITLE);  
  179.             getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,  
  180.                     WindowManager.LayoutParams.FLAG_FULLSCREEN);  
  181.   
  182.             mContext = c;  
  183.             paint = new Paint();  
  184.             paint.setDither(true);  
  185.             paint.setAntiAlias(true); ////抗锯齿,如果没有调用这个方法,写上去的字不饱满,不美观,看地不太清楚  
  186.             paint.setStrokeWidth(2); //设置空心线宽  
  187.             paint.setColor(Color.WHITE);  
  188.             paint.setStyle(Paint.Style.STROKE); //设置画笔风格,空心或者实心。  
  189.             bmp = Bitmap.createBitmap(X_RES, Y_RES, Bitmap.Config.ARGB_8888);  
  190.             cv = new Canvas(bmp);  
  191.             screen_pos = 0;  
  192.             drawCalibrationCross(screen_pos);  
  193.         }  
  194.   
  195.         protected void onDraw(Canvas canvas) {  
  196.             canvas.drawColor(Color.BLACK);  
  197.             canvas.drawBitmap(bmp, 00null);  
  198.   
  199.             //“欢迎”字体大小  
  200.             float txt_welcome_size = 60;  
  201.             //“欢迎”字数  
  202.             float txt_welcome_count = 2;  
  203.             //"请按住十字光标以校准"字体大小  
  204.             float txt_content_size = 36;  
  205.             //"请按住十字光标以校准"字数  
  206.             float txt_content1_count = 10;  
  207.             //"你的屏幕"字数  
  208.             float txt_content2_count = 4;  
  209.   
  210.             //"欢迎"  
  211.             Paint p = new Paint();  
  212.             p.setTextSize(txt_welcome_size);  
  213.             p.setFakeBoldText(true);  
  214.             p.setColor(getResources().getColor(R.color.text_Welcome));  
  215.             canvas.drawText("欢迎",  
  216.                     (X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size / 2,  
  217.                     Y_RES / 2 - txt_welcome_size - 30,  
  218.                     p);  
  219.   
  220.             //"请按住光标中央以校准"  
  221.             p.setFakeBoldText(false);  
  222.             p.setColor(getResources().getColor(R.color.text_content1));  
  223.             p.setTextSize(txt_content_size);  
  224.             //参数2(X_RES / 2 - (txt_content_size / 2 * txt_content1_count)):当前屏幕宽度的一半减去字数  
  225.             canvas.drawText("请按住十字光标以校准",  
  226.                     X_RES / 2 - (txt_content_size / 2 * txt_content1_count),  
  227.                     Y_RES / 2 + 150,  
  228.                     p);  
  229.   
  230.             //"你的屏幕"  
  231.             p.setColor(getResources().getColor(R.color.text_content1));  
  232.             p.setTextSize(txt_content_size);  
  233.             canvas.drawText("你的屏幕",  
  234.                     X_RES / 2 - txt_content_size / 2 * txt_content2_count,  
  235.                     Y_RES / 2 + 200,  
  236.                     p);  
  237.   
  238.             //线,渐变效果!!!  
  239.             Shader shader = new LinearGradient((X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size * 2,  
  240.                     (Y_RES / 2) - txt_welcome_size,  
  241.                     X_RES / 2,  
  242.                     (Y_RES / 2) - txt_welcome_size,  
  243.                     new int[]{Color.WHITE, Color.GREEN},  
  244.                     null,  
  245.                     Shader.TileMode.MIRROR);  
  246.             p.setShader(shader);  
  247.   
  248.             canvas.drawLine((X_RES / 2) - (txt_welcome_size / 2) - txt_welcome_size * 2,  
  249.                     (Y_RES / 2) - txt_welcome_size,  
  250.                     (X_RES / 2) + (txt_welcome_size / 2) + txt_welcome_size * 2,  
  251.                     (Y_RES / 2) - txt_welcome_size,  
  252.                     p);  
  253.   
  254.         }  
  255.   
  256.         private boolean drawCalibrationCross(int pos) {  
  257.   
  258.             if (DEBUG) {  
  259.                 Log.i(TAG, "draw cross at pos " + pos);  
  260.             }  
  261.   
  262.             cv.drawColor(Color.BLACK);  
  263.   
  264.             // draw X line  
  265.             cv.drawLine(cal.xfb[pos] - 10, cal.yfb[pos],  
  266.                     cal.xfb[pos] - 2, cal.yfb[pos], paint);  
  267.             cv.drawLine(cal.xfb[pos] + 2, cal.yfb[pos],  
  268.                     cal.xfb[pos] + 10, cal.yfb[pos], paint);  
  269.   
  270.             // draw Y line  
  271.             cv.drawLine(cal.xfb[pos], cal.yfb[pos] - 10,  
  272.                     cal.xfb[pos], cal.yfb[pos] - 2, paint);  
  273.             cv.drawLine(cal.xfb[pos], cal.yfb[pos] + 2,  
  274.                     cal.xfb[pos], cal.yfb[pos] + 10, paint);  
  275.   
  276.             invalidate();  
  277.             return true;  
  278.         }  
  279.   
  280.         public boolean onTouchEvent(MotionEvent event) {  
  281.             float tmpx, tmpy;  
  282.             boolean ret;  
  283.             if (screen_pos > SAMPLE_COUNTS - 1) {  
  284.                 Log.i(TAG, "get sample ok");  
  285.                 return true;  
  286.             }  
  287.   
  288.             if (event.getAction() == MotionEvent.ACTION_UP) {  
  289.                 tmpx = event.getX();  
  290.                 tmpy = event.getY();  
  291.                 if (Math.abs(cal.xfb[screen_pos] - tmpx) > 15 &&  
  292.                         Math.abs(cal.yfb[screen_pos] - tmpy) > 15) {  
  293.                     UIUtils.showToast(mContext, "无效的校准点");  
  294.                     return false;  
  295.                 }  
  296.   
  297.                 cal.x[screen_pos] = (int) (event.getX() * 4096.0 / (float) X_RES + 0.5);  
  298.                 cal.y[screen_pos] = (int) (event.getY() * 4096.0 / (float) Y_RES + 0.5);  
  299.   
  300.                 if (screen_pos == 4) {  
  301.                     ret = perform_calibration();  
  302.                     if (ret) {  
  303.                         saveCalibrationResult();  
  304.                         UIUtils.showToast(mContext, "校正完毕!");  
  305.                         Intent intent = new Intent(CalibrationActivity.this, CalibrationCompleteActivity.class);  
  306.                         startActivity(intent);  
  307.                         return true;  
  308.                     } else {  
  309.                         screen_pos = 0;  
  310.                         Log.w(TAG, "Calibration failed");  
  311.                     }  
  312.                 } else {  
  313.                     screen_pos++;  
  314.                     drawCalibrationCross(screen_pos);  
  315.                 }  
  316.             }  
  317.             return true;  
  318.         }  
  319.     }  
  320. }  


要点解释:BaseActivity里面主要就写了两个方法,一个是将所有Activity添加进一个Activity的list,命名为ActivityCollector,这样是为了当校正完成跳转到第二个Activity后,点击确定能关闭所有Activity直接退出程序;第二个是remove掉Activity,使其finish。ActivityCollector里面分别有:

public static void addActivity(Activity activity) {
    activities.add(activity);
}

public static void removeActivity(Activity activity) {
    activities.remove(activity);
}

public static void finishAll() {
    for (Activity activity : activities) {
        if (!activity.isFinishing()) {
            activity.finish();
        }
    }
}
这三个方法。

目测我写的代码还是很好懂的,触摸校正的具体算法代码是参照网上的,针对当前触摸校正功能的界面以及其他的一些细节是我修改的。有写的不对或有更好方法实现的话,还请大神们不吝赐教,本人将虚心接受,在撸码的道路上越走越远!