由于安卓触摸事件的分发略显缓慢,尤其是在快速移动时点密度的降低可能会导致绘制曲线的点变得稀薄,从而增加曲线的走样程度。因此我用了JNI + Linux C + NDK做了一套直接从底层设备获取触摸设备坐标信息的库。
一、编写C文件和MakeFile:
/**getevent2.c**/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include <string.h>
#include<jni.h>
#include<android/log.h>
#include <stdint.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/inotify.h>
#include <sys/limits.h>
#include <sys/poll.h>
#include <linux/input.h>
#include <errno.h>
#include "getevent.h"
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "native-activity", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "native-activity", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "native-activity", __VA_ARGS__))
static const char *TAG="getevent";
static const char *device_path = "/dev/input/event4";
//全局变量
JavaVM *g_jvm = NULL;
JNIEnv *g_env = NULL;
jobject g_obj = NULL;
jclass native_clazz;
jmethodID callback;
/**将JAVA字符串数组转C char字符数组(俗称字符串)的数组**/
char* jstringToChar(JNIEnv *env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env,"java/lang/String");
jstring strencode = (*env)->NewStringUTF(env,"GB2312");//转换成Cstring的GB2312,兼容ISO8859-1
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);第二个参数是方法名,第三个参数是getBytes方法签名
//获得签名:javap -s java/lang/String: (Ljava/lang/String;)[B
jmethodID mid = (*env)->GetMethodID(env,clsstring,"getBytes","(Ljava/lang/String;)[B");
//等价于调用这个方法String.getByte("GB2312");
//将jstring转换成字节数组
//用Java的String类getByte方法将jstring转换为Cstring的字节数组
jbyteArray barr= (jbyteArray) (*env)->CallObjectMethod(env,jstr,mid,strencode);
jsize alen = (*env)->GetArrayLength(env,barr);
jbyte* ba = (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
LOGI("alen=%d\n",alen);
if(alen > 0)
{
rtn = (char*)malloc(alen+1+128);
LOGI("rtn address == %p",&rtn);//输出rtn地址
memcpy(rtn,ba,alen);
rtn[alen]=0; //"\0"
}
(*env)->ReleaseByteArrayElements(env,barr,ba,0);
return rtn;
}
JNIEXPORT void Java_com_cjz_jnigetevent_NativeEventCallBack_setJNIEnv( JNIEnv* env, jobject obj){
//保存全局JVM以便在子线程中使用
(*env)->GetJavaVM(env,&g_jvm);
//不能直接赋值(g_obj = obj)
g_obj = (*env)->NewGlobalRef(env,obj);
}
/**使用jni执行getevent数据获取操作**/
JNIEXPORT void JNICALL Java_com_cjz_jnigetevent_NativeEventCallBack_startTrace(JNIEnv *env, jobject thiz, jstring path){
device_path = jstringToChar(env, path);
int lastX = -1, lastY = -1;
int fd=-1;
struct input_absinfo absI;
fd_set rds;
fd = open(device_path, O_RDONLY);
if(fd < 0){
LOGI("main init: device open failure");
}
//保存全局JVM以便在子线程中使用
(*env)->GetJavaVM(env, &g_jvm);
//不能直接赋值(g_obj = obj)
g_obj = (*env)->NewGlobalRef(env, thiz);
g_env = env;
{
native_clazz = (*g_env)->GetObjectClass(g_env, g_obj);
callback = (*g_env)->GetMethodID(g_env, native_clazz, "callback", "(IIIIII)V"); //参数为6个int,返回值为void
//测试一下:
//(*g_env)->CallVoidMethod(g_env, g_obj, callback, 0, 0, 0, 0, 0, 0);
}
LOGI("main init -1");
while(1 && fd != 0) {
FD_ZERO( &rds );
FD_SET( fd, &rds );
/*调用select检查是否能够从/dev/input/event4设备读取数据*/
int ret = select( fd + 1, &rds, NULL, NULL, NULL );
if ( ret < 0 ){
continue;
}
else if ( FD_ISSET( fd, &rds ) ) {
//得到X轴的abs信息
int x, y, pressure;
struct input_event event;
ioctl(fd,EVIOCGABS(ABS_X), &absI);
//printf("x abs lastest value=%d\n",absI.value);
//printf("x abs min=%d\n",absI.minimum);
//printf("x abs max=%d\n",absI.maximum);
x = absI.value;
//得到y轴的abs信息
ioctl(fd,EVIOCGABS(ABS_Y),&absI);
//printf("y abs lastest value=%d\n",absI.value);
//printf("y abs min=%d\n",absI.minimum);
//printf("y abs max=%d\n",absI.maximum);
y = absI.value;
//得到按压轴的abs信息
ioctl(fd,EVIOCGABS(ABS_PRESSURE),&absI);
//printf("pressure abs lastest value=%d\n",absI.value);
//printf("pressure abs min=%d\n",absI.minimum);
//printf("pressure abs max=%d\n",absI.maximum);
pressure = absI.value;
/*从fd中读取sizeof(struct input_event)那么多字节的数据,放到event结构体变量的内存地址的内存块中
字节和该数据结构对此,因此可以直接通过这个数据结构方便地读出数据*/
read(fd, &event, sizeof(struct input_event));
(*g_env)->CallVoidMethod(g_env, g_obj, callback, x, y, pressure, event.type, event.code, event.value);
lastX = x;
lastY = y;
}
}
}
首先感谢如下文章提供的资料:
https://blog.csdn.net/a694543965/article/details/79935086 《linux读取触摸屏事件数据》
https://blog.csdn.net/wave_1102/article/details/39213935 Linux input.h文件
整段代码的大致意义是:
1、以文件的形式读取设备,并用合适的数据结构变量和读到的数据做字节对齐,然后就可以方便地利用这些数据结构里面的变量进行判断,并利用JNI环境回调对应包、类中的JAVA函数,并传入参数。startTrace函数只有最后一个jstring参数用于传入需要读取哪个设备的的path,前面几个参数都是JVM会自己传入的JVM环境上下文和类对象信息。
C文件写好之后就是写MakeFile了,makeFile文件内容如下(大意是使用lm、llog库,编译完之后生成名为libJNIGetEventCtrl的库文件),并保存为Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -lm -llog
LOCAL_MODULE := JNIGetEventCtrl
LOCAL_SRC_FILES := geteventV2.c
include $(BUILD_SHARED_LIBRARY)
随后新建3个文件夹,并把Adnroid.mk和geteventV2.c放到jni文件夹中:
用命令行进入到jni目录,并使用ndk-build进行编译,在libs文件夹中便得到各指令集的so库了。
二、编写JAVA工具类:
首先修改Android Studio的build文件,具体编写可以参考我的写法:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "27.0.3"
defaultConfig {
applicationId "ggg.project.touchsrceenlistener"
minSdkVersion 17
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
//abiFilters "armeabi-v7a", "x86", "x86_64"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
}
重新build一次工程,并在工程目录树的libs文件夹中复制JNI文件夹的libs中的4个文件夹过去,复制完后如图:
刚才C文件中的Java_com_cjz_jnigetevent_NativeEventCallBack,意义就是让com.cjz.jnigetevent的NativeEventCallBack把该C函数通过JNI投影到本类对应的函数中,这样当其他人调用该类的函数时,便可以像调用Java函数那样操作它。现在我们来编写工具类:
首先创建包和类:
类内容如下:
package com.cjz.jnigetevent;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* Created by cjz on 2018/4/26.
*/
public class NativeEventCallBack {
static {
System.loadLibrary("JNIGetEventCtrl");
}
private int width;
private int height;
private int lastAction = MotionEvent.ACTION_CANCEL;
private int ratio; //宽高比
private NEventCallBack eventCallBack;
public native void startTrace(String path);
public NativeEventCallBack(int width, int height){
this.width = width;
this.height = height;
ratio = width / height;
}
//监控触摸设备,java被C语言回调
private void callback(int x, int y, int pressure, int eventType, int eventCode, int eventValue){
//Log.i("NativeEventCallBack", String.format("x:%d, y:%d, pressure:%d, type:%d, code:%d, value:%d", x, y, pressure, eventType, eventCode, eventValue));
if(eventCallBack != null && (eventType == 1 || eventType == 3)) {
float fX = (x / 32767f) * width;
float fY = (y / 32767f) * height;
// Log.i("nativeEvent", String.format("x:%f, y:%f, width:%d, height:%d", fX, fY, width, height));
EventData eventData = new EventData();
eventData.x = fX;
eventData.y = fY;
eventData.pressure = pressure;
eventData.eventType = eventType;
eventData.eventCode = eventCode;
eventData.eventValue = eventValue;
if(eventCode == 0x014A && eventValue == 1) {
if(lastAction == MotionEvent.ACTION_DOWN) {
eventData.action = MotionEvent.ACTION_MOVE;
} else {
eventData.action = MotionEvent.ACTION_DOWN;
}
lastAction = eventData.action;
} else if(eventCode == 0x014A && eventValue == 0) {
eventData.action = MotionEvent.ACTION_UP;
lastAction = eventData.action;
} else {
if (lastAction == MotionEvent.ACTION_DOWN) {
eventData.action = MotionEvent.ACTION_MOVE;
} else if (lastAction == MotionEvent.ACTION_UP) {
eventData.action = MotionEvent.ACTION_UP;
}
}
if(eventData.action != MotionEvent.ACTION_CANCEL) {
eventCallBack.nativeEvent(eventData);
}
}
}
public void setEventCallBack(NEventCallBack eventCallBack) {
this.eventCallBack = eventCallBack;
}
public interface NEventCallBack{
void nativeEvent(EventData eventData);
}
public class EventData {
public float x;
public float y;
public int pressure;
public int eventType;
public int eventCode;
public int eventValue;
public int action = MotionEvent.ACTION_CANCEL;
}
}
其中startTrace为开启数据接收用,callback函数用于接收JNI回调的参数,分析过后传给传入的回调对象。x,y坐标数据都是0~32767,因此需要通过计算一个x,y对32767的比例之后乘以屏幕的长和宽,才能得到该触摸事件对应屏幕的真实位置。
通过查阅input.h得知,其中eventType为1和3时是各类触摸事件,0是同步事件不必对其进行判断。eventCode=0x14a而且value=1时为按下,eventCode=0x14a而且value=0时为松开,可以利用它来实现ACTION_DOWN,ACTION_MOVE(down完之后还是down就用move赋值),ACTION_UP进行赋值。
通过以上内容便可以获得原始触摸数据,并通过计算分别将x,y其占最大值的百分比乘以宽和高得出其所处的屏幕x,y坐标,再利用触摸事件判断事件之间的间隔,便可以实现直接通过底层JNI而非安卓SDK提供的onTouchEvent回调函数来获取触摸事件了。
资源文件地址:
https://download.csdn.net/download/cjzjolly/10614907
有空再传gitHub