多媒体指的就是 文字、图片、音频、视频
一、图片篇
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相素的图片
1
2
3
4
|
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
int screen_width = display.getWidth();
int screen_height = display.getHeight();
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/dog.bmp" );
BitmapFactory.Options opts = new Options();
opts.inJustDecodeBounds = true ;
BitmapFactory.decodeFile(file.getPath(), opts);
int pic_width = opts.outWidth;
int pic_height = opts.outHeight;
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 ;
scale = scale > 1 ? scale: 1 ;
opts.inSampleSize = scale;
opts.inJustDecodeBounds = false ;
Bitmap scaledBitmap = BitmapFactory.decodeFile(file.getPath(), opts);
iv.setImageBitmap(scaledBitmap);
|
4.创建原图的副本
为什么要创建原图的副本,因为原图不可以被修改的。
创建副本的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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();
|
5.图形处理的常见API
主要是通过Matrix这个类的几个方法来实现下面这些效果的
镜像:将图片沿图片的Y轴镜像(水平向下为Y轴的正方向)
关于镜像的话,理解起来有些困难,下面我用图形来说明它的实现原理:

6.案例1_画画板
也是要创建一个原图的副本,并给控件添加一个触摸事件,判断事件的类型来进行相应的操作,
UI界面如下:

代码如下:
其中触摸事件的逻辑和保存图片和发送虚假广播更新图库为重点.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
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);
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
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 );
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;
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()
{
try {
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/paintBoard.png" );
FileOutputStream out = new FileOutputStream(file);
createBitmap.compress(Bitmap.CompressFormat.PNG, 100 , out);
out.close();
Intent intent = new Intent();
intent.setAction( "android.intent.action.MEDIA_MOUNTED" );
intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
sendBroadcast(intent);
Toast.makeText( this , "保存成功" , 0 ).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
7.案例2_撕衣服
原理:利用相对布局控件重叠的特性,将2张图片叠在一起.其中,没有穿衣服的图片放下面,穿衣服
的图片放在上面,而且是原图的副本,这样才能结合触摸事件,修改副本的像素颜色为透明的.
效果图:

代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
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);
Bitmap src_Bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pre19);
final Bitmap copy_Bitmap = Bitmap.createBitmap(src_Bitmap.getWidth(), src_Bitmap.getHeight(), src_Bitmap.getConfig());
final Canvas canvas = new Canvas(copy_Bitmap);
final Paint paint = new Paint();
canvas.drawBitmap(src_Bitmap, new Matrix(), paint);
iv_before.setImageBitmap(copy_Bitmap);
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)核心代码
音乐播放方法的接口定义
1
2
3
4
5
6
7
|
publicinterface Iservice {
publicabstractvoid callPlay();
publicabstractvoid callPause();
publicabstractvoid callRePlay();
publicabstractvoid callSeekTo( int position);
}
|
服务里音乐播放逻辑
播放音乐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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();
}
}
|
暂停音乐
1
2
3
4
|
publicvoid pause() {
System.out.println( "暂停播放" );
musicPlayer.pause();
}
|
继续音乐
1
2
3
4
5
|
publicvoid rePlay() {
System.out.println( "继续播放" );
musicPlayer.start();
}
|
播放指定位置音乐
1
2
3
4
5
|
publicvoid seekTo( int position)
{
System.out.println( "跳转到指定的位置" );
musicPlayer.seekTo(position);
}
|
服务里更新进度逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
privatevoid listenMusicPlayer() {
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 };
Message msg = Message.obtain();
msg.obj = mediaInfo;
MainActivity.handler.sendMessage(msg);
}
}
};
timer.schedule(task, 0 , 1000 );
musicPlayer.setOnSeekCompleteListener( new OnSeekCompleteListener() {
@Override
publicvoid onSeekComplete(MediaPlayer mp) {
task.cancel();
timer.cancel();
}
});
}
|
暴露服务的方法,让其它组件可以使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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种:传统方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
SurfaceView sv = (SurfaceView) findViewById(R.id.sv);
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);
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方法里定义结束播放的逻辑,
记住结束播放时的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
SurfaceHolder holder = sv.getHolder();
holder.addCallback( new Callback() {
private int currentPosition;
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (player != null & player.isPlaying())
{
currentPosition = player.getCurrentPosition();
player.stop();
player.release();
}
}
@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);
player.setOnPreparedListener( new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
player.seekTo(currentPosition);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
即调用。
@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,注意要加上包名。
1
2
3
4
5
|
< io.vov.vitamio.widget.VideoView
android:id = "@+id/vv"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
/>
|
3)mainactivity代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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();
}
});
vv.setMediaController( new MediaController( this ));
|
四、照相与录像
1.照相
1
2
3
4
5
6
7
|
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.录像
1
2
3
4
5
6
7
|
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:捕捉设备的广播事件,如接收短信时,自动打开录像功能,开始“偷偷”在后台录像,然
后把视频保存起来并上传到服务器。这样是不是就有点黑客的意思呢。但是说得简单,如
果要“偷偷”地录像,可能需要一些特殊的技术,这个有待研究了。
本文转自屠夫章哥 51CTO博客,原文链接:http://blog.51cto.com/4259297/1679405,如需转载请自行联系原作者