加载大图片
1. 原理
Android 虚拟机默认为每个应用分配的堆内存是16M,当在界面显示图片时,需要的内存空间不是按图片的实际大小来计算的,而是按像素点的多少乘以每个像素点占用的空间大小来计算的。图片加载到内存中需要把每一个像素都加载到内存中. 所以对内存的要求非常高, 一不小心就会造成OOM(OutOfMemoryError)错误。下面通过一组计算来演示OOM 为什么会发生,以及解决方案。
假设:当前有一张图片,大小仅为1M,但是其规格(图片的像素)为3648*2736,那么完全加载此图片的像素数=3648*2736=9980928。在Android 中像素的表现形式有三种模式:
三种像素如下:
l ARGB_4444 : 2bytes 每个像素占据2 个字节
l ARGB_8888 : 4bytes 每个像素占据4 个字节
l RGB_565 : 4bytes 每个像素占据4 个字节
假设采用ARGB_4444 模式作为像素的表现形式,则该1M 大小的图片在Android 中占用的总空间为:
图片占用空间=总像素数*像素的单位
=9980928 * 2bytes
=19961856bytes
=19M>16M
通过计算大家发现就算采用每个像素占用2 个字节的形式该1M 的图片也需要分配19M 的空间才能100%地将所有像素表现出来,这肯定会导致OOM 的发生。
解决方案:
通过代码可以对图片进行按比例(之所以按照比例缩放是为了避免图片的畸变,比如一张图片长度缩小一半,那么高度也缩小一半就保证了图片不会畸变)缩放,这也是必须要进行的工作。
假设:
图片的宽和高: 3648 * 2736
屏幕的宽和高: 320 * 480
计算缩放比:
宽度缩放比例: 3648 / 320 = 11
高度缩放比例: 2736 / 480 = 5
比较宽和高的缩放比例,哪一个大用哪一个进行缩放,因此我们采用11 作为该图片长和高的缩放比例。
计算缩放后的图片的宽和高:
宽=3648 / 11 = 331
高=2736 / 11 = 248
缩放后图片的宽和高: 331* 248
计算缩放后的图片在Android 中占有的空间:
总空间=331* 248*像素点大小=882088 * 2bytes=160K
最终计算结果是160K,远远小于堆内存的16M 上限,从而解决了OOM 的发生。
2. 实现加载大图片
准备工作
请自己提前准备好一张分辨率比较大的图片,将该图片放到模拟器的sdcard 根目录下。然后创建Android 工程“加载大图片到内存”。
具体实现
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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="loadImage"
android:text="加载显示图片" />
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
2) 主界面MainActivity代码实现
public class MainActivity extends Activity {
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
}
public void loadImage(View view) {
try {
// 1.第一种方式得到图片的宽高信息.注意!ExifInterface只能读取JPEG的图片
// ExifInterface exif = new ExifInterface("mnt/sdcard/big.jpg");
// int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH,
// 0);
// int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH,
// 0);
// System.out.println("widtH:"+width);
// System.out.println("height:"+height);
// 第一种方式获取屏幕的宽高
// DisplayMetrics dm = getResources().getDisplayMetrics();
// int h = dm.heightPixels;
// int w = dm.widthPixels;
// System.out.println("屏幕w:" + w);
// System.out.println("屏幕h:" + h);
// 第二种方式获取屏幕的宽高
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
int screenWidth = wm.getDefaultDisplay().getWidth();
int screenHeight = wm.getDefaultDisplay().getHeight();
System.out.println("屏幕width:" + screenWidth);
System.out.println("屏幕height:" + screenHeight);
//创建一个可选项对象,该对象用于配置图片的处理参数
Options opts = new Options();
// 将该参数设置为true 则加载器不加载图片, 而是把图片的out(宽和高)的字段信息取出来
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile("mnt/sdcard/big.jpg", opts);
//第二种方法获取图片的宽高(实际开发中常用)
int outWidth = opts.outWidth;
int outHeight = opts.outHeight;
// 计算缩放比例
int widthScale = outWidth / screenWidth;
int heightScale = outHeight / screenHeight;
//计算出最大的比例
int scale = widthScale > heightScale ? widthScale : heightScale;
System.out.println("scale:"+scale);
// 使用缩放比例进行缩放加载图片
opts.inJustDecodeBounds = false;// 加载器就会返回图片了
opts.inSampleSize = scale;
Bitmap bitmap = BitmapFactory
.decodeFile("mnt/sdcard/big.jpg", opts);
//显示在屏幕上
iv.setImageBitmap(bitmap);
} catch (Exception e) {
e.printStackTrace();
}
}
注意:
在获取屏幕宽高和图片的宽高上都给出了两种方式去实现,屏幕的宽高可以随便选一种方式去实现,但是图片的宽高最好按照第二种方式去获取,因为并不是所有的图片都是JPG格式,所以为了兼容大部分图片我们最好通过设置opts.inJustDecodeBounds = true的方式,这样我们通过BitmapFactory.decodeFile("mnt/sdcard/big.jpg", opts);就可以将图片的宽高信息提取出来,但是图片本身不会加载到内存,这时我们可以去计算图片的宽高和屏幕的宽高比例,得到最大的比例,来进行缩放图片。
图片特效处理
图片的特效包括图形的缩放、镜面、倒影、旋转、平移等。图片的特效处理方式是将原图的图形矩阵乘以一个特效矩阵,形成一个新的图形矩阵来实现的。
矩阵Matrix 类,维护了一个3*3 的矩阵去更改像素点的坐标。
Android 手机的屏幕坐标系如下图所示。横轴是X 坐标轴,从左往右变大,纵轴是Y 坐标轴,从上往下变大,坐标原点位于屏幕的左上角,用(0,0)表示。
图形的矩阵用数组表示为:
{ 1, 0, 0, 第一行表示像素点的x 坐标:x = 1*x + 0*y + 0*z
0, 1, 0, 第二行表示像素点的y 坐标:y = 0*x + 1*y + 0*z
0, 0, 1 } 第三行表示像素点的z 坐标:z = 0*x + 0*y + 1*z
图片的特效处理正是通过更改图形矩阵的值来实现的,下面分别给出各种特效实现的代码,在代码中会有详细的注释。
不过在android下Matrix这个类帮我们封装了矩阵的一些基本用法,所以我们可以直接使用即可。
图片缩放
/**
* 处理一张照片,对照片进行缩放操作
*
* @param view
*/
public void processImage(View view) {
// 原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/b.jpg");
iv_src.setImageBitmap(srcBitmap);
// 缩放后 显示一个缩放后的图片 在iv_dest
// 用代码编辑图片,最好处理都是图片在内存中的拷贝,不去处理原图.
// 1.创建一个空白的bitmap,宽高信息和原图保存一致.
Bitmap copyedBitmap = Bitmap.createBitmap(srcBitmap.getWidth(),
srcBitmap.getHeight(), srcBitmap.getConfig());
// 2.临摹.创建一个画板
Canvas canvas = new Canvas(copyedBitmap);
// 3.创建画笔
Paint paint = new Paint();
paint.setColor(Color.BLACK);
// 4.作画 matrix:矩阵
Matrix matrix = new Matrix();// 按照1:1的比例作画
matrix.setScale(0.6f, 0.6f);// 缩放 0.6
// 矩阵值
// float[] values = new float[] {
// 0.5f, 0, 0, // x=0.5*x+0*y+0*z
// 0, 0.5f, 0, // y=0*x+0.5*y+0*z
// 0, 0, 1 };// z=0*x+0*y+1*z
// matrix.setValues(values);
// canvas 画布 对照着原图在画纸上面画出一模一样的样子出来
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_dest.setImageBitmap(copyedBitmap);
}
图片平移
/**
* 处理一张照片,对照片进行平移操作
* @param view
*/
public void processImage(View view){
//原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/b.jpg");
iv_src.setImageBitmap(srcBitmap);
//缩放后 显示一个缩放后的图片 在iv_dest
//用代码编辑图片,最好处理都是图片在内存中的拷贝,不去处理原图.
Bitmap copyedBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
//临摹.创建一个画板
Canvas canvas = new Canvas(copyedBitmap);
//创建画笔
Paint paint = new Paint();
paint.setColor(Color.BLACK);
//作画
Matrix matrix = new Matrix();//按照1:1的比例作画
//x,y方向平移100个像素
matrix.setTranslate(100, 100);
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_dest.setImageBitmap(copyedBitmap);
}
注意:
平移后我们发现图片的底部和右侧部分已经看不见了,这是因为我们让这个图片平移出了当前的ImageView 的范围,ImageView 的位置是固定不变的,而ImageView 上的图片平移了,从而导致了这样效果的发生。
图片旋转
/**
* 处理一张照片,对照片进行旋转操作
* @param view
*/
public void processImage(View view){
//原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/tu1.jpg");
iv_src.setImageBitmap(srcBitmap);
//缩放后 显示一个缩放后的图片 在iv_dest
//用代码编辑图片,最好处理都是图片在内存中的拷贝,不去处理原图.
Bitmap copyedBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
//临摹.创建一个画板
Canvas canvas = new Canvas(copyedBitmap);
//创建画笔
Paint paint = new Paint();
paint.setColor(Color.BLACK);
//作画
Matrix matrix = new Matrix();//按照1:1的比例作画
//第一个参数是旋转多少角度 第二,三个参数是以x,y方向上的某一个坐标为中心点
matrix.setRotate(180, srcBitmap.getWidth()/2, srcBitmap.getHeight()/2);
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_dest.setImageBitmap(copyedBitmap);
}
图片镜面
/**
* 处理一张照片,对照片进行镜面操作
* @param view
*/
public void processImage(View view){
//原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/b.jpg");
iv_src.setImageBitmap(srcBitmap);
//缩放后 显示一个缩放后的图片 在iv_dest
//用代码编辑图片,最好处理都是图片在内存中的拷贝,不去处理原图.
Bitmap copyedBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
//临摹.创建一个画板
Canvas canvas = new Canvas(copyedBitmap);
//创建画笔
Paint paint = new Paint();
paint.setColor(Color.BLACK);
//作画
Matrix matrix = new Matrix();//按照1:1的比例作画
//镜面成像以后,图片x轴全为负数,跑出了屏幕范围,
matrix.setScale(-1, 1);
//为了看到效果把图像往x轴正方向移动一个图片的宽度
matrix.postTranslate(srcBitmap.getWidth(), 0);
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_dest.setImageBitmap(copyedBitmap);
}
注意:
上面的镜面效果做了两个工作,第一个是将x 坐标都变成负值,比如原来是10,转换后改为-10,而y坐标不变。由于x 都变成了负数,因此转变后的图片肯定全部跑出原ImageView 范围了,因此需要将图片再做个平移操作,因此需要调用如下一行代码:
matrix.postTranslate(bm.getWidth(), 0);
让转换后的图片往x 的正方向再平移一个图片的宽度。
图片倒影
/**
* 处理一张照片,对照片进行倒影操作
* @param view
*/
public void processImage(View view){
//原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/b.jpg");
iv_src.setImageBitmap(srcBitmap);
//缩放后 显示一个缩放后的图片 在iv_dest
//用代码编辑图片,最好处理都是图片在内存中的拷贝,不去处理原图.
Bitmap copyedBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
//临摹.创建一个画板
Canvas canvas = new Canvas(copyedBitmap);
//创建画笔
Paint paint = new Paint();
paint.setColor(Color.BLACK);
//作画
Matrix matrix = new Matrix();//按照1:1的比例作画
matrix.setScale(1, -1);
matrix.postTranslate(0, 100);
canvas.drawBitmap(srcBitmap, matrix, paint);
iv_dest.setImageBitmap(copyedBitmap);
}
案例-随手涂鸦
实现原理
在Android 中可以给View 控件设置触摸事件监听,这些触摸事件包括用户点击下去(ACTION_DOWN),移动(ACTION_MOVE)和抬起(ACTION_UP)。随手涂鸦就是给ImageView(所有的控件都是View 的子类,都具备触摸监听功能)设置触摸监听事件,然后在ImageView 控件上进行绘制线条操作。
当用户ACTION_DOWN 的时候获取当前的x 和y 坐标作为要绘制线条的起始坐标,当ACTION_MOVE(该事件只要用户在滑动就不断的被调用)的时候不断的获取新的x 和y 坐标,将新的坐标作为要绘制线条的结束坐标就可以绘制一条直线,然后将结束坐标再作为新线条的起始坐标,依次类推,
就能绘制出无限多个小直线,这些直线最终看起来就像是曲线的效果了。
最后可以将绘制好的图片保存到本地文件中。
布局实现
整个界面布局分为三块,第一块可以让用户选择画笔的颜色,第二块可以设置画笔的粗细,通过一个seekbar来展示,第三块即为剩余的空间,设置为一个ImageView,用来显示绘制时的图片。
实现后的效果图如下:
布局XML实现:
<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="请选择画笔的颜色" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<View
android:id="@+id/red"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="#ff0000" />
<View
android:id="@+id/green"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="#00ff00" />
<View
android:id="@+id/blue"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="#0000ff" />
<View
android:id="@+id/yellow"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="#ffff00" />
<View
android:id="@+id/purple"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="#ff00ff" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请设置画笔的粗细" />
<SeekBar
android:id="@+id/seekbar"
android:max="100"
android:progress="50"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv"
android:layout_width="320dip"
android:layout_height="320dip" />
</LinearLayout>
代码实现
public class MainActivity extends Activity implements OnClickListener {
private View red, green, yellow, purple, blue;
private SeekBar seekbar;
private Paint paint;
private ImageView iv;
/**
* 一个可以被修改的图片
*/
private Bitmap alterBitmap;
/**
* 画板
*/
private Canvas canvas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 创建一个空白的图片
alterBitmap = Bitmap.createBitmap(320, 320, Bitmap.Config.ARGB_8888);
canvas = new Canvas(alterBitmap);
paint = new Paint();
// 设置画笔的颜色
paint.setColor(Color.BLACK);
canvas.drawColor(Color.WHITE);
// 设置画笔的宽度
paint.setStrokeWidth(5);
seekbar = (SeekBar) findViewById(R.id.seekbar);
seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int progress = seekBar.getProgress();
paint.setStrokeWidth(progress);
Toast.makeText(MainActivity.this, "画笔宽度为:" + progress, 0)
.show();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
});
red = findViewById(R.id.red);
red.setOnClickListener(this);
green = findViewById(R.id.green);
green.setOnClickListener(this);
yellow = findViewById(R.id.yellow);
yellow.setOnClickListener(this);
purple = findViewById(R.id.purple);
purple.setOnClickListener(this);
blue = findViewById(R.id.blue);
blue.setOnClickListener(this);
iv = (ImageView) findViewById(R.id.iv);
// 给imageview的控件注册一个触摸事件
iv.setOnTouchListener(new OnTouchListener() {
int startX;
int startY;
// 当imageview被触摸的时候调用的方法.
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 按下
System.out.println("摸到");
startX = (int) event.getX();
startY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:// 移动
System.out.println("移动");
int newX = (int) event.getX();
int newY = (int) event.getY();
canvas.drawLine(startX, startY, newX, newY, paint);
iv.setImageBitmap(alterBitmap);
// 记得重新初始化手指在屏幕上的坐标
startX = (int) event.getX();
startY = (int) event.getY();
break;
case MotionEvent.ACTION_UP:// 离开
System.out.println("放手");
break;
}
return true;// false代表的是事件没有处理完毕,等待事件处理完毕, true代表事件已经处理完毕了.
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.red:
paint.setColor(Color.RED);
Toast.makeText(MainActivity.this, "画笔红色", 0).show();
break;
case R.id.yellow:
paint.setColor(Color.YELLOW);
Toast.makeText(MainActivity.this, "画笔黄色", 0).show();
break;
case R.id.green:
paint.setColor(Color.GREEN);
Toast.makeText(MainActivity.this, "画笔绿色", 0).show();
break;
case R.id.purple:
paint.setColor(0xffff00ff);
Toast.makeText(MainActivity.this, "画笔紫色", 0).show();
break;
case R.id.blue:
paint.setColor(Color.BLUE);
Toast.makeText(MainActivity.this, "画笔蓝色", 0).show();
break;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_save) {
// 保存图片
try {
File file = new File(Environment.getExternalStorageDirectory()+"/itheima");
if(!file.exists()){
file.mkdir();
}
File f = new File(file.getAbsoluteFile()+"/"+SystemClock.currentThreadTimeMillis() +".jpg");
FileOutputStream stream = new FileOutputStream(f);
alterBitmap.compress(CompressFormat.JPEG, 100, stream);
stream.close();
Toast.makeText(this, "保存成功", 0).show();
//模拟一个sd卡插入广播.
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
//intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
intent.setData(Uri.fromFile(file));
sendBroadcast(intent);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "保存失败", 0).show();
}
}
if(item.getItemId() == R.id.item_clear){
Toast.makeText(this, "清除画布", 0).show();
//画布清空
canvas.drawColor(Color.WHITE);
//控件图片清空
iv.setImageBitmap(null);
}
return super.onOptionsItemSelected(item);
}
}
注意:
上面的代码中实现了onCreateOptionsMenu这个方法是为了按菜单按钮的时候可以显示出菜单栏供用户点击;onOptionsItemSelected这个方法是可以监听那个menu item被点击了然后实现对应的点击事件;这里我添加了两个操作,一个是保存图片,一个是清除画布的操作。
保存图片的操作需要发送广播通知系统去刷新对应保存图片的位置,比如一般保存在sdcard里面,这里建议大家在sdcard单独创建一个自己应用对应的文件夹进行图片的保存。发送广播的时候注意设置对应的文件夹数据刷新,避免每次让系统去扫描遍历sdcard下的图片,如果图片越多扫描的时间就越多。
案例-撕衣服游戏
实现原理
使用帧布局叠加2 个ImageView,每个ImageView 负责显示一张图片,一张图片有衣服,一张图片没有衣服,没有衣服的图片放置在下面,有衣服的图片放置在上面。给上面的ImageView 设置触摸的事件,当手指触摸到图片上时,将手指触摸到的点周边的图片的像素点设置为透明的,这样下面的图片就一点一
点显示出来了,从而有一种“撕衣服”的感觉。
在编写该案例的时候应该注意的事项如下:
1.触摸事件onTouch 的返回值必须设置为true,否则触摸的事件将不被处理
2.使用BitmapFactory 的decodeResouces 方法得到的图片是没有透明度的,即图片格式为RGB_565,所以若想能够修改透明度,需要使用Canvas 对象对图片进行重绘,重新绘制的图片格式采用ARGB。
3.加载图片时需要对其进行一下压缩,防止图片与控件大小不匹配,导致触摸时点对不上,达不到触摸哪里就设置哪里的像素点透明的效果。一般图片宽高不超过屏幕宽高即可。
具体实现
布局XML:
<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_after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/after" />
<ImageView
android:id="@+id/iv_pre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
/>
</RelativeLayout>
MainActivity代码实现:
public class MainActivity extends Activity {
private ImageView iv_pre;
private Bitmap alterBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv_pre = (ImageView) findViewById(R.id.iv_pre);
// 1.准备原图bitmap.创建一个大小配置一样的空图片alterBitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.pre);
alterBitmap = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), bitmap.getConfig());
//2.创建一个跟图片一样大小的画板,以及画笔
Canvas canvas = new Canvas(alterBitmap);
canvas.drawColor(android.R.color.transparent);
Paint paint = new Paint();
//3.作画.按照原图的样子绘画在画板上
canvas.drawBitmap(bitmap, new Matrix(), paint);
//4.将绘制完成的alterBitmap显示给用户
iv_pre.setImageBitmap(alterBitmap);
//5.注册touch监听事件,按下或移动的时候去改变alterBitmap的像素透明度
iv_pre.setOnTouchListener(new OnTouchListener() {
int x;
int y;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getX();
y = (int) event.getY();
for(int i=-6;i<7;i++){
for(int j=-6;j<7;j++){
if(Math.sqrt(i*i+j*j)<=6){
try {
alterBitmap.setPixel(x+i, y+j, Color.TRANSPARENT);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
iv_pre.setImageBitmap(alterBitmap);
break;
case MotionEvent.ACTION_MOVE:
x = (int) event.getX();
y = (int) event.getY();
for(int i=-6;i<7;i++){
for(int j=-6;j<7;j++){
if(Math.sqrt(i*i+j*j)<=6){
try {
alterBitmap.setPixel(x+i, y+j, Color.TRANSPARENT);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
iv_pre.setImageBitmap(alterBitmap);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;// 事件结束被消费掉了
}
});
}
}
运行上面的代码后初始效果和擦掉一部分像素后的图如下图所示:
音乐播放器
Android 官方提供了MediaPlayer 核心类,用于播放音乐,其状态流程如下图所示。MediaPlayer 必须严格按照状态图操作,否则就会出现错误,这些错误都是底层抛出,严格按照状态图操作的话一般就不会出问题。
使用MediaPlayer 播放音乐的核心方法如下所示:
1. MediaPlayer player = new MediaPlayer(); 创建对象
2. player.reset(); 重置为初始状态
3. player.setAudioStreamType(AudioManager.STREAM_MUSIC);声音流类型
4. player.setDataSource(“/mnt/sdcard/test.mp3”); 设置音频源
5. player.prepare(); 准备
6. player.start(); 开始或恢复播放
7. player.pause(); 暂停播放
8. player.start(); 恢复播放
9. player.stop(); 停止播放
10. player.release(); 释放资源
流程图如下:
下面通过一段代码来演示如何播放网络上的音乐,进行异步加载
界面布局XML:
<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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<EditText
android:id="@+id/et_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入音乐文件的路径" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:layout_weight="1"
android:onClick="play"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:text=">" />
<Button
android:layout_weight="1"
android:onClick="pause"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:text="||" />
<Button
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="stop"
android:text="口" />
</LinearLayout>
</LinearLayout>
MainActivity代码实现:
public class MainActivity extends Activity {
private EditText et_path;
private MediaPlayer mediaPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_path = (EditText) findViewById(R.id.et_path);
}
public void play(View view) {
try {
mediaPlayer = new MediaPlayer();
final ProgressDialog pd = new ProgressDialog(this);
pd.setMessage("正在缓冲...");
//将mediaPlayer设置为未初始化状态,设置完后必须得重新设置数据源以及进行prepare才能进行播放
mediaPlayer.reset();
//设置播放的文件
mediaPlayer.setDataSource(et_path.getText().toString().trim());
//mediaPlayer.prepare();//同步的准备 在主线程中
mediaPlayer.prepareAsync();//异步的准备,开启子线程去准备
pd.show();
mediaPlayer.setOnErrorListener(new OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Toast.makeText(MainActivity.this, "播放失败,错误代码:"+what, 0).show();
return false;
}
});
mediaPlayer.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
pd.dismiss();
mediaPlayer.start();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public void pause(View view) {
if(mediaPlayer!=null&&mediaPlayer.isPlaying()){
mediaPlayer.pause();
return;
}
if(mediaPlayer!=null){
mediaPlayer.start();
}
}
public void stop(View view) {
if(mediaPlayer!=null){
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
}
}
跟之前我们实现的音乐播放器代码一致,主要是大家要明白播放器使用的那张流程图,忘记了对照着流程图走一遍梳理自己的思路即可将播放器的知识给掌握了。
SoundPool
如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:
1) 延时时间较长,且资源占用率高。
2) 不支持多个音频同时播放。
Android中除了MediaPlayer播放音频之外还提供了SoundPool来播放音效,SoundPool使用音效池的概念来管理多个短促的音效,例如它可以开始就加载20个音效,以后在程序中按音效的ID进行播放。
SoundPool主要用于播放一些较短的声音片段,与MediaPlayer相比,SoundPool的优势在于CPU资源占用量低和反应延迟小。另外,SoundPool还支持自行设置声音的品质、音量、 播放比率等参数。
SoundPool提供了一个构造器,该构造器可以指定它总共支持多少个声音(也就是池的大小)、声音的品质等。构造器如下:
SoundPool(int maxStreams, int streamType, int srcQuality):第一个参数指定支持多少个声音;第二个参数指定声音类型:第三个参数指定声音品质。一旦得到了SoundPool对象之后,接下来就可调用SoundPool的多个重载的load方法来加载声音了。
SoundPool提供了如下4个load方法:
l int load(Context context, int resld, int priority):从 resld 所对应的资源加载声音。
l int load(FileDescriptor fd, long offset, long length, int priority):加载 fd 所对应的文件的offset开始、长度为length的声音。
l int load(AssetFileDescriptor afd, int priority):从afd 所对应的文件中加载声音。
l int load(String path, int priority):从path 对应的文件去加载声音。
上面4个方法中都有一个priority参数,该参数目前还没有任何作用,Android建议将该 参数设为1,保持和未来的兼容性。
上面4个方法加载声音之后,都会返回该声音的的ID,以后程序就可以通过该声音的ID 来播放指定声音。
SoundPool提供的播放指定声音的方法:
int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate):该方法的第一个参数指定播放哪个声音;leftVolume、rightVolume指定左、右的音量:priority指定播放声音的优先级,数值越大,优先级越高;loop指定是否循环,0为不循环,-1为循环;rate指定播放的比率,数值可从0.5到2, 1为正常比率。
为了更好地管理SoundPool所加载的每个声音的1D,程序一般会使用一个HashMap对象来管理声音。
归纳起来,使用SoundPool播放声音的步骤如下:
1) 调用SoundPool的构造器创建SoundPool的对象。
2) 调用SoundPool对象的load()方法从指定资源、文件中加载声音。最好使用HashMap< Integer, Integer>来管理所加载的声音。
3) 调用SoundPool的play方法播放声音。
示例代码如下:
public class MainActivity extends Activity {
private SoundPool soundPool;
private int soundID;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//得到一个可以容纳3个声音的音频池
soundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0);
//加载对应资源下的音频文件
soundID = soundPool.load(this, R.raw.shoot, 1);
}
public void click(View view){
//播放音乐
soundPool.play(soundID, 1.0f, 1.0f, 0, 0, 1.0f);
}
}
视频播放器
VedioView
VideoView 跟MediaPlayer 相比播放视频步骤要简单的多,因为VideoView 原生提供了播放,暂停、快进、快退、进度条等方法。使用起来要方便的很多。
1. 在布局文件XML中直接使用VedioView即可
<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" >
<VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
2. MainActivity中初始化控件播放视频
public class MainActivity extends Activity {
private VideoView vv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
vv = (VideoView) findViewById(R.id.vv);
//设置视频的数据源 路径
vv.setVideoPath("/mnt/sdcard/oppo.3gp");
//MediaController 视频播放器的控制器 倒退,暂停,快进,进度条
MediaController mc = new MediaController(MainActivity.this);
//设置控制器 控制的是那一个videoview
mc.setAnchorView(vv);
//设置videoview的控制器为mc
vv.setMediaController(mc);
vv.start();
}
}
VedioView的使用步骤比较简单,不需要死记硬背,忘记的时候参考官网API或者百度即可快速实现一个播放视频的需求。不过系统自带的VedioView支持播放的视频格式较少,一般只有mp4,3GP可以支持,常见的AVI,WMV等格式都不支持,所以如果需要支持全面,需要引入三方的框架(可在GithHub上挑选)。
SurfaceView
下面的Android官网的翻译:
SurfaceView是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的Surface。你可以控制这个Surface的格式和尺寸。Surfaceview控制这个Surface的绘制位置。
surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。surfaceview提供了一个可见区域,只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面 有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
你可以通过SurfaceHolder接口访问这个surface,getHolder()方法可以得到这个接口。
surfaceview变得可见时,surface被创建;surfaceview隐藏前,surface被销毁。这样能节省资源。如果你要查看 surface被创建和销毁的时机,可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder)。
surfaceview的核心在于提供了两个线程:UI线程和渲染线程。这里应注意:
1> 所有SurfaceView和SurfaceHolder.Callback的方法都应该在UI线程里调用,一般来说就是应用程序主线程。渲染线程所要访问的各种变量应该作同步处理。
2> 由于surface可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之间有效,所以要确保渲染线程访问的是合法有效的surface。
1. 定义
可以直接从内存或者DMA等硬件接口取得图像数据,是个非常重要的绘图容器。
它的特性是:可以在主线程之外的线程中向屏幕绘图。这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。在游戏开发中多用到SurfaceView,游戏中的背景、人物、动画等等尽量在画布canvas中画出。
2. 实现
首先继承SurfaceView并实现SurfaceHolder.Callback接口
使用接口的原因:因为使用SurfaceView 有一个原则,所有的绘图工作必须得在Surface 被创建之后才能开始(Surface—表面,这个概念在 图形编程中常常被提到。基本上我们可以把它当作显存的一个映射,写入到Surface 的内容可以被直接复制到显存从而显示出来,这使得显示速度会非常快),而在Surface 被销毁之前必须结束。所以Callback 中的surfaceCreated 和surfaceDestroyed 就成了绘图处理代码的边界。
需要重写的方法
(1)public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在surface的大小发生改变时激发
(2)public void surfaceCreated(SurfaceHolder holder){}
//在创建时激发,一般在这里调用画图的线程。
(3)public void surfaceDestroyed(SurfaceHolder holder) {}
//销毁时激发,一般在这里将画图的线程停止、释放。
整个过程:继承SurfaceView并实现SurfaceHolder.Callback接口 ----> SurfaceView.getHolder()获得SurfaceHolder对象 ---->SurfaceHolder.addCallback(callback)添加回调函数---->SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布----> Canvas绘画 ---->SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。
3. SurfaceHolder
这里用到了一个类SurfaceHolder,可以把它当成surface的控制器,用来操纵surface。处理它的Canvas上画的效果和动画,控制表面,大小,像素等。
几个需要注意的方法:
(1)、abstract void addCallback(SurfaceHolder.Callback callback);
// 给SurfaceView当前的持有者一个回调对象。
(2)、abstract Canvas lockCanvas();
// 锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。
(3)、abstract Canvas lockCanvas(Rect dirty);
// 锁定画布的某个区域进行画图等..因为画完图后,会调用下面的unlockCanvasAndPost来改变显示内容。
// 相对部分内存要求比较高的游戏来说,可以不用重画dirty外的其它区域的像素,可以提高速度。
(4)、abstract void unlockCanvasAndPost(Canvas canvas);
// 结束锁定画图,并提交改变。
SurfaceView+MediaPlayer
由于SurfaceView的特点,所以android系统的vedioView也是用的SurfaceView+MediaPlayer的方式来实现的视频播放,根据这点我们完全可以自己实现一个视频播放器。
1. 布局XML:
<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" >
<SurfaceView
android:id="@+id/sv"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
2. 代码实现
public class MainActivity extends Activity {
private SurfaceView sv;
private MediaPlayer mediaPlayer;
private SharedPreferences sp;
private boolean isPlayCompleted;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sv = (SurfaceView) findViewById(R.id.sv);
sp = getSharedPreferences("config", MODE_PRIVATE);
mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource("/mnt/sdcard/oppo.3gp");
} catch (Exception e1) {
e1.printStackTrace();
}
sv.getHolder().addCallback(new Callback() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
System.out.println("surface被销毁");
if(mediaPlayer!=null){
int position = mediaPlayer.getCurrentPosition();
Editor editor = sp.edit();
if(isPlayCompleted){
editor.putInt("position", 0);
}else{
editor.putInt("position", position);
}
editor.commit();
mediaPlayer.stop();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
System.out.println("surface被创建");
try {
isPlayCompleted = false;
mediaPlayer.prepare();
} catch (Exception e) {
e.printStackTrace();
}
//指定多媒体的内容实在holder里面显示
mediaPlayer.setDisplay(holder);//注意!
mediaPlayer.start();
mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
System.out.println("播放完成");
isPlayCompleted= true;
}
});
mediaPlayer.seekTo(sp.getInt("position", 0));
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
});
}
}
注意:
视频的播放要在surfaceview创建的时候进行播放,结束的时候停止,并记录当前播放的位置,方便下次打开的时候继续播放,同时要注意设置一个是否播放完毕的监听,当播放完毕的时候刚好销毁了surfaceview需要将当前保存的位置重置为0.
调用系统照相机和摄像机功能
调用系统摄像头进行拍照和摄像是通过隐式启动系统Activity 实现的,无需给自己的工程添加权限,直接调用即可。因此我们只需知道系统照相机和摄像机Activity 的action 和category 就可以了。
步骤:
1. 打开Android 源码,查看”\packages\apps\”文件文件目录下的Camera 应用,即系统摄像头的应用程序。打开其清单文件文件,查看其Activity 的action 和category 信息。
2. Camera 类的action 和category 如下:
照相机的意图过滤器
摄像机的意图过滤器
3. 采用隐式调用的方式调用Activity
由于希望在调用拍照或摄像功能后将结果返回到当前应用的Activity,所以在开启Activity 时不能使用startActivity 方法,而是使用startActivityForResult 方法开启Activity,并重写onActivityResult 方法处理回传的数据。
布局文件比较简单,界面只有两个按钮,一个用于打开照相机,一个用于打开摄像机。这里只给出核心代码清单。
public void takePic(View view){
//开启手机的照相机应用拍照获取返回值 隐式意图
// create Intent to take a picture and return control to the calling application
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
file = new File(Environment.getExternalStorageDirectory(),SystemClock.uptimeMillis()+".jpg"); // create a file to save the image
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); // set the image file name
// start the image capture Intent
startActivityForResult(intent, 0);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(file!=null&&file.exists()&&file.length()>0){
System.out.println(file.getAbsolutePath());
ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageURI(Uri.fromFile(file));
}
super.onActivityResult(requestCode, resultCode, data);
}
摄像功能核心代码
public void takePic(View view){
//开启手机的照相机应用拍照获取返回值
// create Intent to take a picture and return control to the calling application
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
file = new File(Environment.getExternalStorageDirectory(),SystemClock.uptimeMillis()+".3gp"); // create a file to save the image
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)); // set the image file name
// start the image capture Intent
startActivityForResult(intent, 0);
}
传感器的使用
使用步骤:
1. 获取SensorManager管理器
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
2. 设置需要获取传感器的type
SENSOR_TYPE_ACCELEROMETER 1 //加速度
SENSOR_TYPE_MAGNETIC_FIELD 2 //磁力
SENSOR_TYPE_ORIENTATION 3 //方向
SENSOR_TYPE_GYROSCOPE 4 //陀螺仪
SENSOR_TYPE_LIGHT 5 //光线感应
SENSOR_TYPE_PRESSURE 6 //压力
SENSOR_TYPE_TEMPERATURE 7 //温度
SENSOR_TYPE_PROXIMITY 8 //接近
SENSOR_TYPE_GRAVITY 9 //重力
SENSOR_TYPE_LINEAR_ACCELERATION 10//线性加速度
Sensor sensor = sm.getDefaultSensor(type);
3. 注册监听
sm.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
private class MyLintener implements SensorEventListener{
//当传感器数据变化的调用的方法
@Override
public void onSensorChanged(SensorEvent event) {
//1 如果是光线传感器,event.values[0] 代表的是光感度
//2 如果是方向传感器,event.values[0]
}
//当传感器精度发生变化的时候调用的方法
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
4. 在退出的时候要记得取消监听
sm.unregisterListener(listener);