月度归档: 2021 年 5 月

仿微信视频通话大小视图切换(SurfaceView实现)

前言

前一段时间做了一个即时通讯的项目,在项目中遇到很多坑,有时间一一做个总结,项目消息发送基于XMPP+Tigase,语言视频通话基于PJSIP+FreeSWITCH,项目UI仿微信。做到视频通话时,遇到本地视图与远程视图切换,网上搜了一篇相关的博客,根据大神思路写了这个Demo,其中用的是第三直播源可能有点不稳定,切换过程可能存在黑屏和无响应的情况,但是用的Pjsip中切换还是很流程的;

布局

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <FrameLayout
  3. xmlns:android=“http://schemas.android.com/apk/res/android”
  4. xmlns:tools=“http://schemas.android.com/tools”
  5. android:layout_width=“match_parent”
  6. android:layout_height=“match_parent”
  7. tools:context=“com.demo.surfaceviewdemo.MainActivity”>
  8. <RelativeLayout
  9. android:layout_width=“match_parent”
  10. android:layout_height=“match_parent”>
  11. <RelativeLayout
  12. android:id=“@+id/rl_remote”
  13. android:layout_width=“match_parent”
  14. android:layout_height=“wrap_content”>
  15. <SurfaceView
  16. android:id=“@+id/surfaceview_remote”
  17. android:layout_width=“match_parent”
  18. android:layout_height=“match_parent”/>
  19. </RelativeLayout>
  20. <RelativeLayout
  21. android:id=“@+id/rl_local”
  22. android:layout_width=“wrap_content”
  23. android:layout_height=“wrap_content”
  24. android:layout_alignParentRight=“true”>
  25. <SurfaceView
  26. android:id=“@+id/surfaceview_local”
  27. android:layout_width=“wrap_content”
  28. android:layout_height=“wrap_content”/>
  29. </RelativeLayout>
  30. </RelativeLayout>
  31. <!–通话时显示的–>
  32. <LinearLayout
  33. android:id=“@+id/ll_call_container”
  34. android:layout_width=“match_parent”
  35. android:layout_height=“wrap_content”
  36. android:layout_gravity=“bottom”
  37. android:layout_marginBottom=“25dp”
  38. android:gravity=“center_horizontal”
  39. android:orientation=“horizontal”>
  40. <TextView
  41. android:id=“@+id/tv_call_quiet”
  42. android:layout_width=“wrap_content”
  43. android:layout_height=“wrap_content”
  44. android:layout_weight=“1”
  45. android:drawablePadding=“10dp”
  46. android:drawableTop=“@mipmap/chat_video_change_voice_img”
  47. android:gravity=“center_horizontal”
  48. android:text=“切到语音聊天”
  49. android:textColor=“#ffffff”
  50. android:textSize=“12sp”/>
  51. <TextView
  52. android:id=“@+id/tv_handup_call”
  53. android:layout_width=“wrap_content”
  54. android:layout_height=“wrap_content”
  55. android:layout_weight=“1”
  56. android:drawablePadding=“10dp”
  57. android:drawableTop=“@mipmap/chat_video_guaduan_img_normal”
  58. android:gravity=“center_horizontal”
  59. android:text=“挂断”
  60. android:textColor=“#ffffff”
  61. android:textSize=“12sp”/>
  62. <TextView
  63. android:id=“@+id/tv_change_camera”
  64. android:layout_width=“wrap_content”
  65. android:layout_height=“wrap_content”
  66. android:layout_weight=“1”
  67. android:drawablePadding=“10dp”
  68. android:drawableTop=“@mipmap/chat_video_change_camera_img”
  69. android:gravity=“center_horizontal”
  70. android:text=“转换摄像头”
  71. android:textColor=“#ffffff”
  72. android:textSize=“12sp”/>
  73. </LinearLayout>
  74. </FrameLayout>

代码

