实训项目:研发一个可以通过键盘和语音实现输入个性化手写字体的聊天应用,并添加多点触控交互技术。
组员各自工作:
李晨晨:
我本周的任务是:根据触摸位置与中心位置的偏置实现相应的字体局部变形。
这个功能的主要作用是实现异形文字,即同一文字的不同外形。比如即使是同一个人把一个字母写两遍,它们也不会完全相同。
1.文字手写绘入及保存:
绘制我们使用了canvas画布,实现非常简单,主要在ImageView的监听器中定义如下
showBitmap = Bitmap.createBitmap(iv_canvas.getWidth(),
iv_canvas.getHeight(), Bitmap.Config.ARGB_8888);
show_canvas = new Canvas(showBitmap);
show_canvas.drawColor(Color.WHITE);
图片的保存
/*
* 保存图片到SD卡上
*/
protected void saveBitmap() {
try {
// 保存图片到SD卡上
File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".png");
FileOutputStream stream = new FileOutputStream(file);
baseBitmap.compress(CompressFormat.PNG, 100, stream);
Toast.makeText(MainActivity.this, "保存图片成功", Toast.LENGTH_LONG).show();
// Android设备Gallery应用只会在启动的时候扫描系统文件夹
// 这里模拟一个媒体装载的广播,用于使保存的图片可以在Gallery中查看
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
sendBroadcast(intent);
} catch (Exception e) {
Toast.makeText(MainActivity.this, "保存图片失败", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
2.交互式变形
一开始我们从网上找各种方法,一个比较好的是canvas的drawBitmapMesh网格变形方法。但经过大量的实验发现没法把变形后的bitmap提取出来,但我们必须要保存它,所以最后放弃了。
之后我自己写了一个比网格变形粗糙的方法,但因为效果比较显著且简单,所以就用了这个方法
方法如图所示:中间圆点为触摸点,根据它与图片中心的偏置来相应的拉扯图片(将每行、每列像素放大),然后将新图片每个点的颜色反向映射到原图像对应点的颜色。
protected int[][] distort(int[][] pixels_copy, int dx, int dy, int row, int col) {
int[][] result_pixels1 = new int[row][col];
int[][] result_pixels2 = new int[row][col];
int currentY, currentX;//当前要拉伸的行数
int xAdd1, yAdd1, xAdd, yAdd;
//逐行拉伸
for (currentY = 0; currentY < row; currentY++) {
//对于每一行要扩展的像素数
if (currentY <= dy) {
xAdd = currentY * Math.abs(dx - col / 2) / dy;
} else {
xAdd = (row - currentY) * Math.abs(dx - col / 2) / (row - dy);
}
//判断往左还是往右拉伸
if (dx <= (col / 2)) {
//往左拉伸
xAdd1 = xAdd;
} else {
xAdd1 = 0;
}
//反向映射当前像素点的颜色
for (int x = 0; x < col; x++) {
int x0 = (x + xAdd1) * col / (col + xAdd);//对应的原图x坐标
result_pixels1[currentY][x] = pixels_copy[currentY][x0];
}
}
//逐列拉伸
for (currentX = 0; currentX < col; currentX++) {
//对于每一列要扩展的像素数
if (currentX <= dx) {
yAdd = currentX * Math.abs(dy - row / 2) / dx;
} else {
yAdd = (col - currentX) * Math.abs(dy - row / 2) / (col - dx);
}
//判断往上还是往下拉伸
if (dy <= row / 2) {
yAdd1 = yAdd;
} else {
yAdd1 = 0;
}
//反向映射颜色值
for (int y = 0; y < row; y++) {
int y0 = (y + yAdd1) * row / (row + yAdd);
result_pixels2[y][currentX] = result_pixels1[y0][currentX];
}
}
return result_pixels2;
}
效果如下:
仝心:
这几天主要在实现录音功能。
首先自定义了一个音频记录管理器AudioRecordManager用于录音,定义prepareAudio方法,用于设置输出路径,音频源、麦克风,输出格式和音频编码。
- public void prepareAudio(){
- try {
- hasPrepare = false;
- File dir = new File(mAudioDir);
- if (!dir.exists()){
- dir.mkdirs();
- }
- mMediaRecorder = new MediaRecorder();
- String fileName = UUID.randomUUID().toString() + ".arm";
- File file = new File(dir,fileName);
- mCurrentFilePath = file.getAbsolutePath();
- // 设置输出路径
- mMediaRecorder.setOutputFile(mCurrentFilePath);
- // 设置音频源,麦克风
- mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
- // 设置输出格式
- mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);
- // 设置音频编码
- mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
- // 准备
- mMediaRecorder.prepare();
- mMediaRecorder.start();
- hasPrepare = true;
- if (mStateListener != null){
- mStateListener.prepareFinish(mCurrentFilePath);
- }
- } catch (IOException e) {
- if (mStateListener != null){
- mStateListener.prepareError(e.getMessage());
- }
- e.printStackTrace();
- }
- }
接下来获取音量等级,等级上限作为参数传入
- public int getVoiceLevel(int maxLevel){
- try {
- if (hasPrepare){
- // getMaxAmplitude = 0 - 32767
- return maxLevel * mMediaRecorder.getMaxAmplitude() / 32768 + 1;
- }
- }catch (Exception e){
- return 1;
- }
- return 1;
- }
- public void releaseAudio(){
- mMediaRecorder.stop();
- mMediaRecorder.release();
- mMediaRecorder = null;
- hasPrepare = false;
- }
- public void cancelAudio(){
- releaseAudio();
- if (mCurrentFilePath != null){
- File file = new File(mCurrentFilePath);
- file.delete();
- }
- }
接下来定义音频对话管理器AudioDialogManager类用于在和好友的对话中发送录音。
- private Context mContext;
- private Dialog mDialog;
- private LayoutInflater mInflater;
- private ImageView mIvRecord;
- private ImageView mIvVoiceLevel;
- private TextView mTvTip;
- public RecordDialogManager(Context context){
- mContext = context;
- mInflater = LayoutInflater.from(mContext);
- }
显示之前的消息记录中的录音的方法:
- public void showDialogRecord(){
- View view = mInflater.inflate(R.layout.dialog_audio_record_button,null);
- mDialog = new Dialog(mContext, R.style.Theme_Audio_Record_Button);
- mDialog.setContentView(view);
- mIvRecord = (ImageView) mDialog.findViewById(R.id.iv_record);
- mIvVoiceLevel = (ImageView) mDialog.findViewById(R.id.iv_voice_level);
- mTvTip = (TextView) mDialog.findViewById(R.id.tv_dialog_tip);
- mDialog.show();
- }
在聊天中发送录音的操作指示,包括取消发送、提示录音时间过短、取消发送
- public void showRecording(){
- if (mDialog != null && mDialog.isShowing()){
- mIvRecord.setImageResource(R.drawable.recorder);
- mIvVoiceLevel.setVisibility(View.VISIBLE);
- mTvTip.setText(mContext.getString(R.string.move_up_cancel));
- }
- }
- public void showDialogToShort(){
- if (mDialog != null && mDialog.isShowing()){
- mIvRecord.setImageResource(R.drawable.voice_to_short);
- mIvVoiceLevel.setVisibility(View.GONE);
- mTvTip.setText(mContext.getString(R.string.record_to_short));
- }
- }
- public void showDialogWantCancel(){
- if (mDialog != null && mDialog.isShowing()){
- mIvRecord.setImageResource(R.drawable.cancel);
- mIvVoiceLevel.setVisibility(View.GONE);
- mTvTip.setText(mContext.getString(R.string.release_cancel));
- }
- }
- public void updateVoiceLevel(int level){
- if (mDialog != null && mDialog.isShowing()){
- int resId = mContext.getResources().getIdentifier("v"+level,
- "drawable",mContext.getPackageName());
- mIvVoiceLevel.setImageResource(resId);
- }
- }
张静:
1.初始化画布并记录用户手指动作,完成绘画
- private View.OnTouchListener touch = new OnTouchListener() {
- // 定义手指开始触摸的坐标
- float startX;
- float startY;
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- // 用户按下动作
- case MotionEvent.ACTION_DOWN:
- // 第一次绘图初始化内存图片,指定背景为白色
- if (baseBitmap == null) {
- baseBitmap = Bitmap.createBitmap(iv_canvas.getWidth(),
- iv_canvas.getHeight(), Bitmap.Config.ARGB_8888);
- canvas = new Canvas(baseBitmap);
- canvas.drawColor(Color.WHITE);
- }
- // 记录开始触摸的点的坐标
- startX = event.getX();
- startY = event.getY();
- break;
- // 用户手指在屏幕上移动的动作
- case MotionEvent.ACTION_MOVE:
- // 记录移动位置的点的坐标
- float stopX = event.getX();
- float stopY = event.getY();
- //根据两点坐标,绘制连线
- paint.setARGB(255, 0, 0, 0);
- canvas.drawLine(startX, startY, stopX, stopY, paint);
- // 更新开始点的位置
- startX = event.getX();
- startY = event.getY();
- // 把图片展示到ImageView中
- iv_canvas.setImageBitmap(baseBitmap);
- break;
- case MotionEvent.ACTION_UP:
- break;
- default:
- break;
- }
- return true;
- }
- };
2. 保存图片到SD卡上
- protected void saveBitmap() {
- try {
- // 保存图片到SD卡上
- File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".png");
- FileOutputStream stream = new FileOutputStream(file);
- baseBitmap.compress(CompressFormat.PNG, 100, stream);
- Toast.makeText(MainActivity.this, "保存图片成功", Toast.LENGTH_LONG).show();
- // Android设备Gallery应用只会在启动的时候扫描系统文件夹
- // 这里模拟一个媒体装载的广播,用于使保存的图片可以在Gallery中查看
- Intent intent = new Intent();
- intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
- intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
- sendBroadcast(intent);
- } catch (Exception e) {
- Toast.makeText(MainActivity.this, "保存图片失败", Toast.LENGTH_LONG).show();
- e.printStackTrace();
- }
- }
3. 清除画板
- protected void resumeCanvas() {
- // 手动清除画板的绘图,重新创建一个画板
- if (showBitmap != null) {
- showBitmap = Bitmap.createBitmap(iv_canvas.getWidth(),
- iv_canvas.getHeight(), Bitmap.Config.ARGB_8888);
- show_canvas = new Canvas(showBitmap);
- show_canvas.drawColor(Color.WHITE);
- new_canvas.setImageBitmap(showBitmap);
- //Toast.makeText(MainActivity.this, "清除画板成功,可以重新开始绘图", Toast.LENGTH_LONG).show();
- }
- if (baseBitmap != null) {
- baseBitmap = Bitmap.createBitmap(iv_canvas.getWidth(),
- iv_canvas.getHeight(), Bitmap.Config.ARGB_8888);
- canvas = new Canvas(baseBitmap);
- canvas.drawColor(Color.WHITE);
- iv_canvas.setImageBitmap(baseBitmap);
- Toast.makeText(MainActivity.this, "清除画板成功,可以重新开始绘图", Toast.LENGTH_LONG).show();
- }
- }
4.我们要实现的功能是基于一个聊天app,因此我了解并学习了一下相关的聊天软件开发基础知识
张雨薇:
创新实训第一周
实训开始的第一周,我上网查询了许多有关多点触控的论文和资料,为今后的代码编写做准备。
论文中提到多点触摸技术的引入使得以触摸屏为代表的人机交互方式更加便利和自然,但同时也带来了如何识别基于多点触摸双手交互手势的问题,多点触摸原理的不同也导致了对双手手势识别方法的不同。此外,常见商用多点触摸设备提供的手势交互语义主要面向日常生活使用,缺乏针对军用,特别是针对指挥所应用的交互语义。在指挥所应用中,需要多点触摸自然手势的环境主要是基于地理信息系统的图上操作,如何将自然手势映射到地理信息系统的图上操作,是自然手势识别的一个重要问题。
针对指挥所应用背景,利用自然交互手势完成轨迹相对比较单调的特点,基于指挥所图上作业应用的交互手势语义定义和触摸信号采集,文中提出了一整套简捷可适应指挥所作业的多点触摸自然手势定义与识别方法,并基于红外多点触摸设备实现了上述方法,进行了实验验证。
指挥所中基于多点触摸的自然手势交互的实现主要包括指挥所自然手势的定义、触摸信号采集、手势识别三部分。根据指挥所作业中的人机交互任务及对人手自然张开与平面触摸时的触点位置关系特征,定义静态和动态两类触摸手势,建立手势集合数据库,包括静态触摸手势集合和动态触摸手势集合。动态触摸手势集合包括平移手势、弧移手势、双手旋转手势、单手旋转手势、缩放手势、捏取释放手势和撮取手势,其中平移手势、弧移手势、捏取释放手势和撮取手势默认为单手手势,缩放手势默认为双手手势。识别方法的设计需基于上述定义的交互手势集合中手势的特点进行,这样才能保证较高的识别正确率和识别效率。对于静态手势而言,人手自然张开触摸时,由于手指长度和位置的不同,触点两两之间的距离有所不同且触点所构成多边形各顶角的角度也呈现一定的特征。经过对不同身高个体手指自然张开触摸平面时触点间距离的测量数据进行统计,确定单手触点间的最大距离,以此为依据将触点分为左右手两个集合。动态手势识别流程。首先判断移动触点间在一个计算周期内有无相对运动,若移动触点间有任意两点之间的相对运动距离大于阈值,则判断为有相对运动;否则,判断为无相对运动,将相对无运动的移动触点模糊处理为一个触点,触点位置取其几何中心值,单移动触点的位置取原值。