不知不觉已经接近半年多没有写过博客了,这段时间,也是我刚好毕业走出校园的时间,由于学习工作的原因,一直没有真正静下心来写下些什么东西。这个星期刚入了小米笔记本pro的坑,本着新电脑新生活的理念嘻嘻--,我决定把这半年来在工作遇到的一些技术难点分享出来,同时也加深自己的一些理解。

一、效果展示:

如下图所示,是Android端参考iOS iMessages10照片选择器所实现的一个效果:(就是一个小相机+最近照片列表的效果)

         

二、实现思路:

刚开始看到这样的一种功能时,我相信很多Android开发程序员都会菊花一紧吧,初看会让人觉得很难实现(可能我比较菜),但其实我们仔细分析下,并没有想象中那么难,首先我们可以先构思一下布局要如何去实现,让自己有一个大概的思路:

滑动列表我们顺其自然的想到用Recycleview来实现,关于小相机+相册和拍照按钮这两个我们可以尝试用添加头部的方式添加到Recycleview里面去,以此达到整体的滑动效果,这样想想似乎很完美,没有毛病,接下来就想办法把小相机弄出来就好了。

刚开始我就是上面这样的一种思路,大概效果也根据这个思路实现了出来,但是这里有一个问题我们忽略掉了,就是Recycleview的复用问题,如果小相机作为头部添加到Recycleview里面去,当滑动列表,小相机从不可见变为可见时,因为复用会导致每次都去重新加载这个小相机,导致滑动非常卡,体验非常不好。

这种布局体验不好,那我们就换一种实现方式,我相信很多人也都想到了,把相册和拍照按钮+小相机+Recycleview的三个布局顺序排放,在它们的外面嵌套一层Horizontalscrollview,这样同样能达到我们的目的,但这种方式同样有一个小坑,Horizontalscrollview和Recycleview相互嵌套会使得Recycleview显示不全(只显示一行),不过这个问题我们可以通过动态计算Recycleview的宽度来解决。

总体思路有了,我们就一步一步来实现我们所需要的效果吧,首先要讲的也是本篇最为重要的一个点,就是小相机的实现方式,其实也就是对Android Camera2的使用,关于Camera2我就不做过多的介绍了,它其实就是安卓5.0开始(API Level 21)的一个新的相机API,可以用来完全控制安卓相机设备。

三、小相机的实现:

1.首先我们应该在布局文件中定义一个TextureView,这个TextureView主要用来装载显示我们所获取到的相机数据,通俗一点来讲,这个TextureView就是我们的小相机啦,这里TextureView的宽高可以由我们自己来设定,但是这里需要注意一点,宽高应该按照一定的比例来设定,不然相机数据会被拉伸,例如我们可以使用4:3或者16:9的比例来设定,布局代码简单如下:

<TextureView
android:id="@+id/textureView"
android:layout_width="90dp"
android:layout_height="160dp" />

2.接下来要先初始化TextureView,首先让TextureView所在的Activity继承TextureView.SurfaceTextureListener这个接口,这个接口需要重写四个方法,分别为:onSurfaceTextureAvailable、onSurfaceTextureSizeChanged、onSurfaceTextureDestroyed、onSurfaceTextureUpdated,重写后调用如下代码进行初始化TextureView:

    private void initTextureView() {
//我们这里顺便new一个handler出来,后面会用到
mCameraThread = new HandlerThread("CameraThread");
mCameraThread.start();
mCameraHandler = new Handler(mCameraThread.getLooper()); mTextureView.setSurfaceTextureListener(this);
}

3.重写的这四个方法中,看名字我们也大概猜到它的意思了,这里我们主要看的是onSurfaceTextureAvailable这个方法,这个方法也是我们最核心的一个方法,当Activity接收到这个方法的回调时,则代表我们的TextureView已准备就绪,此时可以进行相关的相机设置并打开我们的相机获取数据,代码如下:

   /**
* *****************************TextureView.SurfaceTextureListener******************************
*/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//当SurefaceTexture可用的时候,设置相机参数并打开相机
this.width = width;
this.height = height; setupCamera(width, height);
openCamera(mCameraId);
} @Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
} @Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) { }

