一、图片处理
1.1、BitMap
1.Bitmap介绍
Bitmap实现在android.graphics包中。但是Bitmap类的构造函数是私有的,外面并不能实例化,只能是通过JNI实例化。
这必然是某个辅助类提供了创建Bitmap的接口,而这个类的实现通过JNI接口来实例化Bitmap的,这个类就是BitmapFactory。
Bitmap是一片连续的内存空间,而我们的应用程序的内存是散乱的,如果当零散的空间下容纳不了bitmap时就导致OOM。
2.BitmapFactory
其中提供的一个Opitions属性可以用来提前获取到图片的宽高信息,下面会介绍。
2.Bitmap优化
当前有一张图片,大小仅为1M,但是其规格为3648*2736,现在需要加载此图片总像素数=3648*2736=9980928
假设现在像素采用ARGB_4444标准,则其占用的总空间为:
图片占用空间=总像素数 *像素的单位 ----> 9980928 * 2bytes=19961856bytes = 19M > 16M OOM内存溢出所以我们必须对图片进行缩放才能加载:
假设:
图片的宽和高: 3648 * 2736 屏幕的宽和高: 320 * 480计算缩放比:
宽度缩放比例: 3648 / 320 = 11 高度缩放比例: 2736 / 480 = 5比较宽和高的缩放比例, 哪一个大用哪一个进行缩放
缩放后的图片: 3648 / 11 = 331 2736 / 11 = 248 缩放后图片的宽和高: 331* 248 331* 248=882088 * 2bytes=160K3.解析图片的三种方式
1.使用BitmapFactory解析图片
2.使用BitmapDrawable解析图片
3.使用InputStream和BitmapDrawable绘制
1.2、图片特效
图片的特效包括,图形的缩放、镜面、倒影、旋转、位移等。图片的特效是将原图的图形矩阵乘以一个特效矩阵,形成一个新的图形矩阵来实现的。
Matrix维护了一个3*3的矩阵去更改像素点的坐标。 图形的默认矩阵用数组表示为: { 1, 0, 0, 表示向量x = 1x + 0y + 0z 0, 1, 0, 表示向量y = 0x + 1y + 0z 0, 0, 1 } 表示向量z = 0x + 0y + 1z 通过更改图形矩阵的值,可以做出缩放/镜面/倒影等图片特效。1.缩放
2.镜面
唯一不同的地方在矩阵处
3.倒影
4.旋转
5.位移
1.3、颜色过滤器
Android提供了颜色过滤器来进行颜色处理。
a)ColorMatrixColorFilter:通过使用一个4*5的颜色矩阵来创建一个颜色过滤器,改变图片的颜色信息。
b) 图形颜色默认矩阵是一个4x5的矩阵, 数组表现为:
{1, 0, 0, 0, 0, // red 1*R + 0*G + 0*B + 0*A + 0
0, 1, 0, 0, 0, // green 0*R + 1*G + 0*B + 0*A + 0
0, 0, 1, 0, 0, // blue 0*R + 0*G + 1*B + 0*A + 0
0, 0, 0, 1, 0} // alpha 0*R + 0*G + 0*B + 1*A + 0
颜色矩阵的每一行的最后一个值更改时,其对应的颜色值就会发生改变,所以更改颜色只需修改其对应颜色矩阵行的最后一项的值即可,最大值范围为255
a)首先我们声明一个初始化矩阵数组和颜色过滤器
b)重写progressBar的onProgressChanged方法,并修改矩阵数组的值后,重新创建新的颜色过滤器。
1.4、手绘
a)首先在布局文件创建一个ImageView,让其宽高都填充父窗体,并在代码中进行实例化,并设置它的Touch事件。
mIv = (ImageView) findViewById(R.id.iv);mIv.setOnTouchListener(this);
b) 然后重写onTouch(View v, MotionEvent event)方法对手势进行操作
@Overridepublic boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mStartX = (int) event.getX(); mStartY = (int) event.getY(); if(mBitmap == null){ mBitmap = Bitmap.createBitmap(mIv.getWidth(),mIv.getHeight(),Config.ARGB_8888); // 画布 mCanvas = new Canvas(mBitmap); // 设置背景色白色 mCanvas.drawColor(Color.WHITE); mPaint = new Paint(); // 设置画笔颜色为红色,线条粗细为5 mPaint.setColor(Color.RED); mPaint.setStrokeWidth(5); } break; case MotionEvent.ACTION_MOVE: // 记录移动到的位置坐标 int moveX = (int) event.getX(); int moveY = (int) event.getY(); // 绘制线条,连接起始位置和当前位置 mCanvas.drawLine(mStartX, mStartY, moveX, moveY, mPaint); // 设置显示 mIv.setImageBitmap(mBitmap); // 将起始位置改为为当前移动到的位置 mStartX = moveX; mStartY = moveY; break; case MotionEvent.ACTION_UP: break; } return true;}
c) 上述基本功能已经完成,我们还需要一个清空的方法
// 清除界面public void clear(View view){ mBitmap = null; mIv.setImageBitmap(null);}
1.5、三级缓存
1. 简介
现在android应用中不可避免的要使用图片,有些图片是可以变化的,需要每次启动时从网络拉取。
现在有一个问题:假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,
一个很耗流量的应用,其用户数量级肯定要受到影响。当然,我想,向百度美拍这样的应用,必然也有其内部的图片缓存策略。总之,图片缓存是很重要而且是必须的。
2.图片缓存的原理
实现图片缓存也不难,需要有相应的cache策略。这里我采用 内存-文件-网络 三层cache机制,其中内存缓存包括强引用缓存和软引用缓存(SoftReference),
其实网络不算cache,这里姑且也把它划到缓存的层次结构中。当根据url向网络拉取图片的时候,先从内存中找,如果内存中没有,再从缓存文件中查找,
如果缓存文件中也没有,再从网络上通过http请求拉取图片。在键值对(key-value)中,这个图片缓存的key是图片url的hash值,value就是bitmap。所以,
按照这个逻辑,只要一个url被下载过,其图片就被缓存起来了。
关于Java中对象的软引用(SoftReference),如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高 速缓存。使用软引用能防止内存泄露,增强程序的健壮性。
当然,这种回收机制只存在于Android 2.3版本之前,Android 2.3之后垃圾回收器会更倾向于回收软引用、弱引用,它们变得不再可靠。
从代码上来说,采用一个ImageManager来负责图片的管理和缓存,函数接口为public void loadBitmap(String url, Handler handler) ;其中url为要下载的图片地址,
handler为图片下载成功后的回调,在handler中处理message,而message中包含了图片的信息以及bitmap对象。ImageManager中使用的ImageMemoryCache(内存缓存)、
ImageFileCache(文件缓存)以及LruCache(最近最久未使用缓存)会在后续文章中介绍。
二、多媒体
1.1、音频播放
1、MediaPlayer
该播放器同时只能播放一个音乐文件,文件大小并没有限制。MediaPlayer必须严格按照状态图操作,否则就会出现错误。
MediaPlayer播放的实例:
// 创建MediaPlayer对象MediaPlayer mediaPlayer = new MediaPlayer();mediaPlayer.setDataSource(Environment.getExternalStorageDirectory() + "/bg.mp3");// 准备mediaPlayer.prepare();// 判断当前音乐是否在播放boolean isPlaying = mediaPlayer.isPlaying();if(!isPlaying){ mediaPlayer.start();}else{ Toast.makeText(this,"音乐正在播放中",0).show();}
2、SoundPool
SoundPool特点是可以自行设置声音的品质、音量、播放比率等参数。并且它可以同时管理多个音频流,每个流都有独自的ID,对某个音频流的管理都是通过ID进行的。
a)SoundPool最大只能申请1M的内存空间,只能用一些很短的声音片段,而不是用它来播放歌曲或者做游戏背景音乐。
b)SoundPool提供了pause和stop方法,但这些方法建议最好不要轻易使用,因为有些时候它们可能会使你的程序莫名其妙的终止。
c)SoundPool的效率问题。其实SoundPool的效率在这些播放类中算是很好的,这可能会影响用户体验。
SoundPool播放的实例:
// 创建SoundPool对象SoundPool soundPool = new SoundPool(1,AudioManager.STREAM_MUSIC,1);// 加载SoundPool音乐文件int soundID = soundPool.load(this,R.raw.shoot1,1);// 播放soundPool音乐soundPool.play(soundID,1,1,0,0,1);
1.2、视频播放
1.第一种
a)第一种方式可以通过surfaceView和MediaPlayer来实现该功能,首先创建一个SufaceView实例
2.第二种
a)设置布局文件,布局文件比较简单,因此这里只给你VideoView标签。
mSurface = (SurfaceView) findViewById(R.id.sfv); mHolder = mSurface.getHolder();mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
b) 然后通过如下代码播放,注意视频知支持mp4格式
/ 创建播放器对象MediaPlayer player = new MediaPlayer();// 获取音乐路径String path = "/mnt/sdcard/oppo.mp4";// 给播放器设置音乐路径player.setDataSource(path);// 设置音乐格式player.setAudioStreamType(AudioManager.STREAM_MUSIC);// 设置输出画面player.setDisplay(mHolder);// 准备player.prepare();// 播放player.start();
2.第二种
a)设置布局文件,布局文件比较简单,因此这里只给你VideoView标签。
b) 设置VideoView的播放文件路径和媒体控制器,调用start方法即可播放媒体文件,当然现在只是让当前类实现了MediaController接口
VideoView video = (VideoView) findViewById(R.id.video);video.setVideoPath("/mnt/sdcard/oppo.mp4");video.setMediaController(new MediaController(this));// 开始播放video.start();// 设置当前播放器窗口为焦点video.requestFocus();
c)这时,视频已经可以播放,如果想实现自己的业务逻辑,可以定义类实现MediaPlayerControl接口,其中提供大量的回调方法供我们使用。
1.3、传感器
Android手机中内置了很多传感器,其主要类型有:方向、加速度(重力)、光线、磁场、距离(临近性)、温度等。
1.传感器的使用
a) 获取传感器管理器SensorManager
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
b) 通过传感器管理器对象获得指定类型的传感器:
// 获取指定传感器对象,获取系统默认的重力加速度传感器Sensor sensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
c) 通过传感器管理器对象获得手机中所有的传感器
// 获取手机支持的所有传感器ListsensorList = sm.getSensorList(Sensor.TYPE_ALL);for (int i = 0; i < sensorList.size(); i++) { Sensor sensor = sensorList.get(i); // 获取传感器名称 String name = sensor.getName(); // 获取传感器厂商 String vendor = sensor.getVendor(); // 获取传感器版本号 int version = sensor.getVersion();}
d) 使用传感器管理器对象注册传感器来使一个传感器工作:
sm.registerListener(new SensorEventListener() { // 当传感器改变时触发该函数 @Override public void onSensorChanged(SensorEvent event) { } // 当传感器精度更改时触发该函数 @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { }},sensor,SensorManager.SENSOR_DELAY_NORMAL);
(1) listener :传感器事件监听器
(2) sensor :要被注册的传感器对象
(3) rate :采样率,分为最快、游戏、普通、用户界面几种当应用程序请求特定的采样率时,其实只是对传感器子系统的一个建议,不保证特定的采样率可用。
采样率的四种类型详解:
最快: SensorManager.SENSOR_DELAY_FASTEST,最低延迟,一般不是特别敏感的处理不推荐使用,该种模式可能造成耗电,由于传递的为原始数据,算法不处理好会影响游戏逻辑和UI的性能
游戏: SensorManager.SENSOR_DELAY_GAME,游戏延迟,一般绝大多数的实时性较高的游戏都使用该级别 普通: SensorManager.SENSOR_DELAY_NORMAL,标准延迟,对于一般的益智类或EASY级别的游戏可以使用,但过低的采样率可能对一些赛车类游戏有跳帧现象用户界面: SensorManager.SENSOR_DELAY_UI,一般对于屏幕方向自动旋转使用,相对节省电能和逻辑处理,一般游戏开发中我们不使用
总结:由于传感器比较多,这里只是简单的概述,后续会花大量的时间整理传感器的使用。
1.4、摄像头
调用系统摄像头进行拍照和摄像无需添加权限,直接调用即可。只需知道系统摄像头的action和category就可以调用系统摄像头。
a)打开Android源码,查看”\packages\apps\”文件文件目录下的Camera应用,即系统摄像头的应用程序。打开其清单文件文件,查看其Activity的action和category信息。
b)Camera类的action和category
VideoCamera类的action和category
c) 已知调用系统摄像头拍照和摄像功能对应的action和category信息,采用隐式调用的方式调用Activity。
由于希望在调用拍照或摄像功能后回到当前应用的界面,且得知拍照或摄像的结果如何是否成功使用startActivityForResult方法开启Activity,并重写onActivityResult方法处理回传的数据。
1.拍照功能
Intent intent = new Intent();// 设置Actionintent.setAction("android.media.action.IMAGE_CAPTURE");// 创建一个文件File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"my.jsp");// 创建Uri对象Uri uri = Uri.fromFile(file);// 设置图片输出路径intent.putExtra(MediaStore.EXTRA_OUTPUT,uri);// 开启ActivitystartActivityForResult(intent,100);
2.摄像功能
Intent intent = new Intent();intent.setAction("android.media.action.VIDEO_CAPTURE");File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"myVedio.mp4");Uri uri = Uri.fromFile(file);intent.putExtra(MediaStore.EXTRA_OUTPUT,uri);startActivityForResult(intent,101);
3.自定义拍照
a)首先定义一个SurfaceView用作相机的预览
SurfaceView mSurfaceView = (SurfaceView) this.findViewById(R.id.surfaceView);SurfaceHolder holder = mSurfaceView.getHolder();holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);holder.setKeepScreenOn(true);holder.addCallback(new SurfaceCallback());
b )创建SurfaceCallback来开启预览,其中提供了三个回调方法,方便我们对相机预览进行管理。
/**sufaceView的回调*/private final class SurfaceCallback implements Callback { @Override public void surfaceCreated(SurfaceHolder holder) { try { mCamera = Camera.open(0);// 0:打开后置 1:打开前置 if(mCamera != null){ mCamera.setPreviewDisplay(holder); mCamera.setDisplayOrientation(getPreviewDegree(NewCameraCheck.this)); mCamera.startPreview(); }else{ Toast.makeText(NewCameraCheck.this, "获取不到相机实例", Toast.LENGTH_SHORT).show(); } } catch (Exception e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { if(mCamera != null){ parameters = mCamera.getParameters(); parameters.setPictureFormat(PixelFormat.JPEG); parameters.setPreviewSize(width, height); parameters.setPreviewFrameRate(5); parameters.setPictureSize(width, height); parameters.setJpegQuality(80); }else{ Toast.makeText(NewCameraCheck.this, "获取不到相机实例,无法设置参数。", Toast.LENGTH_SHORT).show(); } } // 在Surface销毁时释放资源 @Override public void surfaceDestroyed(SurfaceHolder holder) { if(mCamera != null){ //mCamera.stopPreview(); mCamera.release(); mCamera = null; } }}
c) 提供一个静态方法用来对相机预览的旋转角度进行自动调整
// 提供一个静态方法,用于根据手机方向获得相机预览画面旋转的角度public int getPreviewDegree(Activity activity) { // 获得手机的方向 int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); mDegree = 0; // 根据手机的方向计算相机预览画面应该选择的角度 switch (rotation) { case Surface.ROTATION_0: mDegree = 90; break; case Surface.ROTATION_90: mDegree = 0; break; case Surface.ROTATION_180: mDegree = 270; break; case Surface.ROTATION_270: mDegree = 180; break; } return mDegree;}
d) 我们可以通过调用Camera的tackPicture()方法进行拍照,如果我们需要对焦拍照的话,使用下面这段代码拍照
public void tackImage(){ if(mCamera == null){ Toast.makeText(NewCameraCheck.this, "获取不到相机实例,拍照失败!", Toast.LENGTH_SHORT).show(); return; } mCamera.autoFocus(new AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { mCamera.takePicture(null, null, new MyPictureCallback()); //mCamera.startPreview(); } });}
e) 除此之外,我们相机适配需要注意清单文件中的配置,当然还要记得添加权限
清单文件配置:权限配置:
4.自定义摄像
使用Camera+MediaRecoder + SurfaceView控件可实现录制视频的功能。
a)首先定义一个SurfaceView用作相机的预览
setContentView(R.layout.activity_main);SurfaceView mSurfaceView = (SurfaceView) findViewById(R.id.surface);SurfaceHolder holder = mSurfaceView .getHolder();holder.addCallback(new MyVideoCallback());
b) 创建SurfaceCallback来开启预览,其中提供了三个回调方法,方便我们对相机预览进行管理。
class MyVideoCallback implements Callback{ @Override public void surfaceCreated(SurfaceHolder holder) { mCamera.open(); try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mCamera.stopPreview(); mCamera.release(); }}
c)开始录像需要设置大量的参数,代码中异常未处理,具体操作如下
// 停止预览mCamera.stopPreview();// 解锁摄像头mCamera.unlock();// 初始化MediaRecorder对象MediaRecorder recorder = new MediaRecorder();// 给recorder设置摄像头recorder.setCamera(mCamera);// 设置音频源recorder.setAudioSource(AudioSource.CAMCORDER);// 设置视频源recorder.setVideoSource(VideoSource.CAMERA);// 设置录像质量参数CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);recorder.setProfile(profile);// 设置录像输出路径recorder.setOutputFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/test.mp4");// 设置预览显示对象recorder.setPreviewDisplay(mHolder.getSurface());// 准备recorder.prepare();recorder.start();
d) 如果想停止录像的话,只需要调用如下代码:
mRecorder.stop();mRecorder.reset();mRecorder.release();mCamera.lock();mCamera.startPreview();
e) 当然别忘记添加权限
后续进阶可以参看:Android多媒体开发高级编程
三、拍照和相册获取图片
首先调用系统的相机或打开相册来获取图片
// 拍照btnCamera.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File( Environment.getExternalStorageDirectory(), "temp.jpg"))); startActivityForResult(intent, PHOTOHRAPH); }});// 从相册选择btnPhoto.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, PHOTOZOOM); }});
在activity中的onActivityResult中进行回调获取到打开的图片
public static final int NONE = 0;public static final int PHOTOHRAPH = 1;// 拍照public static final int PHOTOZOOM = 2; // 缩放public static final int PHOTORESULE = 3;// 结果 @Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case NONE: return; case PHOTOHRAPH: // 拍照 // 设置文件保存路径这里放在根目录下 File picture = new File(Environment.getExternalStorageDirectory() + "/temp.jpg"); startPhotoZoom(Uri.fromFile(picture)); break; case PHOTOZOOM: // 从相册选择 startPhotoZoom(data.getData()); break; case PHOTORESULE: Bundle extras = data.getExtras(); if (extras != null) { Bitmap photo = extras.getParcelable("data"); ByteArrayOutputStream stream = new ByteArrayOutputStream(); photo.compress(Bitmap.CompressFormat.JPEG, 75, stream);// (0 - 100)压缩文件 // 设置头像显示 mIvPhoto.setImageBitmap(photo); dialog.dismiss(); } break; }}
其中封装的方法startPhotoZoom,是对图片进行宽高和压缩等处理:
public void startPhotoZoom(Uri uri) { Intent intent = new Intent("com.android.camera.action.CROP"); intent.setDataAndType(uri, "image/*"); intent.putExtra("crop", "true"); // aspectX aspectY 是宽高的比例 intent.putExtra("aspectX", 1); intent.putExtra("aspectY", 1); // outputX outputY 是裁剪图片宽高 intent.putExtra("outputX", 64); intent.putExtra("outputY", 64); intent.putExtra("return-data", true); startActivityForResult(intent, PHOTORESULE);}