为了实现跟微信一样的效果,普通屏幕全屏显示,为了不让视频内容挤到刘海屏中,添加一下代码:

  1. //如果判断有刘海屏不让填充到状态栏
  2. if (DisplayUtil.hasNotchScreen(this)) {
  3. getWindow().addFlags(
  4. WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
  5. | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
  6. } else {
  7. getWindow().addFlags(
  8. WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
  9. | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
  10. | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
  11. }

大小视图切换代码:

  1. /**
  2. * 大小视图切换 (小视图在前面、大视图在后面)
  3. *
  4. * @param sourcView 之前相对布局大小
  5. * @param beforeview 之前surfaceview
  6. * @param detView 之后相对布局大小
  7. * @param afterview 之后surfaceview
  8. */
  9. private void zoomOpera(View sourcView, SurfaceView beforeview,
  10. View detView, SurfaceView afterview) {
  11. RelativeLayout paretview = (RelativeLayout) sourcView.getParent();
  12. paretview.removeView(detView);
  13. paretview.removeView(sourcView);
  14. //设置远程大视图RelativeLayout 的属性
  15. RelativeLayout.LayoutParams params1 = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
  16. RelativeLayout.LayoutParams.MATCH_PARENT);
  17. params1.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
  18. beforeview.setZOrderMediaOverlay(true);
  19. beforeview.getHolder().setFormat(PixelFormat.TRANSPARENT);
  20. sourcView.setLayoutParams(params1);
  21. //设置本地小视图RelativeLayout 的属性
  22. params1 = new RelativeLayout.LayoutParams(defaultLocalwidth, defaultLocalHeight);
  23. params1.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
  24. params1.setMargins(0, defaultLocalMargin, defaultLocalMargin, 0);
  25. //在调用setZOrderOnTop(true)之后调用了setZOrderMediaOverlay(true) 遮挡问题
  26. afterview.setZOrderOnTop(true);
  27. afterview.setZOrderMediaOverlay(true);
  28. afterview.getHolder().setFormat(PixelFormat.TRANSPARENT);
  29. detView.setLayoutParams(params1);
  30. paretview.addView(sourcView);
  31. paretview.addView(detView);
  32. }

效果图一

效果图二

 

一步搞定头像选择、裁剪、拍照的Android图片选择框架

前言

几乎每个APP都需要图片选择和裁剪功能,因为涉及到相机和存储,所以该功能还是要考虑很多兼容性的。这也是github上有一大堆图片选择框架的原因,但是你会发现github上找的图片选择框架并不是简单的只有图片选择, 它还包含视频选择、视频录制、图片压缩等等一大堆功能。其实你只需要一个头像选择的功能,那些框架很多功能你根本用不到,而且代码至少也几十个类,后期有问题改起来也是麻烦事。所以我就封装了一个代码及其简洁的图片选择框架,没有任何多余的功能,涉及的主要功能类只有3个,使用也是非常简单。

 

效果图如下:

效果图.jpg

功能特点

  • 支持通过拍照获取图片
  • 支持通过相册获取图片
  • 支持图片裁剪
  • 支持仿IOS底部弹出选择菜单ActionSheet效果
  • 支持6.0动态授予权限
  • 解决图片有黑边问题
  • 解决7.0调用相机报FileUriExposedException的问题
  • 解决小米miui系统调用系统裁剪图片功能crash问题

使用

Step 1. 添加JitPack仓库

在项目的build.gradle添加JitPack仓库

 

  1. allprojects {
  2. repositories {
  3. maven { url “https://jitpack.io” }
  4. }
  5. }

Step 2. 添加依赖

在需要使用的module中添加依赖(*新版本见 PictureSelector)

 

  1. dependencies {
  2. compile ‘com.github.wildma:PictureSelector:1.1.1’
  3. }

或者引用本地lib

 

compile project(':pictureselector')

Step 3. 拍照或者从相册选择图片

 

  1. /**
  2. * create()方法参数一是上下文,在activity中传activity.this,在fragment中传fragment.this。参数二为请求码,用于结果回调onActivityResult中判断
  3. * selectPicture()方法参数分别为 是否裁剪、裁剪后图片的宽(单位px)、裁剪后图片的高、宽比例、高比例。都不传则默认为裁剪,宽200,高200,宽高比例为1:1。
  4. */
  5. PictureSelector
  6. .create(MainActivity.this, PictureSelector.SELECT_REQUEST_CODE)
  7. .selectPicture(true, 200, 200, 1, 1);

Step 4. 获取裁剪后的图片地址

 

  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. super.onActivityResult(requestCode, resultCode, data);
  4. /*结果回调*/
  5. if (requestCode == PictureSelector.SELECT_REQUEST_CODE) {
  6. if (data != null) {
  7. String picturePath = data.getStringExtra(PictureSelector.PICTURE_PATH);
  8. }
  9. }
  10. }

代码

每个类的注释我都写的很清楚了,所以这里只贴出主要的图片工具类,其他可以到我的Github上查看源码(见文末)。

选择图片工具类

 

  1. package com.wildma.pictureselector;
  2. import android.app.Activity;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.graphics.Bitmap;
  6. import android.graphics.BitmapFactory;
  7. import android.net.Uri;
  8. import android.os.Build;
  9. import android.provider.MediaStore;
  10. import android.support.v4.content.FileProvider;
  11. import android.widget.Toast;
  12. import java.io.File;
  13. import java.io.FileNotFoundException;
  14. /**
  15. * Author wildma
  16. * Github https://github.com/wildma
  17. * CreateDate 2018/6/10
  18. * Desc ${选择图片工具类}
  19. * 使用方法:
  20. * 1. 调用getByCamera()、getByAlbum()可通过拍照或相册获取图片
  21. * 2. 在onActivityResult中调用本工具类的onActivityResult方法处理通过相册或拍照获取的图片
  22. */
  23. public class PictureSelectUtils {
  24. public static final int GET_BY_ALBUM = 0x11;//相册标记
  25. public static final int GET_BY_CAMERA = 0x12;//拍照标记
  26. public static final int CROP = 0x13;//裁剪标记
  27. private static Uri takePictureUri;//拍照图片uri
  28. public static Uri cropPictureTempUri;//裁剪图片uri
  29. /**
  30. * 通过相册获取图片
  31. * @param activity
  32. */
  33. public static void getByAlbum(Activity activity) {
  34. Intent intent = new Intent(Intent.ACTION_PICK,
  35. MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
  36. intent.setType(“image/*”);
  37. activity.startActivityForResult(intent, GET_BY_ALBUM);
  38. }
  39. /**
  40. * 通过拍照获取图片
  41. * @param activity
  42. */
  43. public static void getByCamera(Activity activity) {
  44. takePictureUri = createImagePathUri(activity);
  45. if (takePictureUri != null) {
  46. Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
  47. i.putExtra(MediaStore.EXTRA_OUTPUT, takePictureUri);//输出路径(拍照后的保存路径)
  48. activity.startActivityForResult(i, GET_BY_CAMERA);
  49. } else {
  50. Toast.makeText(activity, “无法保存到相册”, Toast.LENGTH_LONG).show();
  51. }
  52. }
  53. /**
  54. * 创建一个图片地址uri,用于保存拍照后的照片
  55. *
  56. * @param activity
  57. * @return 图片的uri
  58. */
  59. public static Uri createImagePathUri(Activity activity) {
  60. try {
  61. FileUtils.createOrExistsDir(Constant.DIR_ROOT);
  62. StringBuffer buffer = new StringBuffer();
  63. String pathName = buffer.append(Constant.DIR_ROOT).append(Constant.APP_NAME).append(“.”).append(System.currentTimeMillis()).append(“.jpg”).toString();
  64. File file = new File(pathName);
  65. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //解决Android 7.0 拍照出现FileUriExposedException的问题
  66. String authority = activity.getPackageName() + “.fileProvider”;
  67. takePictureUri = FileProvider.getUriForFile(activity, authority, file);
  68. } else {
  69. takePictureUri = Uri.fromFile(file);
  70. }
  71. } catch (Exception e) {
  72. e.printStackTrace();
  73. Toast.makeText(activity, “无法保存到相册”, Toast.LENGTH_LONG).show();
  74. }
  75. return takePictureUri;
  76. }
  77. /**
  78. * 处理拍照或相册获取的图片,默认大小480*480,比例1:1
  79. * @param activity 上下文
  80. * @param requestCode 请求码
  81. * @param resultCode 结果码
  82. * @param data Intent
  83. * @return
  84. */
  85. public static Bitmap onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
  86. return onActivityResult(activity, requestCode, resultCode, data, 0, 0, 0, 0);
  87. }
  88. /**
  89. * 处理拍照或相册获取的图片
  90. * @param activity 上下文
  91. * @param requestCode 请求码
  92. * @param resultCode 结果码
  93. * @param data Intent
  94. * @param w 输出宽
  95. * @param h 输出高
  96. * @param aspectX 宽比例
  97. * @param aspectY 高比例
  98. * @return
  99. */
  100. public static Bitmap onActivityResult(Activity activity, int requestCode, int resultCode, Intent data,
  101. int w, int h, int aspectX, int aspectY) {
  102. Bitmap bm = null;
  103. if (resultCode == Activity.RESULT_OK) {
  104. Uri uri = null;
  105. switch (requestCode) {
  106. case GET_BY_ALBUM:
  107. uri = data.getData();
  108. activity.startActivityForResult(crop(uri, w, h, aspectX, aspectY), CROP);
  109. break;
  110. case GET_BY_CAMERA:
  111. uri = takePictureUri;
  112. activity.startActivityForResult(crop(uri, w, h, aspectX, aspectY), CROP);
  113. break;
  114. case CROP:
  115. bm = dealCrop(activity);
  116. break;
  117. }
  118. }
  119. return bm;
  120. }
  121. /**
  122. * 裁剪,默认裁剪输出480*480,比例1:1
  123. * @param uri 图片的uri
  124. * @return
  125. */
  126. public static Intent crop(Uri uri) {
  127. return crop(uri, 480, 480, 1, 1);
  128. }
  129. /**
  130. * 裁剪,例如:输出100*100大小的图片,宽高比例是1:1
  131. * @param uri 图片的uri
  132. * @param w 输出宽
  133. * @param h 输出高
  134. * @param aspectX 宽比例
  135. * @param aspectY 高比例
  136. * @return
  137. */
  138. public static Intent crop(Uri uri, int w, int h, int aspectX, int aspectY) {
  139. if (w == 0 && h == 0) {
  140. w = h = 480;
  141. }
  142. if (aspectX == 0 && aspectY == 0) {
  143. aspectX = aspectY = 1;
  144. }
  145. Intent intent = new Intent(“com.android.camera.action.CROP”);
  146. intent.setDataAndType(uri, “image/*”);
  147. intent.putExtra(“crop”, “true”);
  148. intent.putExtra(“aspectX”, aspectX);
  149. intent.putExtra(“aspectY”, aspectY);
  150. intent.putExtra(“outputX”, w);
  151. intent.putExtra(“outputY”, h);
  152. /*解决图片有黑边问题*/
  153. intent.putExtra(“scale”, true);
  154. intent.putExtra(“scaleUpIfNeeded”, true);
  155. /*解决跳转到裁剪提示“图片加载失败”问题*/
  156. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  157. intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  158. /*解决小米miui系统调用系统裁剪图片功能camera.action.CROP后崩溃或重新打开app的问题*/
  159. StringBuffer buffer = new StringBuffer();
  160. String pathName = buffer.append(“file:///”).append(FileUtils.getRootPath()).append(File.separator).append(Constant.APP_NAME).append(“.temp.jpg”).toString();
  161. cropPictureTempUri = Uri.parse(pathName);
  162. intent.putExtra(MediaStore.EXTRA_OUTPUT, cropPictureTempUri);//输出路径(裁剪后的保存路径)
  163. // 输出格式
  164. intent.putExtra(“outputFormat”, “JPEG”);
  165. // 不启用人脸识别
  166. intent.putExtra(“noFaceDetection”, true);
  167. //是否将数据保留在Bitmap中返回
  168. intent.putExtra(“return-data”, false);
  169. return intent;
  170. }
  171. /**
  172. * 处理裁剪,获取裁剪后的图片
  173. * @param context 上下文
  174. * @return
  175. */
  176. public static Bitmap dealCrop(Context context) {
  177. Bitmap bitmap = null;
  178. try {
  179. bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(cropPictureTempUri));
  180. } catch (FileNotFoundException e) {
  181. e.printStackTrace();
  182. }
  183. return bitmap;
  184. }
  185. }

github地址:PictureSelector

Android简单、灵活、高效的图片裁剪框架 Android-ImageClipper

Android图片裁剪的实现方式
Android图片的裁剪实现方式有两种:

调用系统的图片裁剪App;
调用第三方图片裁剪框架。
在这里我会贴出如何调用系统的图片裁剪App的示例代码;并且给出我自定义的图片裁剪框架的实现原理和Github的连接。

调用系统的图片裁剪App
/**
* @param srcUri 原始图片的Uri
* @param desUri 制定的裁剪后的图片要保存的路径所转成的Uri
*/
private void callSystemImageCropper(Uri srcUri, Uri desUri) {
Intent intent = new Intent(“com.android.camera.action.CROP”);
intent.setDataAndType(srcUri, “image/*”);
intent.putExtra(“crop”, “true”);
intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
intent.putExtra(“aspectX”, 1);
intent.putExtra(“aspectY”, 1);
intent.putExtra(“outputFormat”, Bitmap.CompressFormat.JPEG);
intent.putExtra(“outputX”, 400);
intent.putExtra(“outputY”, 400);
intent.putExtra(“scale”, true);
intent.putExtra(“scaleUpIfNeeded”, true);
intent.putExtra(“return-data”, false);
startActivityForResult(intent, 222);
}

等待系统裁剪完图片后,你就可以使用desUri(或者desUri对应的*对路径)对裁剪后的图片进行操作了。

这种方式虽然简单,但是对图片只能进行 正方形 裁剪,所以略有缺憾。

常用的第三方裁剪框架
在github上有很多第三方裁剪框架,但是鄙人从来没有成功同步过依赖(鄙人的家庭网络很渣),并且鄙人一旦使用第三方的东西不爽的时候,就会很容易萌生自己造轮子的想法。

这篇文章列出了常用的第三方图片裁剪框架:
Android开发常用开源框架:图片处理

我的图片裁剪框架 Android-ImageClipper
Android-ImageClipper是鄙人实现的一个简单、灵活、高效的图片裁剪框架,你既可以使用写好的Activity,也可以使用ImageClipView来自定义裁剪功能。

实现思路
将原始图片的Bitmap传入ImageClipView,ImageClipView根据所在的父容器的宽高和原始图片的Bitmap的宽高来计算出自身的宽高,以此来保证按照图片的比例正确现实图片。
绘制裁剪框,根据裁剪框的布局参数来计算出在原始图片中进行裁剪的参数,*后根据裁剪参数进行裁剪。
功能设计
矩形裁剪功能,可以自由控制矩形裁剪框的大小和其他参数,实现自由的矩形裁剪。该功能已经实现。
圆形裁剪功能,可以自由缩放圆形裁剪框的大小和位置,实现自由的圆形裁剪。该功能未实现。
椭圆裁剪功能,可以自由缩放椭圆裁剪框的大小和位置,实现自由的椭圆裁剪。该功能未实现。

Android开发技巧-Camera拍照功能

本篇是我对开发项目的拍照功能过程中,对Camera拍照使用的总结。由于camera2是在api level 21(5.0.1)才引入的,而Camera到6.0仍可使用,所以暂未考虑camera2。

文档中的Camera
要使用Camera,首先我们先看一下文档(http://androiddoc.qiniudn.com/reference/android/hardware/Camera.html)中是怎么介绍的。相对于其他*大多数类,文档对Camera的介绍还是比较详尽的,包含了使用过程中所需要的步骤说明,当然,这也表明了它在实际使用中的繁琐。
首先,需要在AndroidManifest.xml中声明以下权限和特性:

<uses-permission android:name=”android.permission.CAMERA” />
<uses-feature android:name=”android.hardware.camera” />
<uses-feature android:name=”android.hardware.camera.autofocus” />

然后,拍照的话,需要以下十步:
1. 通过open(int)方法得到一个实例
2. 通过getParameters()方法得到默认的设置
3. 如果有必要,修改上面所返回的Camera.Parameters对象,并调用setParameters(Camera.Parameters) 进行设置
4. 如果有需要,调用setDisplayOrientation(int)设置显示的方向
5. 这一步很重要,通过setPreviewDisplay(SurfaceHolder)传入一个已经初始化了的SurfaceHolder,否则无法进行预览。
6. 这一步也很重要,通过startPreview()开始更新你的预览界面,在你拍照之前,它必须开始。
7. 调用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)进行拍照,等待它的回调
8. 拍照之后,预览的展示会停止。如果想继续拍照,需要先再调用startPreview()。
9. 调用stopPreview()停止预览。
10. 非常重要,调用release()释放Camera,以使其他应用也能够使用相机。你的应用应该在onPause()被调用时就进行释放,在onResume()时再重新open()。

上面就是文档中关于使用Camera进行拍照的介绍了。接下来说一下我的使用场景。

我的使用场景

这是项目的界面需求。下面一个圆的拍照按钮,然后是一个取消按钮,上面是预览界面(SurfaceView)加个取景框。再上面就是一块黑的了。点拍照,拍照之后,跳到一个裁剪图片的界面,所以不会有连续拍多次照片的场景。
取景框什么的这里略过不谈,布局文件也相对比较简单,下面直接看Java代码里对Camera的使用。

实际使用及填坑
SurfaceHolder的回调
我在Activity中实现SurfaceHolder.Callback接口。然后在onCreate(Bundle)方法中,添加SurfaceHolder的回调。

SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(this);

它的回调方法有3个,分别是surface被创建时的回调surfaceCreated(SurfaceHolder),surface被销毁时的回调surfaceDestroyed(SurfaceHolder)以及surface改变时的回调surfaceChanged(SurfaceHolder holder, int, int, int)。这里我们只关注创建和销毁时的回调,定义一个变量用于标志它的状态。

private boolean mIsSurfaceReady;

@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsSurfaceReady = true;
startPreview();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsSurfaceReady = false;
}

其中的startPreview()方法将在下面讲到。

打开相机
然后是打开相机。这些代码在我定义的openCamera方法中。

if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (RuntimeException e) {
if (“Fail to connect to camera service”.equals(e.getMessage())) {
//提示无法打开相机,请检查是否已经开启权限
} else if (“Camera initialization failed”.equals(e.getMessage())) {
//提示相机初始化失败,无法打开
} else {
//提示相机发生未知错误,无法打开
}
finish();
return;
}
}

打开相机失败的话,我们无法进行下一步操作,所以在提示之后会直接把界面关掉。

拍照参数
final Camera.Parameters cameraParams = mCamera.getParameters();
cameraParams.setPictureFormat(ImageFormat.JPEG);
cameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

分别设置图片格式,以及对焦模式。然后因为我这里是竖屏拍照,所以还需要对Camera旋转90度。

cameraParams.setRotation(90);

注意:涉及到旋转的有两个方法,一个是旋转相机,一个是旋转预览。这里设置的是对相机的旋转。
继续注意:由于机型兼容的问题,这里设置旋转之后,有些手机照片来的照片就是竖的了,但是有些手机(比如万恶的三星)拍出来的照片还是横的,但是它们在照片的Exif信息中有相关的角度属性。所以对于拍出来的照片还是横着的,我们在裁剪时再继续处理。关于照片的旋转处理,后续博客中会讲到。

尺寸参数
这里还是Camera的参数设置,但是我把它单独抽出来是因为,它不像上面设置的参数那样简单直接,而需要进行计算。下面是我们需要注意的问题:

首先,相机的宽高比例主要有两种,一种是16:9,一种是4:3。
其次,我们需要SurfaceView的比例与Camera预览尺寸的比例一样,才不会导致预览出来的结果是变形的。
由于机型分辨率的问题,再加上我们的SurfaceView不是满屏的(即使满屏,还要考虑一些虚拟导航栏和各种奇葩分辨率的机型),16:9的比例我们需要上是不会用到的了,我们会让Camera预览的尺寸比例与SurfaceView的大小比例一样。
要特别注意,一些手机,如果设置预览的大小与设置的图片大小相差太大(但宽高比例相同)的话,拍出来的照片可能范围也不一样。比如你拍的时候明明是一幅画包括画框,保存的图片却只有画框里的内容。
下面的代码还是写在我们的openCamera()方法中。由于我们需要能够获取到SurfaceView的大小,所以openCamera()是这样调用的:

@Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
}

它可以保证在openCamera()被调用时surfaceView一定是绘制完成了的。
然后在openCamera()的后续代码中,先获取surfaceView的宽高比例。注意,对于surfaceView我开始在布局上写的是高度占满剩下的空间。

<SurfaceView
android:id=”@+id/surface_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_above=”@id/bottom”/>

这时候得到的宽高比就是我们所能接受的*小比例了。

// 短边比长边
final float ratio = (float) mSurfaceView.getWidth() / mSurfaceView.getHeight();

然后获取相机支持的图片尺寸,找出*适合的尺寸。

// 设置pictureSize
List<Camera.Size> pictureSizes = cameraParams.getSupportedPictureSizes();
if (mBestPictureSize == null) {
mBestPictureSize =findBestPictureSize(pictureSizes, cameraParams.getPictureSize(), ratio);
}
cameraParams.setPictureSize(mBestPictureSize.width, mBestPictureSize.height);

findBestPictureSize的代码如下。注意,因为我们是旋转了相机的,所以计算的时候,对surfaceView的比例是宽除以高,而对Camera.Size则是高除以宽。

/**
* 找到短边比长边大于于所接受的*小比例的*大尺寸
*
* @param sizes 支持的尺寸列表
* @param defaultSize 默认大小
* @param minRatio 相机图片短边比长边所接受的*小比例
* @return 返回计算之后的尺寸
*/
private Camera.Size findBestPictureSize(List<Camera.Size> sizes, Camera.Size defaultSize, float minRatio) {
final int MIN_PIXELS = 320 * 480;

sortSizes(sizes);

Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
//移除不满足比例的尺寸
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
}
//移除太小的尺寸
if (size.width * size.height < MIN_PIXELS) {
it.remove();
}
}

// 返回符合条件中*大尺寸的一个
if (!sizes.isEmpty()) {
return sizes.get(0);
}
// 没得选,默认吧
return defaultSize;
}

接下来是设置预览图片的尺寸:

// 设置previewSize
List<Camera.Size> previewSizes = cameraParams.getSupportedPreviewSizes();
if (mBestPreviewSize == null) {
mBestPreviewSize = findBestPreviewSize(previewSizes, cameraParams.getPreviewSize(),
mBestPictureSize, ratio);
}
cameraParams.setPreviewSize(mBestPreviewSize.width, mBestPreviewSize.height);

根据图片尺寸,以及SurfaceView的比例来计算preview的尺寸。

/**
* @param sizes
* @param defaultSize
* @param pictureSize 图片的大小
* @param minRatio preview短边比长边所接受的*小比例
* @return
*/
private Camera.Size findBestPreviewSize(List<Camera.Size> sizes, Camera.Size defaultSize,
Camera.Size pictureSize, float minRatio) {
final int pictureWidth = pictureSize.width;
final int pictureHeight = pictureSize.height;
boolean isBestSize = (pictureHeight / (float)pictureWidth) > minRatio;
sortSizes(sizes);

Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
}

// 找到同样的比例,直接返回
if (isBestSize && size.width * pictureHeight == size.height * pictureWidth) {
return size;
}
}

// 未找到同样的比例的,返回尺寸*大的
if (!sizes.isEmpty()) {
return sizes.get(0);
}

// 没得选,默认吧
return defaultSize;
}

上面的两个findBestxxx方法,可以自己根据业务需要进行调整。整体思路就是先对尺寸排序,然后遍历排除掉不满足条件的尺寸,如果找到比例一样的,则直接返回。如果遍历完了仍没找到,则返回*大的尺寸,如果发现都排除完了,只能返回默认的那一个了。
然后,我们还要再根据previewSize来重新设置我们的surfaceView的大小,以使它们的比例完全一样,才不会导致预览时变形。

ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();
params.height = mSurfaceView.getWidth() * mBestPreviewSize.width / mBestPreviewSize.height;
mSurfaceView.setLayoutParams(params);

再下来就是把参数设置过去:

mCamera.setParameters(cameraParams);

然后预览。

预览
由于相机打开会需要一些时间,而surfaceHolder的回调也需要一些时间。我希望的是当相机准备完成可以回调并且surface也创建完毕的时候,就可以马上预览(尽量减小进入界面后可能会有的黑一下的时间),所以这里我的代码如下:

if (mIsSurfaceReady) {
startPreview();
}

同时在surface被创建的时候,也会调用一下这个startPreview()方法。
startPreview()代码如下,在camera初始化之后,首先设置SurfaceHolder对象,然后对预览旋转90度,然后开始预览。

private void startPreview() {
if (mCamera == null) {
return;
}
try {
mCamera.setPreviewDisplay(mSurfaceView.getHolder());
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
BugReport.report(e);
}
}

自动对焦
我希望在点击预览图的时候能够进行自动对焦。由于在界面上我在surfaceview之上放了一个取景框View,所以我直接对这个View设置一个点击事件,进行触发自动对焦。
自动对焦的代码如下:

/**
* 请求自动对焦
*/
private void requestFocus() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mCamera.autoFocus(null);
}