4.接下来我们看setupCamera这个方法里面是如何配置相机的,直接看代码:

    private void setupCamera(int width, int height) {
//获取摄像头的管理者CameraManager
CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
//遍历所有摄像头
for (String cameraId : manager.getCameraIdList()) {
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
//默认是后置摄像头,这个判断是检测哪一个是前置摄像头并进行相关记录(为了后面可以点击切换前后摄像头)
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) {
mCameraIdFront = cameraId;
} else {
mCameraId = cameraId;
} //获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//根据TextureView的尺寸设置预览尺寸
mPreviewSize = getOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height);
//获取相机支持的最大拍照尺寸
mCaptureSize = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new Comparator<Size>() {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getHeight() * rhs.getWidth());
}
}); //此ImageReader用于拍照所需
setupImageReader();
}
} catch (Exception e) {
e.printStackTrace();
}
}

getOptimalSize这个方法作用是根据TextureView的尺寸大小来获取一个合适的预览尺寸,代码如下:

    //选择sizeMap中大于并且最接近width和height的size
private Size getOptimalSize(Size[] sizeMap, int width, int height) {
List<Size> sizeList = new ArrayList<>();
for (Size option : sizeMap) {
if (width > height) {
if (option.getWidth() > width && option.getHeight() > height) {
sizeList.add(option);
}
} else {
if (option.getWidth() > height && option.getHeight() > width) {
sizeList.add(option);
}
}
}
if (sizeList.size() > 0) {
return Collections.min(sizeList, new Comparator<Size>() {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getWidth() * rhs.getHeight());
}
});
}
return sizeMap[0];
}

setupImageReader是对ImageReader的一个简单配置,在ImageReader这里我们可以获取到小相机所拍摄到的照片,可以在这里进行相关存储照片等操作,这里我把拍完的照片(如何拍照请看后面)保存到了SD卡根目录的/ifreegroup/CameraV2/文件夹中,需要注意的一点是:这里如果保存完后想进行一些刷新界面的操作,需要使用Handler和Message的方式,不要直接在setOnImageAvailableListener回调中进行,不然会报错,代码代码如下:

private ImageReader mImageReader;
private void setupImageReader() {
//2代表ImageReader中最多可以获取两帧图像流
mImageReader = ImageReader.newInstance(mCaptureSize.getWidth(), mCaptureSize.getHeight(),
ImageFormat.JPEG, 2);
     
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image mImage = reader.acquireNextImage();
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String path = Environment.getExternalStorageDirectory() + "/ifreegroup/CameraV2/";
File mImageFile = new File(path);
if (!mImageFile.exists()) {
mImageFile.mkdir();
}
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = path + "IMG_" + timeStamp + ".jpg";
FileOutputStream fos = null;
try {
fos = new FileOutputStream(fileName);
fos.write(data, 0, data.length); Message msg = new Message();
msg.what = CAPTURE_OK;
msg.obj = fileName;
mCameraHandler.sendMessage(msg); } catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
mImage.close();
}
}, mCameraHandler); mCameraHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case CAPTURE_OK:
//图片已经保存好了,在这里做你想做的事
break;
}
}
};
}

5.到这里我们的相机已经配置完毕了,一切准备就绪,接下来就去打开相机吧,这里指的是使用Camera2 API 打开我们的相机数据,而不是指调用打开我们的系统相机,关于怎么打开相机,这里分装成了一个方法openCamera(mCameraId),也就是我们上面在onSurfaceTextureAvailable所调用的方法,这个方法又是怎样的呢,我们也直接来看代码:

    private void openCamera(String CameraId) {
//获取摄像头的管理者CameraManager
CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
//检查权限
try {
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
//打开相机,第一个参数指示打开哪个摄像头,第二个参数stateCallback为相机的状态回调接口,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
manager.openCamera(CameraId, mStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
} private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
//相机已打开,此时可以开启我们的小相机预览
startPreview();
} @Override
public void onDisconnected(CameraDevice camera) {
camera.close();
mCameraDevice = null;
} @Override
public void onError(CameraDevice camera, int error) {
camera.close();
mCameraDevice = null;
}
};

