多媒体指的就是 文字、图片、音频、视频
一、图片篇
1.图片大小的计算
图片大小 = 分辨率 * 位深/8 (以字节为单位)
分辨率:就是图片的长*宽,单位是相素。
位深:就是每个相素所占的二进制位数。(右击图片的属性可以查看图片的位深)
对于BMP图片,有以下几种存储格式:
1)单色:每个像素最多能够表示2种颜色, 2 = 2的1次方,只要使用长度为1的二进制位来表
示,位深为1,那么一个像素占1/8个byte,也就是Log22 / 8。
2)16色:每个像素最多能够表示16种颜色,16=2的4次方,只要使用长度为4的二进制位来表
示,位深为4,那么一个像素占4/8个byte ,也就是Log216 / 8。
3)256色:每个像素最多能够表示256种颜色,256=2的8次方,只要使用长度为8的二进制位来
表示,位深为8,那么一个像素占8/8个byte ,也就是Log2256 / 8。
4)24位:每个像素最多能够表示1600多万多种颜色,16777216 = 2的24次方,只要使用长度
为24的二进制位来表示,位深就为24,那么一个像素占 24 / 8个byte,也就是
Log216777216 / 8。
RGB24使用24位来表示一个像素
下面这个是我截的图:
对于其它常见格式的图片,如PNG、 JPG等,图片采用了压缩算法进行了压缩,所以图片像素的
大小不能轻易的算出来,此处暂不花费过多的时间去研究。
2.Android下图片大小的计算
在Android系统下,通常用BitmapFactory来加载图片。
Android中 Bitmap的默认加载使用ARGB_8888色彩模式,每个像素会占用4byte。
比如:一个512*512的图片,无论什么格式,加载进入内存都占用512*512*4=1MB,所以,Android
图片占用内存大小,只与图片的分辨率(像素)以及加载使用的色彩模式有关)
补充参考链接: http://www.cnblogs.com/fengzhblog/p/3227471.html
http://my.oschina.net/u/1389206/blog/324731
3.Android下加载大图片
1)oom异常
我利用Android提供的ImageView控件来直接加载一张2400*3200相素的图片
ImageView iv = (ImageView) findViewById(R.id.iv);
Bitmap srcBitmap = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getPath() + "/bigpic.bmp");
iv.setImageBitmap(srcBitmap);
然后报了下面这个异常:
上面我用红色矩形标记的那一行,意思就是说30720012字节分配超过了heap size的最大值
16777216字节。那么这两个数字是怎么来的呢?
在创建AVD的时候,我给heap size分配的内存为16MB = 16777216字节,所以16777216
指的是AVD的heap size,这个参数是虚拟机给每个应用程序分配的最大内存空间。
在上面图片大小的计算中,已经说到了 Android中图片大小计算的方法,我的代码是采用
Bitmap来加载图片的,所以图片的大小为2400*3200*4 = 30720000(与30720012相比少了12个
字节???)
可见,Android在加载图片的时候,图片的分辨率如果过大的话,就会发生内存溢出异常out
of memory。那么下面就介绍Android中如何避免这个异常出现。
2)缩放加载大图片
图片分辨率(尺寸)过大,那么加载的时候就把图片尺寸变小,那么应该怎么变小呢?一般
情况就是根据手机的屏幕分辨率来加载图片的。步骤如下:
第1步:获取手机分辨率
第2步:获取图片分辨率
第3步:计算绽放比
第4步:按绽放比去加载图片
代码如下:
//[1]获取手机分辨率 通过WindowManager这个类去实现。
//得到WindowManager对象
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
//得到display对象
Display display = windowManager.getDefaultDisplay();
int screen_width = display.getWidth();
int screen_height = display.getHeight();
//[2]获取图片的分辨率
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/dog.bmp");
BitmapFactory.Options opts = new Options();
opts.inJustDecodeBounds = true; //表示不会为图片的像素分配内存空间
BitmapFactory.decodeFile(file.getPath(), opts);
//当opts的上面这个属性设为true时,这个方法会返回null,但是会将
//图片的size信息存放到opts对象里。关于这个方法的使用,API说得很明白。
int pic_width = opts.outWidth;
int pic_height = opts.outHeight;
//[3]计算缩放比
int scale = 1; //最终用的缩放比
//我个人认为严密的计算缩放比应该是向上取舍
int scale_x = (int) Math.ceil(pic_width * 1.0 / screen_width);
int scale_y = (int) Math.ceil(pic_height * 1.0 / screen_height);
scale = scale_x > scale_y ? scale_x:scale_y ;
//如果缩放比小于或等于1,就没有必要缩放了。
scale = scale > 1 ? scale:1;
//[4]按绽放比去加载图片
opts.inSampleSize = scale;
//指定加载图片时的缩放比,如果为4,图片的大小会变成原来的1/16。
//关于这个属性,API说得很明白。
opts.inJustDecodeBounds = false;
Bitmap scaledBitmap = BitmapFactory.decodeFile(file.getPath(), opts);
iv.setImageBitmap(scaledBitmap);
4.创建原图的副本
为什么要创建原图的副本,因为原图不可以被修改的。
创建副本的代码如下:
//前面的缩放步骤,在此就省略不写了。
src_Bitmap = BitmapFactory.decodeFile(pathName, opts);
//【第一步】获取原图的参数
int src_width = src_Bitmap.getWidth();
int src_height = src_Bitmap.getHeight();
Config src_config = src_Bitmap.getConfig(); //颜色模式
//【第二步】创建一个副本作为模板,用原图的参数。
copy_Bitmap = Bitmap.createBitmap(src_width, src_height, src_config);
//【第三步】创建一个画布,以模板为参数
canvas = new Canvas(copy_Bitmap);
//【第四步】创建画笔
paint = new Paint();
//【第五步】开始作画 包括对图片的旋转、缩放、平移等操作。
//canvas.DrawXxx();
//【第六步】 将副本图片显示到控件上
5.图形处理的常见API
主要是通过Matrix这个类的几个方法来实现下面这些效果的
旋转 setRotate
缩放 setScale
平移 setTranslate
镜像 包括2种:倒影和镜像,通过缩放和平移的组合为实现.
倒影:将图片沿图片的X轴镜像(水平向右为X轴的正方向)
镜像:将图片沿图片的Y轴镜像(水平向下为Y轴的正方向)
关于镜像的话,理解起来有些困难,下面我用图形来说明它的实现原理:
6.案例1_画画板
也是要创建一个原图的副本,并给控件添加一个触摸事件,判断事件的类型来进行相应的操作,
UI界面如下:
代码如下:
其中触摸事件的逻辑和保存图片和发送虚假广播更新图库为重点.
package com.itheima.drawpic;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;
public class MainActivity extends Activity {
private Paint paint;
private Bitmap createBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ImageView iv_display = (ImageView) findViewById(R.id.iv_display);
//[1]获取手机分辨率 通过WindowManager这个类去实现。
//得到WindowManager对象
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
//得到display对象
Display display = windowManager.getDefaultDisplay();
int screen_width = display.getWidth();
int screen_height = display.getHeight();
createBitmap = Bitmap.createBitmap(screen_width,screen_height/2 ,Bitmap.Config.ARGB_8888);
//用填充相素给画布图片添加颜色
for(int x = 0; x < createBitmap.getWidth(); x++)
{
for (int y = 0 ; y < createBitmap.getHeight();y++)
{
createBitmap.setPixel(x, y, Color.GRAY);
}
}
//创建一个画布
final Canvas canvas = new Canvas(createBitmap);
paint = new Paint();
iv_display.setImageBitmap(createBitmap);
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
//给ImageView添加触摸事件,开始作画。
//(*****************************重点*********************************)
iv_display.setOnTouchListener(new OnTouchListener() {
float start_x = 0;
float start_y = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
//得到事件类型
int action = event.getAction();
//判断事件的类型
switch (action) {
case MotionEvent.ACTION_DOWN:
System.out.println("按下了...");
//手按下的时候记录起始坐标
start_x = event.getX();
start_y = event.getY();
break;
case MotionEvent.ACTION_MOVE:
System.out.println("移动中...");
//手拖动的时候开始作画
float stop_x = event.getX();
float stop_y = event.getY();
canvas.drawLine(start_x, start_y, stop_x,stop_y, paint);
//记得要更新起点的坐标
start_x = stop_x;
start_y = stop_y;
//更新UI
iv_display.setImageBitmap(createBitmap);
break;
case MotionEvent.ACTION_UP:
System.out.println("抬起了...");
break;
default:
break;
}
return true;
}
});
}
//注册各个按钮的点击事件
public void click(View v)
{
switch (v.getId()) {
case R.id.bt_changePaintColor:
changePaintColor();
break;
case R.id.bt_changePaintWidth:
changePaintWidth();
break;
case R.id.bt_save:
save();
break;
default:
break;
}
}
//改变画笔的颜色
public void changePaintColor()
{
paint.setColor(Color.BLUE);
}
//改变画笔的粗细
public void changePaintWidth()
{
paint.setStrokeWidth(5);
}
//保存图片(*****************************重点*********************************)
public void save()
{
//调用Bitmap的compress方法
try {
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/paintBoard.png");
FileOutputStream out = new FileOutputStream(file);
createBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
//给系统发送一条广播(SD卡状态的改变、开关机),以便相册及时更新。
//下面给手机发送一条欺骗的sd卡状态改变的广播
Intent intent = new Intent();
intent.setAction("android.intent.action.MEDIA_MOUNTED");
//注意还有一条data的scheme信息
intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
sendBroadcast(intent);
Toast.makeText(this, "保存成功", 0).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
7.案例2_撕衣服
原理:利用相对布局控件重叠的特性,将2张图片叠在一起.其中,没有穿衣服的图片放下面,穿衣服
的图片放在上面,而且是原图的副本,这样才能结合触摸事件,修改副本的像素颜色为透明的.
效果图:
代码如下:
package com.itheima.dragclothes;
import java.util.BitSet;
import android.R.integer;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.os.Bundle;
import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.ImageView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//得到关心的控件,并设置图片。
ImageView iv_after = (ImageView) findViewById(R.id.iv_after);
final ImageView iv_before = (ImageView) findViewById(R.id.iv_before);
iv_after.setImageResource(R.drawable.after19);
//注意上面的图片由于要修改,所以要搞副本。
//[1]得到原图
Bitmap src_Bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pre19);
//[2]得到原图的副本
final Bitmap copy_Bitmap = Bitmap.createBitmap(src_Bitmap.getWidth(), src_Bitmap.getHeight(), src_Bitmap.getConfig());
//[3]创建画布,以副本为模板。
final Canvas canvas = new Canvas(copy_Bitmap);
//[4]创建画笔
final Paint paint = new Paint();
canvas.drawBitmap(src_Bitmap, new Matrix(), paint);
iv_before.setImageBitmap(copy_Bitmap);
//[5]给上层的p_w_picpathview添加触摸事件
iv_before.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
System.out.println("move.............");
float current_x = event.getX();
float current_y = event.getY();
for(int i = -7; i < 7;i++)
{
for (int j = -7; j < 7; j++)
{
if(Math.sqrt(i * i + j * j) <= 7)
{
try {
copy_Bitmap.setPixel((int)(current_x + i), (int)(current_y + j), Color.TRANSPARENT);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
iv_before.setImageBitmap(copy_Bitmap);
break;
default:
break;
}
return true;
}
});
}
}
二、音频篇
在Android中,音频播放使用MediaPlayer控件。
1.MediaPlayer的生命周期图
看着貌似很复杂,其实很简单,表示的是MediaPlayer调用哪个方法会进入到哪个状态,或者转
换到哪个状态。
值得一提的就是prepare和prepareAsync的区别:
★prepare:本身就封装了“准备”的动作,所以执行需要点时间,速度比较慢。叫
做同步准备。
★prepareAsync:自己没有封装“准备”动作,我猜应该是开启了另外一个线程去执行
这个“准备”的动作,所以执行速度非常快。所以调用这个方法让
MediaPlayer到达prepared状态不能直接使用start方法,因为有可
能MediaPlayer还没有准备好。应该监听MediaPlayer的onCompare方
法,当prepare动作完成的方法里执行start方法。叫做异步准备。
关于同步和异步的区别:
★同步就是进程在执行请求的时候,如果请求没有响应地话,进程会一直等下去。
★异步就是进程在执行请求的时候,不管请求有没有响应,进程会照常往下执行,
如果请求响应了,再去执行相应的动作。
2.案例1_百度音乐盒
1)UI设计
2)设计思路及核心技术
★音乐播放器都需要能在后台长期运行,所以需要用到服务,把音乐播放逻辑写在服务里。
★但是又要在UI界面调用服务里的方法,所以应该使用startService与bindService相结合开启
服务。
★另外的一个重点就是怎么让进度条随着服务里音乐播放的进度同步更新的问题。可以利
用Timer类来启动一个计时器,实时的监听音乐播放的进度并发送给UI界面的Handler对象。
3)核心代码
★音乐播放方法的接口定义
//定义一个接口来暴露想暴露的方法
publicinterface Iservice {
publicabstractvoid callPlay();
publicabstractvoid callPause();
publicabstractvoid callRePlay();
publicabstractvoid callSeekTo(int position);
}
★服务里音乐播放逻辑
播放音乐
publicvoid play() {
System.out.println("播放音乐");
try {
musicPlayer = new MediaPlayer();
musicPlayer.reset(); //进入空闲状态
musicPlayer.setDataSource(Environment.getExternalStorageDirectory().getPath()+ "/luanhong.mp3"); //进入初始化状态
musicPlayer.prepareAsync(); //进入准备状态
musicPlayer.setOnPreparedListener(new OnPreparedListener() {
@Override
publicvoid onPrepared(MediaPlayer mp) {
musicPlayer.start(); //进入播放状态
}
});
listenMusicPlayer();
} catch (Exception e) {
e.printStackTrace();
}
}
暂停音乐
publicvoid pause() {
System.out.println("暂停播放");
musicPlayer.pause();
}
继续音乐
publicvoid rePlay() {
System.out.println("继续播放");
musicPlayer.start(); //官方文档说start是开始或恢复播放,或
//请直接看Media生命周期图
}
播放指定位置音乐
publicvoid seekTo(int position)
{
System.out.println("跳转到指定的位置");
musicPlayer.seekTo(position);
}
★服务里更新进度逻辑
privatevoid listenMusicPlayer() {
// 将音乐的播放进度和UI界面的seekBar结合在一起
// 得到音乐的总长度
finalint duration = musicPlayer.getDuration();
timer = new Timer();
task = new TimerTask() {
@Override
publicvoid run() {
// 如果音乐播放器不为空且正在播放,获取音乐播放器的播放进度。
if (musicPlayer != null & musicPlayer.isPlaying()) {
int currentPosition = musicPlayer.getCurrentPosition();
int[] mediaInfo = newint[] { duration, currentPosition };
// 通过Handler对象来发送给UI界面。
Message msg = Message.obtain();
msg.obj = mediaInfo;
MainActivity.handler.sendMessage(msg);
}
}
};
timer.schedule(task, 0, 1000); // 0.1秒之后开始每隔1秒监听一次进度
//音乐播放完毕取消计时器及其任务
musicPlayer.setOnSeekCompleteListener(new OnSeekCompleteListener() {
@Override
publicvoid onSeekComplete(MediaPlayer mp) {
task.cancel();
timer.cancel();
}
});
}
★暴露服务的方法,让其它组件可以使用。
// 定义一个Binder的子类对象,在服务的onBind方法里返回这个类的对象。
publicclass MyBinder extends Binder implements Iservice {
@Override
publicvoid callPlay() {
play();
}
@Override
publicvoid callPause() {
pause();
}
@Override
publicvoid callRePlay() {
rePlay();
}
@Override
publicvoid callSeekTo(int position) {
seekTo(position);
}
}
三、视频篇
Android中,视频播放使用的控件仍然是MediaPlayer。但是MediaPlayer进行视频播放的时
候,只支持3GP、MP4两种格式。当然,视频播放的逻辑完全可以用来播放音频。
相比于音频播放,视频播放有以下不同点:
1.视频播放需要一个组件来展示,一般用SurfaceView或者第三方类库vitamio_lib的VideoView
组件。
2.视频播放不需要在后台长期运行,所以它的逻辑一般不写在服务里。
1.案例1_SurfaceView视频播放
SurfaceView控件的介绍:
1)是一个重量级的控件
2)内部维护了一个双缓冲机制AB。A线程加载数据,B线程负责显示;B线程加载数据,A负责显示。
3)可以直接在子线程中更新UI。(还有进度相关的控件可以直接在子线程中更新UI)
视频播放的原理和音频播放的原理类似,包括创建MediaPlayer对象,加载资源,准备,开
始播放等步骤,不同的是多一个显示的步骤。需要获取SurfaceView的SurfaceHolder对象,
然后通过MediaPlayer的setDisplay方法将视频画面显示到SurfaceHolder对象上面。
由于将画面显示到SurfaceHolder有2种方式,所以SurfaceView播放视频的方式也有2种
★第1种:传统方式
//得到SurfaceView控件
SurfaceView sv = (SurfaceView) findViewById(R.id.sv);
//得到SurfaceView控件的持有者SurfaceHolder
final SurfaceHolder holder = sv.getHolder();
try {
new Thread(){
public void run() {
try {
player = new MediaPlayer();
player.reset();
player.setDataSource("http://192.168.17.66/miss.mp4");
player.prepareAsync();
SystemClock.sleep(100);
player.setDisplay(holder); //将视频显示到holder对象上
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
} catch (Exception e) {
e.printStackTrace();
}
★第2种:调用SurfaceHolder的回调方法
这种方式可以更好地控制视频的播放,如在surfaceDestroyed方法里定义结束播放的逻辑,
记住结束播放时的位置。
//得到sv的holder对象
SurfaceHolder holder = sv.getHolder();
//调用holder的回调方法,实现 Callback接口的3个方法。
holder.addCallback(new Callback() {
private int currentPosition;
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//停止播放,点击回退或HOME键视频不会在后台播放。
//如果点击HOME,重新打开视频会继续上次的位置播放。
//如果点击回退,重新打开视频会重头开始播放。
if(player != null & player.isPlaying())
{
currentPosition = player.getCurrentPosition();
player.stop();
player.release();
}
}
//当surface对象创建后,该方法就会被立即调用。
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
player = new MediaPlayer();
player.reset();
player.setDataSource("http://192.168.1.108/song.mp3");
player.prepareAsync();
player.setDisplay(holder); //将视频显示到holder对象上
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
player.seekTo(currentPosition);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//当surface发生任何结构性的变化时(格式或者大小),该方法就会被立
即调用。
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
});
补充:关于SurfaceView,请看http://blog.csdn.net/pathuang68/article/details/7351317
2.案例2_VedioView视频播放
这个VedioView是第3方类库vitamio里的控件,在使用之前,要先导入这个类库。
在导入的时候注意要钩选 copy to this workspace选项。导入完后,在要引用的项目上,
右击选择属性Android,在library中add vitamio_lib ,然后点 apply ok。
vitamio类库的使用步骤:
1)配置InitActivity
<activityandroid:name="io.vov.vitamio.activity.InitActivity"></activity> 修改完清单文件后,要clean一下工程,否则部署程序的话就会报Initactivity没有配置的
错误。
2)在布局中定义VideoView,注意要加上包名。
<io.vov.vitamio.widget.VideoView android:id="@+id/vv" android:layout_width="match_parent" android:layout_height="match_parent" />
3)mainactivity代码
//检查类库是否可用
if(!LibsChecker.checkVitamioLibs(this))
{
return;
}
final VideoView vv = (VideoView) findViewById(R.id.vv);
vv.setVideoPath("http://192.168.17.66/miss.mp4");
vv.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
vv.start();
}
});
//设置VideoView的控制器
vv.setMediaController(new MediaController(this));
四、照相与录像
1.照相
public void takePhoto()
{
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/photo.jpg");
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(intent,1);
}
2.录像
public void recordVedio()
{
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/vedio.mp4");
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(intent,2);
}
也许单纯调用系统功能觉得挺没有意思的,但是如果将录像功能与系统的一些其它功能相结合就会
发现它的妙趣横生:
想法1:捕捉设备的广播事件,如接收短信时,自动打开录像功能,开始“偷偷”在后台录像,然
后把视频保存起来并上传到服务器。这样是不是就有点***的意思呢。但是说得简单,如
果要“偷偷”地录像,可能需要一些特殊的技术,这个有待研究了。
转载于:https://blog.51cto.com/4259297/1679405