这里我只需要相机能够对焦,并不是要在对焦成功之后才进行拍照,所以回调我传了一个null。
之所以这样使用是因为,之前我写的是对焦成功之后才拍照,但是会有两个问题:一是对焦会有一个过程,这样对完焦之后才拍照会慢,二是可能在点拍照的时候预览的界面正是我们想要的,但是一对焦,可能对焦失败,导致没有拍照或者是拍出来的是模糊的。

拍照
拍照也是异步回调,并且会需要点时间,所以这里我定义了一个mWaitForTakePhoto变量,表示正在拍照,还没完成。在拍照的过程中,不允许重新对焦或重新拍照。

private void takePhoto() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mWaitForTakePhoto = true;
mCamera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
onTakePhoto(data);
mWaitForTakePhoto = false;
}
});
}

保存照片。这里返回的data可以直接写入文件,就是一张jpg图了。

private void onTakePhoto(byte[] data) {
final String tempPath = mOutput + “_”;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tempPath);
fos.write(data);
fos.flush();
//启动我的裁剪界面
} catch (Exception e) {
BugReport.report(e);
} finally {
IOUtils.close(fos);
}
}

相机的打开与关闭以及Activity的生命周期
@Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
}

@Override
protected void onPause() {
super.onPause();
closeCamera();
}