6.配置好了相机,也打开了相机,接下来就去开启我们的小相机预览吧,代码如下:

 public void startPreview() {
SurfaceTexture mSurfaceTexture = mTextureView.getSurfaceTexture();
if (mSurfaceTexture != null) {
//设置TextureView的缓冲区大小
mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
//获取Surface显示预览数据
Surface mSurface = new Surface(mSurfaceTexture);
try {
//创建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示预览请求
mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//设置Surface作为预览数据的显示界面
mCaptureRequestBuilder.addTarget(mSurface);
//创建相机捕获会话,第一个参数是捕获数据的输出Surface列表,第二个参数是CameraCaptureSession的状态回调接口,当它创建好后会回调onConfigured方法,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
mCameraDevice.createCaptureSession(Arrays.asList(mSurface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
//创建捕获请求
mCaptureRequest = mCaptureRequestBuilder.build();
mCameraCaptureSession = session;
//设置反复捕获数据的请求,这样预览界面就会一直有数据显示
mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mCameraHandler); } catch (Exception e) {
e.printStackTrace();
}
} @Override
public void onConfigureFailed(CameraCaptureSession session) { }
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}

7.如何拍照呢?也很简单,这里我也分装成了一个方法capture(),每次拍照只需要调用这个方法就行了,这里有一点需要注意一下,就是前置摄像头拍完照后照片会变歪,所以需要我们自己手动旋转一下,代码如下所示:

    //拍照方向
private static final SparseIntArray ORIENTATION = new SparseIntArray(); static {
ORIENTATION.append(Surface.ROTATION_0, 90);
ORIENTATION.append(Surface.ROTATION_90, 0);
ORIENTATION.append(Surface.ROTATION_180, 270);
ORIENTATION.append(Surface.ROTATION_270, 180);
}
private void capture() {
if (mCameraDevice == null) {
return;
}
try {
final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
int rotation =activity.getWindowManager().getDefaultDisplay().getRotation();
mCaptureBuilder.addTarget(mImageReader.getSurface());
//CameraFront是自定义的一个boolean值,用来判断是不是前置摄像头,是的话需要旋转180°,不然拍出来的照片会歪了
if (CameraFront) {
mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(Surface.ROTATION_180));
} else {
mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
} CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
unLockFocus();
}
};
mCameraCaptureSession.stopRepeating();
mCameraCaptureSession.capture(mCaptureBuilder.build(), CaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
} private void unLockFocus() {
try {
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
//mCameraCaptureSession.capture(mCaptureRequestBuilder.build(), null, mCameraHandler);
mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

8.如果你跟着上面一步一步的敲下来,那么你的小相机估计也能看到后置摄像头数据并实现拍照保存照片了~ ,如果不行,好好在回去校对看有没有哪里写错了吧。

9.前后摄像头的切换功能,关于这个实现非常简单,已知我们前面在配置相机的时候已经对前摄像头的ID进行了记录,我们只需要重新的调用 setupCamera()和openCamera()这两个方法重新刷新下界面就可以了,代码如下:(代码中mCameraIdFront是前置摄像头ID,mCameraId是后置摄像头ID,想要打开哪个摄像头,只需要将相关ID传入openCamera(id)这个方法里面就可以了)

    private  boolean CameraFront;
public void switchCamera() {
if (mCameraDevice != null) {
mCameraDevice.close();
} if (CameraFront) {
setupCamera(width, height);
openCamera(mCameraId);
CameraFront = false;
} else {
setupCamera(width, height);
openCamera(mCameraIdFront);
CameraFront = true;
}
}

10.这里有个小坑,就是在小相机在已经加载好的情况下,如果你在其他地方调用到了系统相机,当你回到小相机页面时,你会发现小相机没有了预览数据,所以这里我还定义了一个刷新相机界面的方法,同样是调用 setupCamera()和openCamera()这两个方法,供我们在必要合适的时候重新刷新一下小相机数据,防止其黑屏,代码如下所示,代码里的mCameraId是我们前面获取到的后置摄像头ID,这里表示刷新后默认先打开后置摄像头

    public void refreshCamera() {
setupCamera(width, height);
openCamera(mCameraId);
CameraFront = false;
}

到这里,我们的小相机基本功能已经可以使用了,接下来只需要把Recycleview列表加上去就好了。

四、Recycleview列表显示最近保存的照片:

1.这里主要是提供一个可以获取最近保存到手机图片的方法,代码如下:

   private static ArrayList<String> getNearImags(Context context) {
int count = 0;
ArrayList<String> img_path = new ArrayList<>();
// 获取SDcard卡的路径
String sdcardPath = Environment.getExternalStorageDirectory().toString(); ContentResolver mContentResolver = context.getContentResolver();
Cursor mCursor = mContentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA},
MediaStore.Images.Media.MIME_TYPE + "=? OR " + MediaStore.Images.Media.MIME_TYPE + "=?",
new String[]{"image/jpeg", "image/gif"}, MediaStore.Images.Media._ID + " DESC"); // 获取ipg和gif图片,并按图片ID降序排列 while (mCursor.moveToNext()) {
count++;
// 过滤掉不需要的图片,只获取拍照后存储照片的相册里的图片
String path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Media.DATA));
//只取前30张
if (count > 30) {
break;
}
}
mCursor.close();
return img_path;
}

