转载请标明出处:
http://blog.csdn.net/gj782128729/article/details/52561771;
本文出自:【高境的博客】
首先我们打开电脑中的画图板,截取像素800*400的区域,在图中作画:
分别以24位位图,单色位图,16色位图,256色位图分别保存成4个文件:
查看以上几个图片文件,发现图片的大小不一致,那么图片的总大小是如何计算的呢?以下是计算公式:
图片的总大小 = 图片的总像素 * 每个像素的大小
每个像素的大小取决于图片能表示的颜色的数量。
只能表示黑白两种颜色,使用0和1就足够表示了,每个像素需要一个长度为1的二进制数字表示颜色即每个像素占用1/8个字节。根据公式,上面保存的单色的图片的大小为:
400*800*(1/8) = 40000字节,查看单色图片的属性如1下图:
发现图片的总大小为40062字节,为什么会比我们计算的值大呢?因为图片还要存储一些额外的信息,比如时间等等。
只能表示16种颜色,使用16个数字(0 - 15),换成二进制为0000 - 1111,每个像素需要一个长度为4的二进制数字能表示颜色即一个像素占1/2个字节。
只能表示256种颜色,使用256个数字(-128 ~ 127),换成二进制位0000 0000-1111 1111,一个像素占1个字节。
24位图表示范围为24位个二进制数,我们利用计算器可以看到,24位最大可以表示16777215个数字,所以24位图能表一千六百多万种颜色。
每个像素占用24位,也就是3个字节,分别用RGB表示:
R:0 - 255,使用1个字节就可以表示;
G:同上;
B:同上。
每个像素占用4个字节,分别用ARGB表示:
A:透明度,0 - 255
如果图片要显示到界面,那么内存中需要保存图片的所有像素的颜色信息,内存中使用ARGB保存。
Android系统以ARGB表示每个像素,所以每个像素占用4个字节,很容易内存溢。下面,使用ImageView加载SD卡中的一张大内存的图片,该图片大小如下图:
界面布局:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RelativeLayout>
MainActivity中获取到图片的bitmap对象,利用ImageView显示:
public class MainActivity extends Activity {
@SuppressWarnings("deprecation")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = (ImageView) findViewById(R.id.iv);
BitmapFactory.decodeFile("/mnt/sdcard/dog.jpg");
iv.setImageBitmap(bitmap);
}
}
运行后,程序会崩溃,查看Logcat日志输出,发现内存溢出如下图:
为什么会出现内存溢出呢?当创建模拟器的时候,有一个VM Heap选项,这个选项代表手机模拟器给每个应用默认分配的内存大小,当一个应用的使用内存超过了16M,那么就会报内存溢出的错误。
上图的日志是说创建一个30720012字节的文件时内存溢出。我们来计算下加载的图片显示到手机上的总的大小,图片的分辨率为2400*3200如下图:
在安卓中,是使用ARGB表示图片像素的,所以一个像素是4个byte,根据计算公式,该图片的总大小为2400*3200*4=30720000,发现和日志输出中的数值是一致的。
1.获取屏幕宽高
//通过Context的getWindowManager()方法获取Window的管理者对象
Display dp = getWindowManager().getDefaultDisplay();
int screenWidth = dp.getWidth();
int screenHeight = dp.getHeight();
另一种获取Windowmanager对象的方法:
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
2.获取图片宽高
Options opts = new Options();
//inJustDecodeBounds属性值为true,表示只请求图片属性,不申请内存
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
//获取图片的宽
int imageWidth = opts.outWidth;
//获取图片的高
int imageHeight = opts.outHeight;
3.获取缩放比例
图片的宽高除以屏幕宽高,算出宽和高的缩放比例,取较大值作为图片的缩放比例。
int scale = 1;
int scaleX = imageWidth / screenWidth;
int scaleY = imageHeight / screenHeight;
if(scaleX >= scaleY && scaleX > 1){
scale = scaleX;
}
else if(scaleY > scaleX && scaleY > 1){
scale = scaleY;
}
4.按缩放比例加载图片
//设置缩放比例
opts.inSampleSize = scale;
//inJustDecodeBounds属性为false,表示为图片申请内存
opts.inJustDecodeBounds = false;
//从文件中获取Bitmap,参数1表示文件路径,参数2表示图片参数。
Bitmap bm = BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
iv.setImageBitmap(bm);
我们在使用美图秀秀对图片进行编辑操作后,保存图片的时候,是保存为另外一张图片。其实其内部原理是先创建一张原图的副本,然后再副本图片上进行用户编辑操作,所以最后保存的时候是新的一张图片。
此外,直接加载的bitmap对象是只读的,无法修改,要修改图片只能在内存中创建出一个一模一样的bitmap副本,然后修改副本。
首先,创建布局,有两个ImageView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<ImageView
android:id="@+id/iv_src"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
LinearLayout>
ScrollView>
LinearLayout>
Activity中,创建图片副本,修改副本图片,并显示到另一个ImageView上。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv_copy = (ImageView) findViewById(R.id.iv_copy);
ImageView iv_src = (ImageView) findViewById(R.id.iv_src);
Bitmap srcBitmap = BitmapFactory.decodeFile("mnt/sdcard/meinv.png");
iv_src.setImageBitmap(srcBitmap);
//创建一个和原图大小一样,并且图片参数一样的空白bitmap
Bitmap copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(),srcBitmap.getHeight(), srcBitmap.getConfig());
//创建画布对象
Canvas canvas = new Canvas(copyBitmap);
//创建画笔对象
Paint paint = new Paint();
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
//将拷贝图片的bitmap对象上坐标(20,30)点设置成红色
copyBitmap.setPixel(20,30, Color.RED);
//将创建的拷贝的bitmap对象设置到另一个ImageView上
iv_copy.setImageBitmap(copyBitmap);
}
}
Matrix matrix = new Matrix();
//让矩阵旋转30度
matrix.setRotate(30);
//参数1表示旋转的角度,参数2和参数3是旋转的中心点
matrix.setRotate(30, copyBitmap.getWidth()/2, copyBitmap.getHeight()/2);
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_copy.setImageBitmap(copyBitmap);
Matrix matrix = new Matrix();
matrix.setTranslate(20, 0);
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_copy.setImageBitmap(copyBitmap);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_copy.setImageBitmap(copyBitmap);
Matrix matrix = new Matrix();
//设置缩放
matrix.setScale(-1.0f, 1.0f);
//设置位移
matrix.postTranslate(copyBitmap.getWidth(), 0);
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_copy.setImageBitmap(copyBitmap);
注意:setXXX方法每次修改都是最新的的操作,会覆盖上一次操作,post是在上一次修改的基础上继续修改。
Matrix matrix = new Matrix();
matrix.setScale(1.0f, -1.0f);
matrix.postTranslate(0, copyBitmap.getHeight());
//调用画布对象Canvas的drawBitmap()方法将原图的Bitmap画到画布上,参数1表示原图的bitmap,参数2表示矩阵,参数3表示画笔
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_copy.setImageBitmap(copyBitmap);
本案例模拟实现安卓版的美图秀秀中的功能,拖动SeekBar,图片的颜色值随着变化。
界面效果:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="青------红" />
<SeekBar
android:id="@+id/sb_red"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="紫------绿" />
<SeekBar
android:id="@+id/sb_green"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="黄------蓝" />
<SeekBar
android:id="@+id/sb_blue"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
LinearLayout>
MainActivity中初始化控件,给SeekBar设置监听并且创建原图的一个副本:
public class MainActivity extends Activity implements OnSeekBarChangeListener {
private Paint paint;
private Canvas canvas;
private Bitmap srcBitmap;
private ImageView iv;
private Bitmap copyBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
SeekBar sb_blue = (SeekBar) findViewById(R.id.sb_blue);
SeekBar sb_green = (SeekBar) findViewById(R.id.sb_green);
SeekBar sb_red = (SeekBar) findViewById(R.id.sb_red);
sb_blue.setOnSeekBarChangeListener(this);
sb_green.setOnSeekBarChangeListener(this);
sb_red.setOnSeekBarChangeListener(this);
srcBitmap = BitmapFactory.decodeFile("mnt/sdcard/meinv.png");
copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
canvas = new Canvas(copyBitmap);
paint = new Paint();
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
iv.setImageBitmap(copyBitmap);
}
}
实现SeekBar监听:
//当进度改变的时候调用
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
//当开始拖动的时候调用
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
//当停止拖动的时候调用
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int id = seekBar.getId();
//获取当前SeekBar的进度
int progress = seekBar.getProgress();
//创建一个颜色矩阵对象
ColorMatrix cm = new ColorMatrix();
float rf = 0;
float gf = 0;
float bf = 0;
switch (id) {
case R.id.sb_red:
rf = progress / 128.0f;
break;
case R.id.sb_green:
gf = progress / 128.0f;
break;
case R.id.sb_blue:
bf = progress / 128.0f;
break;
}
/**设置颜色矩阵,颜色矩阵的计算公式如下
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
New Red Value = 1*128 + 0*128 + 0*128 + 0*0 + 0
New Blue Value = 0*128 + 1*128 + 0*128 + 0*0 + 0
New Green Value = 0*128 + 0*128 + 1*128 + 0*0 + 0
New Alpha Value = 0*128 + 0*128 + 0*128 + 1*0 + 0
*/
cm.set(new float[] { rf, 0 , 0 , 0, 0,
0, gf, 0 , 0, 0,
0, 0 , bf, 0, 0,
0, 0 , 0 , 1, 0 });
//给Paint对象设置颜色过滤
paint.setColorFilter(new ColorMatrixColorFilter(cm));
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
iv.setImageBitmap(copyBitmap);
}
本案例实现在一块背景上可以画画的功能,类似画画板的功能。
布局界面:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RelativeLayout>
在MainActivity中,创建一个背景的图片副本,让ImageView显示:
public class MainActivity extends Activity {
private Paint paint;
private Canvas canvas;
private ImageView iv;
private Bitmap copyBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.bg);
copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(),srcBitmap.getHeight(), srcBitmap.getConfig());
canvas = new Canvas(copyBitmap);
paint = new Paint();
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
//画一条线,参数1是起点x坐标,参数2是起点y坐标,参数3是终点x坐标,参数4是终点y坐标,参数5是画笔对象。
canvas.drawLine(10, 10, 40, 100, paint);
iv.setImageBitmap(copyBitmap);
}
}
接下来,我们给ImageView设置触摸事件,随着触摸事件在ImageView上画画:
iv.setOnTouchListener(new OnTouchListener() {
int startX = 0;
int startY = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
//调用event.getAction()获取触摸事件类型
int action = event.getAction();
switch (action) {
//MotionEvent.ACTION_DOWN表示按下事件
case MotionEvent.ACTION_DOWN:
//获取当前手指触摸的x和y坐标
startX = (int) event.getX();
startY = (int) event.getY();
break;
//MotionEvent.ACTION_MOVE表示手指移动事件
case MotionEvent.ACTION_MOVE:
//获取当前手指触摸的x和y坐标
int newX = (int) event.getX();
int newY = (int) event.getY();
//实时更改画线的起点坐标
canvas.drawLine(startX, startY, newX, newY, paint);
startX = newX;
startY = newY;
iv.setImageBitmap(copyBitmap);
break;
//MotionEvent.ACTION_UP表示抬起事件
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
});
接下来,实现刷子效果,可以改变画笔颜色:
public void click1(View view) {
//设置画笔粗细
paint.setStrokeWidth(20);
}
public void click2(View view) {
//设置画笔颜色为绿色
paint.setColor(Color.GREEN);
}
public void click3(View view) {
paint.setColor(Color.RED);
}
将图片保存到SD卡:
public void click4(View view){
File file = new File(Environment.getExternalStorageDirectory().getPath(), ”haha.png”);
FileOutputStream fos;
try {
fos = new FileOutputStream(file);
//调用Bitmap对象的compress()方法保存图片,参数1是图片的格式,参数2是图片压缩的质量,参数3是输出流。
copyBitmap.compress(CompressFormat.PNG, 100, fos);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
加入权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
系统每次收到SD卡就绪广播时,都会去遍历SD卡的所有文件和文件夹,把遍历到的所有多媒体文件都在MediaStore数据库保存一个索引,这个索引包含多媒体文件的文件名、路径、大小。
图库每次打开时,并不会去遍历SD卡获取图片,而是通过内容提供者从MediaStore数据库中获取图片的信息,然后读取该图片。
系统开机或者点击挂载SD卡按钮时,系统会发送sd卡就绪广播,我们也可以手动发送挂载SD卡就绪广播。
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
sendBroadcast(intent);
本案例实现撕衣服的效果,手指在图片上滑动,划过的地方将会把衣服去掉。效果图如下:
实现原理:在屏幕上面有两个ImageView,下面的ImageView是没有穿衣服的图片,上面的是穿衣服的。然后创建一个穿衣服的图片的Bitmap副本,设置给上方的ImageView,给上方的ImageView设置触摸监听,当手指移动的时候在副本Bitmap上画透明像素,这样下方的ImageView就显示出来了。
1.布局代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/after19" />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RelativeLayout>
2.具体代码逻辑:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//上方的ImageView
final ImageView iv = (ImageView) findViewById(R.id.iv);
//通过BitmapFactory.decodeResource()方法加载图片
Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pre19);
//创建图片副本
final Bitmap alterBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
Paint paint = new Paint();
Canvas canvas = new Canvas(alterBitmap);
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
//给上方ImageView设置图片副本背景
iv.setImageBitmap(alterBitmap);
//给ImageView设置触摸事件
iv.setOnTouchListener(new OnTouchListener() {
//当手指滑动的时候调用
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
for (int i = -7; i < 7; i++) {
for (int j = -7; j < 7; j++) {
if (Math.sqrt(i * i + j * j) < 7) {
try {
//在图片副本上画透明像素
alterBitmap.setPixel((int) event.getX() + i, (int) event.getY() + j, Color.TRANSPARENT);
} catch (Exception e) {
}
}
}
}
//给ImageView设置修改后的背景,实时刷新
iv.setImageBitmap(alterBitmap);
break;
}
return true;
}
});
}
}
本案例实现点击按钮实现播放音频的功能。点击按钮播放SD卡上的音频文件。播放音频需要使用到MediaPlayer这个api,下图是MediaPlayer的状态图解:
将xiaopingguo.mp3音频文件导入到SD卡中,如下图:
Activity中点击按钮播放音乐:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View v) {
//创建MediaPlayer对象
final MediaPlayer player = new MediaPlayer();
//设置播放的类型
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
//设置播放的数据源
player.setDataSource("/mnt/sdcard/xiaopingguo.mp3");
//player.setDataSource("http://192.168.116.132:8080/xiaopingguo.mp3");
//准备播放
player.prepare();
//如果播放的是网络资源,那么使用prepareAsync()方法来准备播放,因为prepare()方法是阻塞线程的
// player.prepareAsync();
//设置准备监听,当播放准备好后回调onPrepared()方法
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//开始播放
player.start();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
在Activity中播放音频,当按返回键回到Home界面后,由于应用的进程变成了空进程,所以很容易被系统回收。那么怎么解决这个问题呢?可以在Service中操作播放音频,这样就不容易被系统回收。
1.创建布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="click1"
android:text="播放" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="click2"
android:text="暂停" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="click3"
android:text="继续播放" />
<SeekBar
android:id="@+id/sb_control"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
LinearLayout>
2.创建播放音频Service
定义IService接口提供服务中的方法:
public interface Iservice {
public void callPlay();
public void callPause();
public void callReplay();
public void callSeekTo(int position);
}
编写Service类,并在Service中定义操作音频的方法并创建Binder对象:
public class MusicService extends Service {
private MediaPlayer player;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
player = new MediaPlayer();
super.onCreate();
}
@Override
public void onDestroy() {
super.onDestroy();
}
public void play(){
//player.reset()方法用来重置MediaPlayer
player.reset();
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
player.setDataSource("/mnt/sdcard/xiaopingguo.mp3");
player.prepare();
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
//当开始播放时开始更新进度
updateSeekBar();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//定义暂停播放的方法
public void pause(){
player.pause();
}
//定义继续播放的方法
public void rePlay(){
player.start();
}
//定义指定从某个地方开始播放
public void seekTo(int position){
player.seekTo(position);
}
private class MyBinder extends Binder implements Iservice{
@Override
public void callPlay() {
play();
}
@Override
public void callPause() {
pause();
}
@Override
public void callReplay() {
rePlay();
}
@Override
public void callSeekTo(int position) {
seekTo(position);
}
}
}
更新进度条:
private void updateSeekBar() {
//歌曲的总的时长
final int duration = player.getDuration();
//创建Timer定时任务对象
Timer timer = new Timer();
//创建任务对象
TimerTask task = new TimerTask() {
@Override
public void run() {
//getCurrentPosition()获取当前播放的位置
int currentPosition = player.getCurrentPosition();
//创建Message消息对象,将歌曲总长度和当前的位置作为数据存入Message
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putInt("duration", duration);
bundle.putInt("currentPosition", currentPosition);
msg.setData(bundle);
//发送消息
MainActivity.handler.sendMessage(msg);
}
};
//执行定时任务,参数1为任务对象,参数2为多久开始执行任务,参数3为任务执行的间隔时间
timer.schedule(task, 50, 1000);
}
3.Activity中实现播放音频,暂停播放,继续播放和处理更新进度
public class MainActivity extends Activity {
private Myconn myconn;
private Iservice iservice;
private static SeekBar sb_contrller;
//定义Handler,处理进度更新操作
public static Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
Bundle data = msg.getData();
int duration = data.getInt("duration");
int currentPosition = data.getInt("currentPosition");
//设置SeekBar的总进度
sb_contrller.setMax(duration);
//设置SeekBar的当前位置
sb_contrller.setProgress(currentPosition);
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sb_contrller = (SeekBar) findViewById(R.id.sb_control);
//设置SeekBar改变监听
sb_contrller.setOnSeekBarChangeListener(new
OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int position = seekBar.getProgress();
//调用服务中的方法,从SeekBar的拖动到的位置开始播放
iservice.callSeekTo(position);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,boolean fromUser) {
}
});
Intent intent = new Intent(this, MusicService.class);
//先调用startService()让服务能一直运行,再调用bindService()方法使Activity可以调用服务中的方法
startService(intent);
myconn = new Myconn();
bindService(intent, myconn, BIND_AUTO_CREATE);
}
public void click1(View v) {
iservice.callPlay();
}
public void click2(View v) {
iservice.callPause();
}
public void click3(View v) {
iservice.callReplay();
}
private class Myconn implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
iservice = (Iservice) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
@Override
protected void onDestroy() {
unbindService(myconn);
super.onDestroy();
}
}
本案例实现播放SD卡中的一个mp4文件的视频。实现该功能需要用到MediaPlayer中的api,此外还需要SurfaceView来显示播放的视频。
SurfaceView是用来播放视频的控件,使用了双缓冲技术:内存中有两个画布,A画布显示至屏幕,B画布在内存中绘制下一帧画面,绘制完毕后B显示至屏幕,A在内存中继续绘制下一帧画面。
首先创建布局,布局中使用SurfaceView:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<SurfaceView
android:id="@+id/sv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RelativeLayout>
在MainActivity中实现播放:
public class MainActivity extends Activity {
private SurfaceView sfv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sfv = (SurfaceView) findViewById(R.id.sfv);
}
public void click(View v) {
new Thread() {
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
final MediaPlayer player = new MediaPlayer();
try {
player.setDataSource("http://192.168.116.132:8080/test.mp4");
player.prepareAsync();
//获取SurfaceHolder对象
SurfaceHolder holder = sfv.getHolder();
//给MediaPlayer设置holder对象
player.setDisplay(holder);
//设置准备好的监听,当准备好了之后调开始播放
player.setOnPreparedListener(new OnPreparedListener(){
@Override
public void onPrepared(MediaPlayer mp) {
//开始播放
player.start();
}
});
} catch (Exception e) {
e.printStackTrace();
}
};
}.start();
}
}
这是由于SurfaceView是重量级组件,对画面的实时更新要求较高。我们查看SurfaceView的API文档,如下图:
文档需要我们实现两个回调方法,给SurfaceHolder设置CallBack,通过回调可以知道SurfaceView的状态,SurfaceView一旦不可见,就会被销毁,一旦可见,就会被创建,销毁时停止播放,再次创建时再开始播放。
SurfaceHolder holder = sv.getHolder();
holder.addCallback(new Callback() {
//当SurfaceView销毁时调用
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//记录SurfaceView不可见的时候播放的位置
lastPosition = player.getCurrentPosition();
//判断player是否为空或者是否正在播放,如果不为空并且正在播放,需要将player暂停
if (player != null && player.isPlaying()) {
player.pause();
}
}
//当SurfaceView创建的时候调用,可以在这个方法中,创建MediaPlayer对象和准备工作等操作
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
player = new MediaPlayer();
player.setDataSource("http://192.168.116.132:8080/test.mp4");
player.setDisplay(holder);
player.prepareAsync();
player.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
//调用MediaPlayer的seekTo()方法从指定位置播放,比如当我们按Home键,下次进入的时候需要从之前的位置播放
player.seekTo(lastPosition);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//当SurfaceView改变的时候调用
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
});
VideoView也是播放视频的一个控件,这个类继承了SurfaceView。
界面布局:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
RelativeLayout>
Activity中利用VideoView播放视频:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
VideoView vv = (VideoView) findViewById(R.id.videoView);
vv.setVideoPath("http://192.168.116.132:8080/test.mp4");
vv.start();
}
}
由于系统原生的SurfaceView和VideoView对视频播放功能的不完整,所以现实开发中,开发人员很少使用这两个控件。开发中一般都会使用开源框架来实现播放视频的功能,因为开源框架支持播放视频的格式比较多,而且功能比较强大。Vitamio是一款Android与iOS平台上的全能多媒体开发框架,它的官网地址是https://www.vitamio.org。下图是Vitamio的官网首页:
下面使用Vitamio框架播放视频。Vitamio开源框架是以类库的方式提供给开发者的,实际上就是一个安卓工程,我们将Vitamio类库导入到Eclipse,右击查看项目属性可以发现它是一个类库,如下图:
那么我们的项目如何引入Vitamio类库呢?右击项目选择Properties,如下图:
这时候项目中就可以使用vitamio中的api了。
首先我们在布局中使用vitamio提供的播放视频的控件:
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
.vov.vitamio.widget.VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
在MainActivity中使用VideoView播放视频:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//检查类库
if (!LibsChecker.checkVitamioLibs(this)) {
return;
}
final VideoView vv = (VideoView) findViewById(R.id.vv);
//设置播放文件资源路径
vv.setVideoPath("http://192.168.116.132:8080/test.mp4");
vv.setOnPreparedListener(new OnPreparedListener() {
//设置准备监听,当准备好了之后才可以播放
@Override
public void onPrepared(MediaPlayer mp) {
//开始播放
vv.start();
}
});
//设置控制器
vv.setMediaController(new MediaController(getApplicationContext()));
}
}
注意,最后还需要在清单文件中配置一个Activity:
<activity android:name="io.vov.vitamio.activity.InitActivity">activity>
我们可以隐式的开启系统提供的照相Activity,通过系统照相功能进行拍照。
//创建意图对象
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//创建照片保存的File对象
File file = new File(Environment.getExternalStorageDirectory(),"paizhao.png");
//设置文件保存的Uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
//开启Activity
startActivityForResult(intent, 0);
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory(),"luxiang.3gp");
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
//设置视频的质量
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
startActivityForResult(intent, 0);