关闭相机时,首先要取消掉自动对焦,否则如果正好在自动对焦,又关掉相机,会引发异常。接着停止preview,然后再释放:

private void closeCamera() {
if (mCamera == null) {
return;
}
mCamera.cancelAutoFocus();
stopPreview();
mCamera.release();
mCamera = null;
}

总结
1,该类的全部代码见:https://gist.github.com/msdx/f8ca0fabf0092f67d829 。没有Demo项目,没有Demo项目,没有Demo项目。如果你打开不了该链接,请先确认是否能打开github。如果打开不了github,请科学上网
2,文档很重要。
3,我不保证我的代码完全没问题,至少我现在没发现。如果有出现什么问题,欢迎提出。
4,注意相机打开和释放。
5,注意不同机型的相机旋转设置。特别是三星。
6,尺寸计算,previewSize的比例一定要和surfaceView可以显示的比例一样,才不会变形。

Android开发技巧-大图裁剪

本篇内容是接上篇《Android开发技巧-定制仿微信图片裁剪控件》 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪。

裁剪控件的简单使用
XML代码
使用如普通控件一样,首先在布局文件里包含该控件:

<com.githang.clipimage.ClipImageView
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:id=”@+id/clip_image_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_above=”@+id/bottom”
app:civClipPadding=”@dimen/padding_common”
app:civHeight=”2″
app:civMaskColor=”@color/viewfinder_mask”
app:civWidth=”3″/>

支持的属性如下:

civHeight 高度比例,默认为1
civWidth 宽度比例,默认为1
civTipText 裁剪的提示文字
civTipTextSize 裁剪的提示文字的大小
civMaskColor 遮罩层颜色
civClipPadding 裁剪框边距
Java代码
如果裁剪的图片不大,可以直接设置,就像使用ImageView一样,通过如下四种方法设置图片:

mClipImageView.setImageURI(Uri.fromFile(new File(mInput)));
mClipImageView.setImageBitmap(bitmap);
mClipImageView.setImageResource(R.drawable.xxxx);
mClipImageView.setImageDrawable(drawable);

裁剪的时候调用mClipImageView.clip();就可以返回裁剪之后的Bitmap对象。

大图裁剪
这里会把大图裁剪及图片文件可能旋转的情况一起处理。
注意:由于裁剪图片*终还是需要把裁剪结果以Bitmap对象加载到内存中,所以裁剪之后的图片也是会有大小限制的,否则会有OOM的情况。所以,下面会设一个裁剪后的*大宽度的值。

读取图片旋转角度
在*篇《 Android开发技巧——Camera拍照功能 》的时候,有提到过像三星的手机,竖屏拍出来的照片还是横的,但是有Exif信息记录了它的旋转方向。考虑到我们进行裁剪的时候,也会遇到类似这样的照片,所以对于这种照片需要旋转的情况,我选择了在裁剪的时候才进行处理。所以首先,我们需要读到图片的旋转角度:

/**
* 读取图片属性:旋转的角度
*
* @param path 图片*对路径
* @return degree旋转的角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}

如果你能确保要裁剪的图片不大不会导致OOM的情况发生的话,是可以直接通过这个角度,创建一个Matrix对象,进行postRotate,然后由原图创建一个新的Bitmap来得到一个正确朝向的图片的。但是这里考虑到我们要裁剪的图片是从手机里读取的,有可能有大图,而我们的裁剪控件本身只实现了简单的手势缩放和裁剪功能,并没有实现大图加载的功能,所以需要在设置图片进行之前进行一些预处理。

采样缩放
由于图片较大,而我们又需要把整张图都加载进来而不是只加载局部,所以就需要在加载的时候进行采样,来加载缩小之后的图片,这样加载到的图片较小,就能有效避免OOM了。
以前文提到的裁剪证件照为例,这里仍以宽度为参考值来计算采样值,具体是用宽还是高或者是综合宽高(这种情况较多,考虑到可能会有很长的图)来计算采样值,还得看你具体情况。在计算采样的时候,我们还需要用到上面读到的旋转值,在图片被旋转90度或180度时,进行宽和高的置换。所以,除了相关的控件,我们需要定义如下相关的变量:

private String mOutput;
private String mInput;
private int mMaxWidth;

// 图片被旋转的角度
private int mDegree;
// 大图被设置之前的采样比例
private int mSampleSize;
private int mSourceWidth;
private int mSourceHeight;

计算采样代码如下:

mClipImageView.post(new Runnable() {
@Override
public void run() {
mClipImageView.setMaxOutputWidth(mMaxWidth);

mDegree = readPictureDegree(mInput);

final boolean isRotate = (mDegree == 90 || mDegree == 270);

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mInput, options);

mSourceWidth = options.outWidth;
mSourceHeight = options.outHeight;

// 如果图片被旋转,则宽高度置换
int w = isRotate ? options.outHeight : options.outWidth;

// 裁剪是宽高比例3:2,只考虑宽度情况,这里按border宽度的两倍来计算缩放。
mSampleSize = findBestSample(w, mClipImageView.getClipBorder().width());
//代码未完,将下面的[缩放及设置]里分段讲到。
}
});

由于我们是需要裁剪控件的裁剪框来计算采样,所以需要获取裁剪框,因此我们把上面的代码通过控件的post方法来调用。
inJustDecodeBounds在许多讲大图缩放的博客都有讲到,相信很多朋友都清楚,本文就不赘述了。
注意:采样的值是2的幂次方的,如果你传的值不是2的幂次方,它在计算的时候*终会往下找到*近的2的幂次方的值。所以,如果你后面还需要用这个值来进行计算,就不要使用网上的一些直接用两个值相除进行计算sampleSize的方法。精确的计算方式应该是直接计算时这个2的幂次方的值,例如下面代码:

/**
* 计算*好的采样大小。
* @param origin 当前宽度
* @param target 限定宽度
* @return sampleSize
*/
private static int findBestSample(int origin, int target) {
int sample = 1;
for (int out = origin / 2; out > target; out /= 2) {
sample *= 2;
}
return sample;
}

缩放及设置
接下来就是设置inJustDecodeBounds,inSampleSize,以及把inPreferredConfig设置为RGB_565,然后把图片给加载进来,如下:

options.inJustDecodeBounds = false;
options.inSampleSize = mSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
final Bitmap source = BitmapFactory.decodeFile(mInput, options);

这里加载的图片还是没有旋转到正确朝向的,所以我们要根据上面所计算的角度,对图片进行旋转。我们竖屏拍的图,在一些手机上是横着保存的,但是它会记录一个旋转90度的值在Exif中。如下图中,左边是保存的图,它依然是横着的,右边是我们显示时的图。所以我们读取到这个值后,需要对它进行顺时针的旋转。

代码如下:

// 解决图片被旋转的问题
Bitmap target;
if (mDegree == 0) {
target = source;
} else {
final Matrix matrix = new Matrix();
matrix.postRotate(mDegree);
target = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);
if (target != source && !source.isRecycled()) {
source.recycle();
}
}
mClipImageView.setImageBitmap(target);

这里需要补充的一个注意点是:Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);这个方法返回的Bitmap不一定是重新创建的,如果matrix相同并且宽高相同,而且你没有对Bitmap进行其他设置的话,它可能会返回原来的对象。所以在创建新的Bitmap之后,回收原来的Bitmap时要判断是否可以回收,否则可能导致创建出来的target对象被回收而使ImageView的图片无法显示出来。
如上,就是完整的设置大图时的处理过程的代码。

裁剪
裁剪时需要创建一个裁剪之后的Bitmap,再把它保存下来。下面介绍一下这个创建过程。完整代码如下:

private Bitmap createClippedBitmap() {
if (mSampleSize <= 1) {
return mClipImageView.clip();
}

// 获取缩放位移后的矩阵值
final float[] matrixValues = mClipImageView.getClipMatrixValues();
final float scale = matrixValues[Matrix.MSCALE_X];
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;

// 获取在旋转之前的裁剪位置
final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
final Rect clipRect = getRealRect(srcRect);

final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();

outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);

final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}

// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}
}

下面分段介绍。

计算在采样缩小前的裁剪框
首先,如果采样值不大于1,也就是我们没有进行图片缩小的时候,就不需要进行下面的计算了,直接调用我们的裁剪控件返回裁剪后的图片即可。否则,就是我们对图片进行缩放的情况了,所以会需要综合我们的采样值mSampleSize,计算我们的裁剪框实际上在原图上的位置。所以会看到相对于上篇所讲的裁剪控件对裁剪框的计算,这里多乘了一个mSampleSize的值,如下:

// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;

然后我们创建这个在原图大小时的裁剪框:

final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);

计算在图片旋转前的裁剪框
对于大图的裁剪,我们可以使用BitmapRegionDecoder类,来只加载图片的一部分,也就是用它来加载我们所需要裁剪的那一部分,但是它是从旋转之前的原图进行裁剪的,所以还需要对这个裁剪框进行反向的旋转,来计算它在原图上的位置。
如下图所示,ABCD是旋转90度之后的图片,EFGH是我们的裁剪框。

但是在原图中,它们的对应位置如下图所示:

也就是B点成了A点,A点成了D点,等等。
所以我们获取EFGH在ABCD中的位置,也不能像裁剪控件那样,而需要进行反转之后的计算。以旋转90度为例,现在我们的左上角变成了F点,那么它的left就是原来的top,它的top就是图片的高度减去原来的right,它的right就是原来的bottom,它的bottom就是图片的高度减去原来的left,完整代码如下:

private Rect getRealRect(RectF srcRect) {
switch (mDegree) {
case 90:
return new Rect((int) srcRect.top, (int) (mSourceHeight – srcRect.right),
(int) srcRect.bottom, (int) (mSourceHeight – srcRect.left));
case 180:
return new Rect((int) (mSourceWidth – srcRect.right), (int) (mSourceHeight – srcRect.bottom),
(int) (mSourceWidth – srcRect.left), (int) (mSourceHeight – srcRect.top));
case 270:
return new Rect((int) (mSourceWidth – srcRect.bottom), (int) srcRect.left,
(int) (mSourceWidth – srcRect.top), (int) srcRect.right);
default:
return new Rect((int) srcRect.left, (int) srcRect.top, (int) srcRect.right, (int) srcRect.bottom);
}
}

所以在原图上的真正的裁剪框位置是:
final Rect clipRect = getRealRect(srcRect);

局部加载所裁剪的图片部分
大图裁剪,我们使用BitmapRegionDecoder类,它可以只加载指定的某一部分的图片内容,通过它的public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)方法,我们可以把所裁剪的内容加载出来,得到一个Bitmap,这个Bitmap就是我们要裁剪的内容了。但是,我们加载的这部分内容,同样可能太宽,所以还可能需要进行采样缩小。如下:

final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();//用于*图图片的精确缩放

outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);

final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}

计算出采样值sampleSize之后,再使用它及我们计算的裁剪框,加载所裁剪的内容:

// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}

总结
完整代码见github上我的clip-image项目的示例ClipImageActivity.java。
上面例子中,我所用的图片并不大,下面我打包了一个大图的apk,它使用了维基百科上的一张世界地图,下载地址如下:http://download.csdn.net/detail/maosidiaoxian/9464200

上面的例子截图:

可以看出,在这个例子中,虽然在裁剪过程当中图片被缩放过所以不太清晰,但是我们真正的裁剪是对原图进行裁剪再进行适当的缩放的,所以裁剪之后的图片更清晰。

Android开发技巧-定制仿微信图片裁剪控件

拍照——裁剪,或者是选择图片——裁剪,是我们设置头像或上传图片时经常需要的一组操作。上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现。

背景
下面的需求都来自产品。
裁剪图片要像微信那样,拖动和放大的是图片,裁剪框不动。
裁剪框外的内容要有半透明黑色遮罩。
裁剪框下面要显示一行提示文字(这点我至今还是持保留意见的)。
在Android中,裁剪图片的控件库还是挺多的,特别是github上比较流行的几个,都已经进化到比较稳定的阶段,但比较遗憾的是它们的裁剪过程是拖动或缩放裁剪框,于是只好自己再找,看有没有现成或半成品的轮子,可以不必从零开始。
踏破铁鞋无觅处,*天不负苦心人。我终于找到了两篇博客:《Android高仿微信头像裁剪》和《Android 高仿微信头像截取 打造不一样的自定义控件》,以及csdn上找到的前面博客所对应的一份代码,并*终实现了自己的裁剪控件。

大神的实现过程
首先先了解一下上面的高仿微信裁剪控件的实现过程。说起来也不难,主要是下面几点:
1,重写ImageView,并监听手势事件,包括双点,两点缩放,拖动,使它成为一个实现缩放拖动图片功能的控件。
2,定义一个Matrix成员变量,对于维护该图片的缩放、平移等矩阵数据。
3,拖动或缩放时,图片与裁剪框的相交面积一定与裁剪框相等。即图片不能拖离裁剪框。
3,在设置图片时,先根据图片的大小进行初始化的缩放平移操作,使得上面第三条的条件下图片尽可能的小。
4,每次接收到相对应的手势事件,都进行对应的矩阵计算,并将计算结果通过ImageView的setImageMatrix方法应用到图片上。
5,裁剪框是一个单独的控件,与ImageView同样大,叠加到它上面显示出来。
6,用一个XXXLayout把裁剪框和缩放封装起来。
7,裁剪时,先创建一个空的Bitmap并用其创建一个Canvas,把缩放平移后的图片画到这个Bitmap上,并创建在裁剪框内的Bitmap(通过调用Bitmap.createBitmap方法)。

我的定制内容
我拿到的代码是鸿洋大神版本之后再被改动的,代码上有点乱(虽然功能上是实现的裁剪)。在原有的功能上,我希望进行的改动有:

合并裁剪框的内容到ImageView中
裁剪框可以是任意长宽比的矩形
裁剪框的左右外边距可以设置
遮罩层颜色可以设置
裁剪框下有提示文字(自己的产品需求)
后面产品又加入了一条裁剪图片的*大大小
属性定义
在上面的功能需求中,我定义了以下属性:

<declare-styleable name=”ClipImageView”>
<attr name=”civHeight” format=”integer”/>
<attr name=”civWidth” format=”integer”/>
<attr name=”civTipText” format=”string”/>
<attr name=”civTipTextSize” format=”dimension”/>
<attr name=”civMaskColor” format=”color”/>
<attr name=”civClipPadding” format=”dimension”/>
</declare-styleable>

其中:

civHeight和civWidth是裁剪框的宽高比例。
civTipText提示文字的内容
civTipTextSize提示文字的大小
civMaskColor遮罩层的颜色值
civClipPadding裁剪内边距。由于裁剪框是在控件内部的,*终我选择使用padding来说明裁剪框与我们控件边缘的距离。
成员变量
成员变量我进行了一些改动,把原本用于定义裁剪框的水平边距变量及其他没什么用的变量等给去掉了,并加入了自己的一些成员变量,*终如下:

private final int mMaskColor;//遮罩层颜色

private final Paint mPaint;//画笔
private final int mWidth;//裁剪框宽的大小(从属性上读到的整型值)
private final int mHeight;//裁剪框高的大小(同上)
private final String mTipText;//提示文字
private final int mClipPadding;//裁剪框相对于控件的内边距

private float mScaleMax = 4.0f;//图片*大缩放大小
private float mScaleMin = 2.0f;//图片*小缩放大小

/**
* 初始化时的缩放比例
*/
private float mInitScale = 1.0f;

/**
* 用于存放矩阵
*/
private final float[] mMatrixValues = new float[9];

/**
* 缩放的手势检查
*/
private ScaleGestureDetector mScaleGestureDetector = null;
private final Matrix mScaleMatrix = new Matrix();

/**
* 用于双击
*/
private GestureDetector mGestureDetector;
private boolean isAutoScale;

private float mLastX;
private float mLastY;

private boolean isCanDrag;
private int lastPointerCount;

private Rect mClipBorder = new Rect();//裁剪框
private int mMaxOutputWidth = 0;//裁剪后的图片的*大输出宽度

构造方法
构造方法里主要是多了一些我们自定义属性的读取:

public ClipImageView(Context context) {
this(context, null);
}

public ClipImageView(Context context, AttributeSet attrs) {
super(context, attrs);

setScaleType(ScaleType.MATRIX);
mGestureDetector = new GestureDetector(context,
new SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (isAutoScale)
return true;

float x = e.getX();
float y = e.getY();
if (getScale() < mScaleMin) {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);
} else {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
}
isAutoScale = true;

return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(this);

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);
mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);
mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);
mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);
mTipText = ta.getString(R.styleable.ClipImageView_civTipText);
mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);
final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);
mPaint.setTextSize(textSize);
ta.recycle();

mPaint.setDither(true);
}

定义裁剪框
裁剪框的位置
裁剪框是在控件正中间的,首先我们从属性中读取到的是宽高的比例,以及左右边距,但是在构造方法中,由于控件还没有绘制出来,无法获取到控件的宽高,所以并不能计算裁剪框的大小和位置。所以我重写了onLayout方法,在这里计算裁剪框的位置:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
final int width = getWidth();
final int height = getHeight();
mClipBorder.left = mClipPadding;
mClipBorder.right = width – mClipPadding;
final int borderHeight = mClipBorder.width() * mHeight / mWidth;
mClipBorder.top = (height – borderHeight) / 2;
mClipBorder.bottom = mClipBorder.top + borderHeight;
}

绘制裁剪框
这里我顺便把绘制提示文字的代码也一并给出,都是在同一个方法里的。很简单,重写onDraw方法即可。绘制裁剪框有两种方法,一是绘制一个满屏的遮罩层,然后从中间抠出一个长方形出来,但是我用的时候发现抠不出来,所以我采用的是下面这一种:

先画上下两个矩形,再画左右两个矩形,中间所围起来的没有画的部分就是我们的裁剪框。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();

mPaint.setColor(mMaskColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, width, mClipBorder.top, mPaint);
canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint);
canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint);
canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint);

mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint);

if (mTipText != null) {
final float textWidth = mPaint.measureText(mTipText);
final float startX = (width – textWidth) / 2;
final Paint.FontMetrics fm = mPaint.getFontMetrics();
final float startY = mClipBorder.bottom + mClipBorder.top / 2 – (fm.descent – fm.ascent) / 2;
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mTipText, startX, startY, mPaint);
}
}