五、动态计算RecycleView的宽度:

Recycleview的LayoutManager我使用的是StaggeredGridLayoutManager,设置的是两行,如下:

photoRecycleView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL));

列表总共两行并且是固定的,这就比较好办了,我们获取拿到的照片总数,然后看其能不能被2整除,可以的话只要除以2然后乘以每一个item的宽度就是Recycleview的宽度了,如果不可以被2整除,那么还需要再加多一个item的宽度,代码如下:(这里的128dp是我默认的每一个item的宽度)

   //动态计算recycleview高度
if (getNearImags(context) != null) {
int size = getNearImags(context).size();
ViewGroup.LayoutParams mParams = photoRecycleView.getLayoutParams();
if (size % 2 == 0) {
mParams.width = ScreenUtil.dip2px(128) * size / 2;
} else {
mParams.width = ScreenUtil.dip2px(128) * (size / 2) + 1;
}
photoRecycleView.setLayoutParams(mParams);
}
photoRecycleView.setAdapter(adapter);

到这里,关于Android模仿iOS iMessages10照片选择器的实现思路大概就是这样了,这个过程中遇到的坑也都进行了红色标注,因为是集成在项目里面的,暂时还没有DEMO,如果有其他的问题,欢迎大家一起讨论~

QQ:471497226@qq.com