修改图片的初始显示
这里我不使用全局布局的监听(通过getViewTreeObserver加入回调),而是直接重写几个设置图片的方法,在设置图片后进行初始显示的设置:

@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
postResetImageMatrix();
}

@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
postResetImageMatrix();
}

@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
postResetImageMatrix();
}

private void postResetImageMatrix() {
post(new Runnable() {
@Override
public void run() {
resetImageMatrix();
}
});
}

resetImageMatrix()方法设置图片的初始缩放及平移,参考图片大小,控件本身大小,以及裁剪框的大小进行计算:

/**
* 垂直方向与View的边矩
*/
public void resetImageMatrix() {
final Drawable d = getDrawable();
if (d == null) {
return;
}

final int dWidth = d.getIntrinsicWidth();
final int dHeight = d.getIntrinsicHeight();

final int cWidth = mClipBorder.width();
final int cHeight = mClipBorder.height();

final int vWidth = getWidth();
final int vHeight = getHeight();

final float scale;
final float dx;
final float dy;

if (dWidth * cHeight > cWidth * dHeight) {
scale = cHeight / (float) dHeight;
} else {
scale = cWidth / (float) dWidth;
}

dx = (vWidth – dWidth * scale) * 0.5f;
dy = (vHeight – dHeight * scale) * 0.5f;

mScaleMatrix.setScale(scale, scale);
mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));

setImageMatrix(mScaleMatrix);

mInitScale = scale;
mScaleMin = mInitScale * 2;
mScaleMax = mInitScale * 4;
}

注意:这里有一个坑。把一个Bitmap设置到ImageView中,显示时要计算的是ImageView获取的Drawable对象以及这个对象的宽高,而不是Bitmap对象。Drawable对象可能由于对Bitmap的放大或缩小显示,导致它的宽或高与Bitmap的宽高不同。
还有一点小注意:获取控件宽高是要在控件被绘制出来之后才能获取得到的,所以上面我通过post一个Runnable对象到主线程的Looper中,保证它是在界面绘制完成之后被调用。

缩放及拖动
缩放及拖动时都需求判断是否超出边界,如果超出,则取允许的*终值。这里的代码我没怎么动,稍后可直接参考源码,暂不赘述。

裁剪
这里是另外一个改造的重点了。
首先,鸿洋大神是通过创建一个空的Bitmap,并根据它创建出一个Canvas对象,然后通过draw方法把缩放后的图片给绘制到这个Bitmap中,再调用Bitmap.createBitmap得到属于裁剪框的内容。但是我们已经重写了onDraw方法画出裁剪框,所以这里就不考虑了。
另外,这种方法还有一个问题:它绘制的是Drawable对象。如果我们设置进去的是一个比较大的Bitmap,那么就可能被缩放了,这里裁剪的是缩放后的Bitmap,也就是它不是对原图进行裁剪的。
这里我参考了其他裁剪图片库,通过保存了缩放平移的Matrix成员变量进行计算,获取出裁剪框在其的对应范围,并根据*终所需(我们产品要限制一个*大大小),得到*终的图片,代码如下:

public Bitmap clip() {
final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;

Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}

return Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);
}

由于我们是对Bitmap进行裁剪,所以首先获取这个Bitmap:

final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

然后,我们的矩阵值可以通过一个包含9个元素的float数组读出:

final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);

比如,读X上的缩放值,代码为matrixValues[Matrix.MSCALE_X]。
要特别注意一点,在前文也有提到,这里缩放的是Drawable对象,但是我们裁剪时用的Bitmap,如果图片太大的话是可能在Drawable上进行缩放的,所以缩放大小的计算应该为:

final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();

然后获取图片平移量:

final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

计算裁剪框对应在图片上的起点及宽高:

final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;

上面就是我们所要裁剪出来的*终结果。
但是,我前面也说的,应产品需求,要限制*大输出大小。由于我们裁剪出来的图片宽高比是3:2,我这里只取宽度(你要取高度也可以)进行限制,所以又加上了如下代码,当裁剪出来的宽度超出我们*大宽度时,进行缩放。

Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}

*终根据上面计算出来的值,创建裁剪出来的Bitmap:

Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);

这样,图片裁剪控件就算全部完成。

实现效果

Android大图片裁剪终*解决方案

约几个月前,我正为公司的APP在Android手机上实现拍照截图而烦恼不已。

上网搜索,确实有不少的例子,大多都是抄来抄去,而且水平多半处于demo的样子,可以用来讲解知识点,但是一碰到实际项目,就漏洞百出。

当时我用大众化的解决方案,暂时性的做了一个拍照截图的功能,似乎看起来很不错。问题随之而来,我用的是小米手机,在别的手机上都运行正常,小米这里却总是碰钉子。虽然我是个理性的米粉,但是也暗地里把小米的工程师问候了个遍。真是惭愧!

翻文档也找不出个答案来,我一直对com.android.camera.action.CROP持有大大的疑问,它是从哪里来,它能干什么,它接收处理什么类型的数据?Google对此却讳莫如深,在官方文档中只有Intent中有只言片语言及,却不甚详尽。

随着项目的驱动,我不能抱着不了解原理就不往前走的心态,唯一要做的,是解决问题。*后在德问上找到一条解决方案,说是哪怕是大米也没问题。当时乐呵呵将代码改了改,确实在所有的手机上跑起来了,一时如释重负,对这个的疑问也抛诸脑后了。

直到月前,BOSS要求将拍照上传到服务器的图片分辨率加倍。OK,加倍简单,增加outputX以及outputY不就得了?

1
intent.putExtra(“outputX”, outputX);
2
intent.putExtra(“outputY”, outputY);
这一增加,吓了我一跳。BOSS的手机拍到的照片几乎就是个缩略图,但是被我问候了全体工程师的小米在这个时候就体现出国产神机的范儿了,小米上的尺寸一切正常。这个为什么呢?我大致了解原因,却不知道如何解决。

在Android中,Intent触发Camera程序,拍好照片后,将会返回数据,但是考虑到内存问题,Camera不会将全尺寸的图像返回给调用的Activity,一般情况下,有可能返回的是缩略图,比如120*160px。

这是为什么呢?这不是一个Bug,而是经过精心设计的,却对开发者不透明。

以我的小米手机为例,摄像头800W像素,根据我目前设置拍出来的图片尺寸为3200*2400px。有人说,那就返回呗,大不了耗1-2M的内存,不错,这个尺寸的图片确实只有1.8M左右的大小。但是你想不到的是,这个尺寸对应的Bitmap会耗光你应用程序的所有内存。Android出于安全性考虑,只会给你一个寒碜的缩略图。

在Android2.3中,默认的Bitmap为32位,类型是ARGB_8888,也就意味着一个像素点占用4个字节的内存。我们来做一个简单的计算题:3200*2400*4 bytes =   30M。

如此惊人的数字!哪怕你愿意为一张生命周期超不过10s的位图愿意耗费这么巨大的内存,Android也不会答应的。

1
Mobile devices typically have constrained system resources.
2
Android devices can have as little as 16MB of memory available to a single application.
这是Android Doc的原文,虽然不同手机系统的厂商可能围绕16M这个数字有微微的上调,但是这30M,一般的手机还真挥霍不起。也只有小米这种牛机,内存堪比个人PC,本着土财主般挥金如土的霸气才能做到。

OK,说了这么多,无非是吐吐苦水,爆爆个人经历而已,实际的解决方案在哪里呢?

我也是Google到的,话说一般百度不了的问题,那就Google或者直接StackOverFlow,只不过得看英文罢了。

*后翻来覆去,我在国外的一个Android团队的博客中找到了相应的方案,印证了我的猜想同时也给出了实际的代码。

我将这篇文章翻译成了中文,作为本博客的基础,建议详细看看。

【译】如何使用Android MediaStore裁剪大图片

这篇博客了不起的地方在于解决了Android对返回图片的大小限制,并且详细解释了裁剪图片的Intent附加数据的具体含义。OK,我只是站在巨人的肩膀上,改善方案,适应更广泛需求而已。

拿图说事儿:

Intent(“com.android.camera.action.CROP”)对应的所有可选数据都一目了然。在了解上面个个选项的含义之后,我们将目光着眼于三个*为重要的选项:

data、MediaStore.EXTRA_OUTPUT以及return-data。

data和MediaStore.EXTRA_OUTPUT都是可选的传入数据选项,你可以选择设置data为Bitmap,或者将相应的数据与URI关联起来,你也可以选择是否返回数据(return-data: true)。

为什么还有不用返回数据的选项?如果对URI足够了解的话,应该知道URI与File相似,你所有的操作如裁剪将数据都保存在了URI中,你已经持有了相应的URI,也就无需多此一举,再返回Bitmap了。

前面已经说到,可以设置data为Bitmap,但是这种操作的限制在于,你的Bitmap不能太大。因此,我们前进的思路似乎明确了:截大图用URI,小图用Bitmap。

我将这个思路整理成一张图片:

这篇主要让大家了解需求的来源,以及如何去思考分析并解决问题。下一篇博客将介绍具体的操作。

 

无框架完整搭建安卓app及其服务端

技术背景:

我的一个项目做的的是图片处理,用 python 实现图片处理的核心功能后,想部署到安卓app中,但是对于一个对安卓和服务器都一知半解的小白来说要现学的东西太多了。

而实际上,我们的项目要求并不算高,以我现有的知识也是能实现相应功能的,所以我将在本文记录下一次没用到任何服务器框架的服务器搭建经历。

 

需要的技术:

  <java>,<socket>,<android>

确切的说只要你会java,就能实现你想要的所有功能了。因为android是基于java的,其使用的代码和原生java一模一样,只是在android上把前后台完全分割开了。

而对于socket也很容易使用,就算没有了解过计算机网络,在看过我这篇博客后你也能有一定的了解。