Android模仿iOS iMessages10照片选择器的实现的更多相关文章

  1. iOS 模仿微信的照片选择器

    功能和微信的基本一致. 这个选择器使用了循环方式,保证在浏览图片时内存中只加载最多3张图片.稳定的内存大小,可定义图片尺寸.详细说明在github. 下载地址: https://github.com/ ...

  2. Android:抄QQ照片选择器(按相册类别显示,加入选择题)

    这个例子的目的是为了实现类似至QQ照片选择功能.选择照片后,,使用类似新浪微博 微博 页面上显示. 先上效果图:     本例中使用的主要技术: 1.使用ContentProvider读取SD卡全部图 ...

  3. Android 打造属于自己的照片选择器

    前言 在做第一个项目时照片选择器使用了开源的PhotoPicker 渐渐无法满足需求,就想着打造一款属于自己的照片选择器. 花了一周的时间完成了该项目,其实代码有一大半并非自己写的,在阅读PhotoP ...

  4. 部分Android或IOS手机拍照后照片被旋转的问题

    1.我们平时手机拍的照片,传到电脑后,使用Photoshop或者其它图片浏览工具打开时,发现图片是被转过的.可是Windows上预览却是正的.其实原因是部分Android或IOS手机拍照后,将图片角度 ...

  5. iOS解决隐藏导航栏后,打开照片选择器后导航栏不显示的问题以及更换导航栏背景色

    问题描述: 遇到一种情况,在一个控制器上(隐藏了导航栏),打开照片选择器 UIImagePickerController后,照片选择器头部一片空白,且上滑相册时,信息会有错乱效果. 原因分析: 通过查 ...

  6. UIImagePicker照片选择器

    UIImagePickerController 1.+(BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType ...

  7. Cocos2d-x 3.x 头像选择,本地相册图片+图片编辑(Android、IOS双平台)

    大连游戏产业不是很发达,最后,选择一个应用程序外包公司.积累的工作和学习过程中的一点业余生活微信体验,我想分享的游戏小朋友的爱. 在应用开发过程中会经常实用户上传头像的功能,在网上找了N多资料发现没有 ...

  8. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  9. js模仿ios select效果

    github:https://github.com/zhoushengmufc/iosselect webapp模仿ios下拉菜单 html下拉菜单select在安卓和IOS下表现不一样,iossel ...

随机推荐

  1. div内部实现图片旋转、放大、缩小、拖拽

    药药,切克闹,一人我编码累,累把那bug写成堆.秋高气爽空气干燥你一定dei多喝水,过完了这周我就要回去.趁还有几天.你尽情的来跟我怼~~~ 新的一年,很久没更博客了,眼看十一要来了,听说过了十一就等 ...

  2. Java 编程思想 Chapter_14 类型信息

    本章内容绕不开一个名词:RTTI(Run-time Type Identification) 运行时期的类型识别 知乎上有人推断作者是从C++中引入这个概念的,反正也无所谓,理解并能串联本章知识才是最 ...

  3. Raspiberry Camera详解+picamera库+Opencv控制

    使用树莓派的摄像头,将树莓派自身提供的picamera的API数据转换为Python Oencv可用图像数据: # import the necessary packages from picamer ...

  4. load data(sql)

    一般对于数据库表的插入操作,我们都会写程序执行插入sql,插入的数据少还可以,如果数据多了.执行效率上可能就不太理想了.load data语句用于高速地从一个文本文件中读取数据,装载到一个表中,相比于 ...

  5. hdu 5937 -- Equation(搜索)

    题目链接 problem description Little Ruins is a studious boy, recently he learned addition operation! He ...

  6. Divisors poj2992

    Divisors Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 9940   Accepted: 2924 Descript ...

  7. C - Coin Change (III)(多重背包 二进制优化)

    C - Coin Change (III) Time Limit:2000MS     Memory Limit:32768KB     64bit IO Format:%lld & %llu ...

  8. struts整合easyUI以及引入外部jsp文件url链接问题

    找了很久没有解决,在这篇博客中找到了思路,在此引用: 使用EasyUI搭建后台页面框架 EasyUI菜单的实现 ssh项目可参考: ssh框架项目实战

  9. Python实战之Selenium自动化测试web刷新FW

    需求:将手工登录,手工刷新服务器的FW转化为Python+Selenium实现自动化操作. 1.创建用户表,实现数据与脚本分离.需要读取模块. 2.自动化刷新FW. 不说话,直接上代码: 1userd ...

  10. HDU2036 改革春风吹满地

    第一次看到这题果断放弃,毕竟几何白痴,第二次刷没做的题的时候突然想到这个三角形面积的向量法:S=|x1*y2-x2*y1|  但是此题可能是凹多边形,所以不能加绝对值,可以画个凹四边形看看. HDU2 ...