要实现的功能:

1,android界面及后台

2,安卓和服务器建立连接,并进行连接有效性检查

3,基于字节流的图片收发

4,java调用python用预先训练好的fgsm模型处理图片,并将结果发给客户端

开始实现:

 一:安卓app

首先我们建个安卓工程,看看结构是什么样的:

容易看出,这里有两个大目录分别是”app”,”login”,这两的大目录的子目录的结构是一样的,都有三个子目录(“manifests”,”java”,”res”)。

没错,这两个大目录就是我写的两个界面(顾名思义,登录界面和登录后的界面),这样是不是就觉得恍然大悟了,怪不得平时app都是一个界面一个界面的,

这点和pc还是有点不同的。

 

manifests下写的是xml文件,也就是常用的标签配置文件,用来定义界面的外观。

java中就是你写的java代码,也就是安卓后台代码,一般是给前端界面添加监听,以及网络通信和处理代码。

res就是资源文件夹了,用来放置app需要的资源,比如图标,图片,视频,音乐等。

 

Android开发的经典入门教材和学习路线

Android开发书籍推荐:从入门到精通系列学习路线书籍介绍

(https://www.diycode.cc/wiki/androidbook)

很多时候我们都会不断收到新手的提问“Android开发的经典入门教材和学习路线?”、“Android 开发入门教程有哪些推荐?”等类似的问题,我们不断重复回答这些问题,这让我们萌生了做《 Android开发书籍推荐:从入门到精通系列学习路线书籍介绍》的想法,整理收集开发大牛的学习经验,以便让我们少走弯路,更快速成长。希望这个系列可以成为大家手头应对新手的好答案。

JAVA入门书籍:

《Introduction to java programming》

《Core java》中文译名《Java核心技术》 分为基础知识和高级特性

《Java核心技术(卷1):基础知识(原书第9版) [Core Java Volume I—Fundamentals (Ninth Edition)]》

《Java核心技术(卷2):高级特性(原书第9版) [Core Java, Volume II–Advanced Features]》

JAVA进阶书籍:

《Thinking in java》

《Effective Java》

汤涛推荐理由:第二本要反复多看几遍,另外Java学习还有个技巧,把各种代码检查工具报告的警告都正确处理掉,一个不漏,保证你成长飞快。

stormzhang推荐理由:公认的Java进阶必备,《Effective Java》是一本实用至上的书,78条建议,满满的干货。

袁辉辉推荐理由:《Thinking in java》:非常经典的Java书籍,有些人觉得这个书不适合初学者,可就是我看的*个本Java书,或许是当初自学Java没有高人指点,便挑选了经典书来入手。看一本经典书,*遍能理解个大概,能对整体有一个概念,这就可以了,反复多读几遍,细细咀嚼,每一遍都会有不同的领悟。

一、Android入门:

Android Training (http://hukai.me/android-training-course-in-chinese/index.html)

Android API Guides (https://developer.android.com/guide/index.html)

胡凯推荐理由:入门使用官方的这两份文档是*好不过的了,没有比这个更权威,更准确的Android学习资料了。中文书可以随便买两本入门,配合一起看看就好了。在实践的过程中多参考官方的Sample Code,多按照官方的推荐进行代码实践!

汤涛推荐理由:官方文档,权威专业,入门不二之选,正确的入门姿势,对后续的成长帮助非常之大。

《*行代码》

《疯狂Android讲义》

《Android4高级编程》

《Android编程权威指南》

徐宜生推荐理由:全面、基础,内容丰富!基础类型的书只要看一本就够了,用来全面了解知识体系和结构,不用全部精读,只需要有概念即可。

CJJ推荐理由:《*行代码》作者郭霖,看了郭霖很多博客文章,每一篇都写的很详细,也很用心。这本书大概浏览了一遍,内容浅显易懂,非常适合初学者!

任玉刚推荐理由:《*行代码》作者郭霖,手把手教你入门,清晰易懂。

袁辉辉推荐理由:《疯狂Android讲义》正是这样一本书,也是我看过的*本Android书籍,书中并没有深奥的理论,有大量的实例,边看的过程中,边在电脑面前跟着敲了一遍实例代码,*好能做到理解性地敲,而非看一个单词再电脑面前敲一个。我大概花了一周时间把这本书看完并把大部分的实例代码都亲手敲了一遍。《*行代码》作者郭霖,网上有不少人都推荐这本书作为Android入门书,我大概扫了一遍,知识点较基础,作者文笔不错,书中还穿插了不少打怪涨经验升级的片段,比较风趣。

二、Android进阶:

《App研发录》

《Android群英传》

《深入理解Android》

《Android开发艺术探索》

《Android系统源代码情景分析》

袁辉辉推荐理由:《深入理解Android》邓凡平,作者功力深厚,以情景为分支,从framework源代码层面来,深入分析Android系统,非常适合高级应用开发工程师和系统工程师阅读。《Android系统源代码情景分析 》罗升阳,对Android系统的理解非常深,老罗知识体系很全,文章从app/framework/native/kernel等全方面剖析,这是Android界的尽人皆知的大牛,“老罗栽树,大家乘凉”,非常值得一看,前提要是有扎实基础。

stormzhang推荐理由:《Android开发艺术探索》这是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点,Android开发进阶值得拥有!

任玉刚推荐理由:《Android开发艺术探索》,作者任玉刚。分析android核心知识点,直指高级工程师进阶要点!(作为艺术探索一书的读者,我也是激励推荐的!!非常赞的一本书!)

《Clean Code》

《Clean Coder》

汤涛推荐理由:进阶是个大话题,只看一两本是不够的,甚至只看书也是不够的,能进阶到什么程度只能靠自己积*主动的积累。 硬要推荐的话,就不说 Android 的书了,市面上几本热门书都可以看看。这里推荐的两本,同一人所写,都有中文译本。教你代码怎么写得更好,以及怎么做一个更职业的程序员。

《HeadFirst设计模式》

《重构:改善既有代码的设计》

胡凯推荐理由:在大量实践Android程序之后,我们需要会分辨哪种写法是更优秀的,通过重构来改善既有的代码,通过设计模式的不断理解实践对既有的框架进行优化,追求更加设计良好的程序。

三、Android底层:

《深入理解Android ***》系列书籍,邓凡平老师写的系列。

《Android源码设计模式》,结合设计模式分析源码

《Android框架揭秘》,底层架构的一本好书

徐宜生推荐理由:底层书籍对于应用开发者来说,不用过于深入,主要用于学习设计思路和了解底层设计机制

《Linux内核设计与实现》

《深入理解Linux内核》

袁辉辉推荐理由:Android底层是基于Linux Kernel,所以想成为Android全栈工程师,了解Linux Kernel是非常有必要的。这方面书籍较多,我就列举两本《Linux内核设计与实现》,《深入理解Linux内核》。*阶段只需加深对Android系统整体性的理解,不必拘泥于每个细节都理解,看完能大抵理解kernel进程的管理和调度机制,内存管理和内存寻址,I/O驱动设备(字符设备、块设备、网络设备)和调度机制等有所了解就够了;如果都理解了也就够了,如果想再深入,可以结合Kernel代码多看两遍。

小结:

不管看多少书,更重要的是自己思考,动手重复的实践!也许这个过程很耗时间,但是,这个不断以代码去验证自己的某些猜想的过程,便是技术成长的历程!

本系列书籍推荐方法:

1、按照自身的学习路程,亲自看过的书籍;

2、写一个小小邀请,邀请一些开发牛人给列一个书单,然后综合筛选;

3、整理完毕了,网络上让大家继续推荐,随时更新;

4、你的一些整理方法,总之做出一份优质的推荐书籍就好哈;

5、因为Android发展太快了,所以一些时间特别久远的书籍可能不合适了,比如2012年前出版的一些Android开发书籍已经不适用了;

6、需要有一些适当的JAVA基础书籍推荐。

 

另一份书籍推荐

一个老鸟发的公司内部整理的 Android 学习路线图(https://www.diycode.cc/topics/122)

———————————————————————————————————————-

一个老鸟发的公司内部整理的 Android 学习路线图 Markdown 版本

(https://www.diycode.cc/topics/122)

jixiaohua发了一篇一个老鸟也发了一份他给公司内部小伙伴整理的路线图。另一份 Android 开发学习路线图。可惜不是MarkDown格式的,所以jixiaohua直接上传的截图,在jixiaohua的呼吁下,我花了些时间,把这篇大牛的推荐清单编辑成了Markdown格式,方便大家浏览,学习。

有一些链接可能还不是特别准确,因为我只能根据图片上的书或者资源的名字去Google可能的书籍,所以链接上有什么不对的,欢迎大家评论指出,我会及时更正。请参考原文:另一份 Android 开发学习路线图。帮助修改。谢谢。

1.基础工具部分: 中文手册,我猜测是Maven中文手册,可是我并没有找到这样的资 源,欢迎知道的朋友告诉我;

2.Android部分有 『第三方库集合』,我没能找到资源地址;

3.书籍我大多是给的豆瓣链接,如果觉得不合适可以替换一下;

程序设计

一、java

(a)基本语法(如继承、异常、引用、泛型等)

*Java核心技术 卷I(适合入门)

(https://book.douban.com/subject/25762168/)

*进阶

*Effective Java中文版(如何写好的Java代码)

(https://book.douban.com/subject/3360807/)

*Java解惑(介绍烂Java代码是什么样的)

(https://book.douban.com/subject/5362860/)

(b)多线程、并发

*Java并发编程实战 (系统全面的介绍了Java的并发,如何设计支持并发的数据结构)

(https://book.douban.com/subject/10484692/)

(c)Java 7

*Java程序员修炼之道 (详细的介绍Java 7 的新特性)

(https://book.douban.com/subject/24841235/)

(d)Java 8

*写给大忙人看的Java SE 8

(https://book.douban.com/subject/26274206/)

*函数式编程思维

(https://book.douban.com/subject/26587213/)

(e)Java虚拟机

*深入理解Java虚拟机 (并不是那么难,Java程序员都该看看)

(https://book.douban.com/subject/24722612/)

(f)性能优化

*Java性能优化权威指南 (后面的章节好像用处不大,前面有些部分还是值得看)

(https://book.douban.com/subject/25828043/)

二、算法与数据结构

*算法时间复杂度、空间复杂度的基本认知

*熟悉常用数据结构:链表、队列、散列表、树等;

*递归、分支等基本思想;

*常用算法应用:排序、查找、比较等

*数据结构与算法分析 (涵盖面比较全、示例是Java语言)

(https://book.douban.com/subject/1139426/)

*算法设计与分析基础 (实用主义的典型、偏算法设计)

(https://book.douban.com/subject/26337727/)

*编程珠玑 (实践型算法数据)

(https://book.douban.com/subject/3227098/)

三、操作系统

*对Linux/OS的基本认知

*Linux的常用命令

*鸟哥的Linux私房菜

(https://book.douban.com/subject/4889838/)

*Linux内核设计与实现(原书第3版) (很精炼的语言描述清楚了内核算法)

(https://book.douban.com/subject/6097773/)

四、网络

*Http/Https

*TCP/IP

*图解HTTP

(https://book.douban.com/subject/25863515/)

*图解TCP/IP

(https://book.douban.com/subject/24737674/)

*进阶

*TCP/IP详解

(https://book.douban.com/subject/1088054/)

五、Android

*四大组件(服务、广播、ContentProvider、页面容器)

*基础UI组件(ListView、ViewPager)

*异步任务机制(AsyncTask、Handler、线程池)

*布局优化(层级、绘制、碎片化处理)

*图片加载(Bitmap、缓冲区)

*UniversalMusicePlayer (通过学习一个音乐播放器的代码能很快了解四大组件)

(https://github.com/googlesamples/android-UniversalMusicPlayer)

*Android Training官方课程

(http://hukai.me/android-training-course-in-chinese/index.html)

*Android一些重要知识点解析整理

(https://github.com/FX-Max/Point-of-Android)

*Android UI/UX库(各类常用组件及扩展组件的集合)

(https://github.com/wasabeef/awesome-android-ui)

*Picasso 、 Glide (两个图片加载库)

(http://square.github.io/picasso/)

(https://github.com/bumptech/glide)

*The Google I/O 2015 Android App (Google大会官方的App,适合学习各类实现)

(https://github.com/google/iosched)

*Android开发技术前线 (定期翻译、发布国内外Android优质的技术、开源库、软件架构设计、测试等文章)

(http://www.devtf.cn/)

*进阶

*第三方库集合 (列举了常见的各方向第三方库)

软件工程

一、基础工具

*IDE、Git、Maven

*Android Studio

(https://developer.android.com/studio/index.html)

*Git权威指南中文手册

(http://iissnan.com/progit/html/zh/ch1_0.html)

二、软件质量

*代码整洁

*码质量

*码重构

*编写可读代码的艺术 (来自Google工程师,专注于代码可读性)

(http://iissnan.com/progit/html/zh/ch1_0.html)

*代码整洁之道(使用面向对象+敏捷开发原则编写清晰可维护的代码)

(https://book.douban.com/subject/4199741/)

*重构-改善既有代码的设计 (学习改善已有代码)

(https://book.douban.com/subject/4262627/)

*重构手册 (改善代码的实际操作)

(https://book.douban.com/subject/1173730/)

三、设计模式

*23种常见设计模式

*大话设计模式

(https://book.douban.com/subject/2334288/)

*Head First设计模式(两本入门级的设计模式书籍)

(https://book.douban.com/subject/2243615/)

*进阶

*设计模式-可复用面向对象软件的基础(设计模式在实际中的应用)

(https://book.douban.com/subject/1052241/)

四、敏捷开发

*解析*限编程

(https://book.douban.com/subject/1790225/)

*敏捷开发的艺术

(https://book.douban.com/subject/4037534/)

*进阶

*敏捷软件开发-原则、模式与实践

(https://book.douban.com/subject/5348122/)

五、专业开发

*序员职业素养

*更高效、更实效

*程序员的是职业素养

(https://book.douban.com/subject/11614538/)

*程序员修炼之道-从小工到专家

(https://book.douban.com/subject/5387402/)

六、思考人生

*黑客与画家 (硅谷创业之父Paul Craham 的文集,主要介绍黑客及优秀程序员的爱好和动机)

(https://book.douban.com/subject/6021440/)

 

另一份Android开发学习路线

因为表格不是按照MD编辑器做的,目前这份是截图啦

 

mysql数据库规范详解

mysql数据库规范详解

基础规范

【建议】使用InnoDB存储引擎

【强制】无特殊要求必须使用UTF8字符集

【强制】数据表、数据字段必须加入中文注释

【强制】禁止使用存储过程、视图、触发器、Event。特殊情况申请评审

【强制】不在数据库做运算,cpu计算务必移至业务层

命名规范

【建议】 命名使用具有实际意义的英文词汇、词汇缩写,词汇之间使用下划线分隔;

【强制】 命名只能使用小写英文字母、数字、下划线,且必须英文字母开头,下划线为分割符,不能超过32个字符,数据库对象名尽可能简短。避免使用MySQL的保留字

【强制】普通表名命名规则为功能模块前缀_+tablename(login_users); 临时表:tmp前缀+tablename+8位时间后缀(tmp_users_20170501); 备份表:bak前缀+tablename+8位时间后缀(bak_users_20170501); 归档表命名规则:arch前缀+tablename+归档规则 (arch _users_2013)【强制】各表之间相同意义的字段必须同名,数据类型、长度、单位必须相同。

【强制】索引以idx_开头唯一索引以uq_idx开头,后面跟索引所在字段名,多单词组成的列名,取尽可能代表意义的缩写,如t_user_contacts表member_id和friend_id上的组合索引:idx _mid_fid,,组合索引命名应注意字段顺序。如在字段member_id和字段user_id上创建组合索引,则可以命名为idx _uid_mid(userid, member_id)

常用约定:

【建议】序号列字段:以id为后缀,如:user_id表示用户编号

【建议】编码字段:以code为后缀,如:cust_code表示客户编码

【建议】布尔值字段:以“is_”前缀+字段描述+形容词。如member表上表示为enabled的会员的列命名为is_member_enabled。0:否;1:是

【强制】状态字段:以“_status”为后缀,前面加业务逻辑名。如:用户状态可命名为user_status,订单状态为order_status 以此类推

表设计规范(***)

【强制】表设计必须有表主键,并且主键不能提供给外部系统,给外部系统的必须使用业务主键,如user表的业务主键设计,如下id 表主键,自增,表主键不能像外部系统提供 xxx_id 为业务主键,使用IdGenerater(id生成工具类生成,见附件),可以提供给外部系统,使用bigint存储

【强制】表必须有主键,如果使用auto_increment作为自增主键,注意导出初始化脚本时不要设置起始值。

【强制】枚举类型使用tinyint类型

【强制】单表字段数不要太多,*多不要大于50个,且尽可能的少用字符型数据类型

【强制】日期的数据(不包含时分秒的),使用int(11)存储(如,yyyy、yyyyMM、yyyyMMdd),时间的数据((包含时分秒的)),使用datetime存储。

【强制】每个表都必须包含两个保留字段:create_time(创建时间),update_time(*后修改时间)creater varchar(50)(创建人),updater varchar(50)(修改人),设置为非空字段属性。这两个字段不包含额外的业务逻辑。

【强制】每个表设置is_del(0为未删除,1为删除)标记位字段,设置为非空,默认为0的字段属性,生产环境不允许物理删除。特殊表再议

【强制】表和列定义的时候必须加上comment,并能精确描述表和列的含义。类型、状态等字段必须明确给出各个值代表的含义;金钱等计量字段必须给出精确的计量单位;外键字段必须明确给出关联的表和字段

【强制】若需要JOIN的字段(连接键),字段名称、数据类型、长度和单位必须保持*对一致,避免隐式转换

【强制】禁止使用TEXT、BLOB类型(大文本、大文件、大照片存放在文件系统),可以把文件放到文件服务器中,数据库只存url

【强制】不推荐使用enum,set。因为它们浪费空间,且枚举值写死了,变更不方便。推荐使用tinyint或smallint

【强制】如果有业务流转的加字段:业务流水号

【强制】如果一次操作多张表需要查看修改或者回退操作的,加操作流水号

【强制】禁止创建外键约束,外键约束由应用程序控制。外键会导致表与表之间耦合,update与delete操作都会涉及相关联的表,影响sql 的性能,甚至会造成死锁。

【强制】排序字段都不允许为空,并设置默认值。

字段设计规范

【强制】字符串类型一律使用VARCHAR类型,对于明确长度的建议使用char,如身份证号等

【强制】禁止使用TEXT、BLOB类型。会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数据库性能

【建议】字段定义为NOT NULL并且提供默认值。null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化;需要更多的存储空;只能采用is null或is not null,而不能采用=、in、=’2017-02-15′– 正确的写法是: SELECT uid FROM t_user WHERE day>= xxxfunc (‘2017-02-15 00:00:00’)

【强制】禁止使用OR条件。使用IN或者UINON代替

【强制】禁止大表使用JOIN查询,禁止大表使用子查询。*大影响数据库性能

【强制】禁止负向查询,以及%开头的模糊查询。a)负向查询条件:NOT、!=、、!、NOT IN、NOT LIKE等,会导致全表扫描 b)%开头的模糊查询,会导致全表扫描

【强制】使用IN不能超过200

【建议】UNION ALL 代替 UNION操作。

【建议】order by的顺序尽量与索引保持一致

【强制】大批量更新凌晨操作,避开高峰

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速