1、图片处理-加载大图片
图片总大小 = 图片总像素 * 每个像素的大小
单色图:每个像素占用1/8个字节
16色图:每个像素占用1/2个字节
256色图:每个像素占用1个字节
24位图:每个像素占用3个字节
为什么要进行处理在显示呢?因为堆内存只有64M(不同手机不一样),加载图片会内存溢出OOM。
android解析图片ARGB 一个1.7M图片需要申请 29M左右的内存,会崩。
图片的像素比我的屏幕的像素要大,如果不缩小,多加载的不能显示没有意义
一般的步骤:
a,获取屏幕宽高
Display dp = getWindowManager().getDefaultDisplay();
int screenWidth = dp.getWidth();
int screenHeight = dp.getHeight();
b,获取图片宽高
Options opts = new Options();
//请求图片属性但不申请内存
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
int imageWidth = opts.outWidth;
int imageHeight = opts.outHeight;
c,图片的宽高除以屏幕宽高,算出宽和高的缩放比例,取较大值作为图片的缩放比例
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;
}
d,按缩放比例加载图片
opts.inSampleSize = scale;//设置缩放比例
opts.inJustDecodeBounds = false;//为图片申请内存
Bitmap bm = BitmapFactory.decodeFile("sdcard/dog.jpg", opts);
iv.setImageBitmap(bm);
代码演示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View v){
Options opts = new Options();
//只请求图片宽高,不解析图片像素
opts.inJustDecodeBounds = true;
//返回null,获取图片宽高,保存在opts对象中
BitmapFactory.decodeFile("sdcard/test.jpg", opts);
//获取图片宽高
int imageWidth = opts.outWidth;
int imageHeight = opts.outHeight;
//获取屏幕宽高
Display dp = getWindowManager().getDefaultDisplay();
int screenWidth = dp.getWidth();
int screenHeight = dp.getHeight();
//计算缩放比例
int scale = 1;
int scaleWidth = imageWidth / screenWidth;
int scaleHeight = imageHeight / screenHeight;
//判断取哪个比例
if(scaleWidth >= scaleHeight && scaleWidth > 1){
scale = scaleWidth;
}
else if(scaleWidth < scaleHeight && scaleHeight > 1){
scale = scaleHeight;
}
//设置缩小比例
opts.inSampleSize = scale;
opts.inJustDecodeBounds = false;
//获取缩小后的图片的像素信息
Bitmap bm = BitmapFactory.decodeFile("sdcard/test.jpg", opts);
ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageBitmap(bm);
}
}
2、创建图片的副本
默认加载的图片是不能操作的,比如想旋转,平移都是不行的,所以我们需要创建一个图片的副本
//加载原图
Bitmap srcBm = BitmapFactory.decodeFile("sdcard/photo.jpg");
iv_src.setImageBitmap(srcBm);
//创建与原图大小一致的空白bitmap
Bitmap copyBm = Bitmap.createBitmap(srcBm.getWidth(), srcBm.getHeight(), srcBm.getConfig());
//定义画笔
Paint paint = new Paint();
//把纸铺在画版上
Canvas canvas = new Canvas(copyBm);
//把srcBm的内容绘制在copyBm上
canvas.drawBitmap(srcBm, new Matrix(), paint);
iv_copy.setImageBitmap(copyBm);//我们就可以操作iv_copy这个副本了
3、图片的简单特效处理
首先定义一个矩阵对象
Matrix mt = new Matrix();
代码演示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//加载原图,放在资源文件夹下
Bitmap bmSrc = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
//创建副本
//1.创建与原图一模一样大小的bitmap对象,该对象中目前是没有内容的,可以比喻为创建了和原图一样大小的白纸
Bitmap bmCopy = Bitmap.createBitmap(bmSrc.getWidth(), bmSrc.getHeight(), bmSrc.getConfig());
//2.创建画笔对象
Paint paint = new Paint();
//3.创建画板,把白纸铺到画板上
Canvas canvas = new Canvas(bmCopy);
Matrix mt = new Matrix();
//平移效果,指定平移距离
//mt.setTranslate(20, 10);
//缩放效果,指定缩放比例
//mt.setScale(2, 0.5f, bmCopy.getWidth() / 2, bmCopy.getHeight() / 2);
//旋转效果
//mt.setRotate(45, bmCopy.getWidth() / 2, bmCopy.getHeight() / 2);
//镜面效果
//mt.setScale(-1, 1);
//mt.postTranslate(bmCopy.getWidth(), 0);
//倒影效果
mt.setScale(1, -1);
mt.postTranslate(0, bmCopy.getHeight());
canvas.drawBitmap(bmSrc, mt, paint);
ImageView iv_src = (ImageView) findViewById(R.id.iv_src);
iv_src.setImageBitmap(bmSrc);
ImageView iv_copy = (ImageView) findViewById(R.id.iv_copy);
iv_copy.setImageBitmap(bmCopy);
}
4、画图板
记录用户触摸事件的XY坐标,绘制直线,可以保存已经画好的图片
给ImageView设置触摸侦听,得到用户的触摸事件,并获知用户触摸ImageView的坐标
注意:必须要使用图片的副本操作,因为原图是操作不了的
在图片的View上设置侦听
代码演示:
public class MainActivity extends Activity {
private int startX;
private int startY;
private Paint paint;//画笔
private Canvas canvas;//理解为画板
private ImageView iv;
private Bitmap bmCopy;//复制图
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bitmap bmSrc = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
bmCopy = Bitmap.createBitmap(bmSrc.getWidth(), bmSrc.getHeight(), bmSrc.getConfig());//白纸
paint = new Paint();//笔
canvas = new Canvas(bmCopy);//画板,将空白的bmcopy放到画板中,就可以在上面操作了
canvas.drawBitmap(bmSrc, new Matrix(), paint);//作画
iv = (ImageView) findViewById(R.id.iv);
iv.setImageBitmap(bmCopy);
iv.setOnTouchListener(new OnTouchListener() {
//用户手指只要触摸屏幕,就会产生触摸事件
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {//判断触摸事件的类型
case MotionEvent.ACTION_DOWN://手指触摸
//触摸事件中会包含手指触摸的坐标
startX = (int) event.getX();
startY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE://手指滑动
int newX = (int) event.getX();
int newY = (int) event.getY();
//指定线的起点和终点
canvas.drawLine(startX, startY, newX, newY, paint);
iv.setImageBitmap(bmCopy);
//把本此画线的终点设置为下一次画线的起点
startX = newX;
startY = newY;
break;
case MotionEvent.ACTION_UP://手指抬起
break;
}
//true表示告诉系统,这个触摸事件由iv处理
//false表示不处理该触摸事件,事件往上传递
return true;//这里一定要注意了,一定要设置为true
}
});
}
public void red(View v){//设置画笔的颜色
paint.setColor(Color.RED);
}
public void green(View v){//设置画笔的颜色
paint.setColor(Color.GREEN);
}
public void brush(View v){
paint.setStrokeWidth(8);//改变线条粗细
}
//保存图片
public void save(View v){
File file = new File("sdcard/test.png");
FileOutputStream fos;
try {
fos = new FileOutputStream(file);
//把图片压缩到本地文件
bmCopy.compress(CompressFormat.PNG, 100, fos);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
//手动发一个sd卡就绪广播
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
sendBroadcast(intent);
}
}
注意:保存图片后,在图库里面是显示不了的,因为图库读取的是sd里面的文件在数据库中的索引值,只有在收到sd就绪广播的时候才会扫描sd文件,
将文件路径存到数据库中,所以我们要手动发一个sd卡就绪的广播,这样在图库中就可以看到我们保存的图片了。
5、游戏案例:
一个撕衣服的游戏小案例
主要的原理就是:把穿内衣和穿外衣的照片重叠显示,内衣照在下面,用户滑动屏幕时,触摸的是外衣照,把手指经过的像素都置为透明,内衣照就显示出来了。
注意在布局文件中设置Ralaytive的布局,不然重叠不到一起,而且图片的像素大小最好和屏幕保持一至,不然会出现撕不准的问题(需要计算比例处理)。
关键的代码:
iv.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int newX = (int) event.getX();
int newY = (int) event.getY();
copyBm.setPixel(newX, newY, Color.TRANSPARENT);//把指定的像素变成透明
iv.setImageBitmap(copyBm);
break;
}
return true;
}
});
每次只设置一个像素点太慢,以触摸的像素为圆心,半径为5画圆,圆内的像素全部置为透明
for (int i = -10; i < 10; i++) {
for (int j = -10; j < 10; j++) {
if(Math.sqrt(i * i + j * j) <= 10)
copyBm.setPixel(newX + i, newY + j, Color.TRANSPARENT);
}
}
6、音乐播放器-本地:
我们查看下API里面的说明就可以大概知道用法了,里面有它使用的一些状态图介绍。
主要的框架代码和服务差不多,使用的是MediaPlayer
播放音频的代码应该运行在服务中,定义一个播放服务MusicService
学习简单的播放,暂停,继续播放,seek等,然后界面显示进度,也可以拖动进度。使用的是SeekBar这个功能
代码演示:
MainActivity.java:
public class MainActivity extends Activity {
ControllerInterface ci;//定义一个中间人的接口,后面获取服务的对象使用
private static SeekBar sb;
public static Handler handler = new Handler(){//注意这里定义static,后面在服务中就直接可以使用这个handler更新界面了
public void handleMessage(android.os.Message msg) {
Bundle bundle = msg.getData();
int duration = bundle.getInt("duration");
int currentPosition = bundle.getInt("currentPosition");
//设置进度条显示进度
sb.setMax(duration);
sb.setProgress(currentPosition);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sb = (SeekBar) findViewById(R.id.sb);
//设置滑动侦听
sb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
//seekBar:触发该方法执行的seekBar对象
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//获取sb的当前进度,然后设置给音乐服务的播放进度
ci.seekTo(seekBar.getProgress());
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
});
Intent intent = new Intent(this, MusicService.class);
startService(intent);//把进程变成服务进程
bindService(intent, new ServiceConnection() {//绑定服务获取中间人对象
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ci = (ControllerInterface) service;//获取到服务中的中间人对象
}
}, BIND_AUTO_CREATE);
}
public void play(View v){//调用服务中间人对象里面的方法,这个中间人方法有会去调用服务中的具体实现的代码方法
ci.play();
}
public void pause(View v){//这样activity就可以调用服务中的方法了
ci.pause();
}
public void continuePlay(View v){
ci.continuePlay();
}
}
ControllerInterface.java:中间人的接口定义
public interface ControllerInterface {
void play();
void pause();
void continuePlay();
void seekTo(int progress);
}
MusicService.java:定义服务进程
public class MusicService extends Service {
MediaPlayer player;
private Timer timer;
@Override
public IBinder onBind(Intent intent) {
return new MusicController();
}
@Override
public void onCreate() {
super.onCreate();
player = new MediaPlayer();//创建服务的时候就把播放器准备好
}
@Override
public void onDestroy() {//服务销毁的时候,播放器也销毁掉
super.onDestroy();
//销毁player
player.stop();
player.release();//释放资源
if(timer != null){
timer.cancel();
timer = null;
}
}
class MusicController extends Binder implements ControllerInterface{
@Override
public void play() {
MusicService.this.play();//调用服务中的play方法,就是具体的代码实现
}
@Override
public void pause() {
MusicService.this.pause();
}
@Override
public void continuePlay() {
MusicService.this.continuePlay();
}
@Override
public void seekTo(int progress) {
MusicService.this.seekTo(progress);
}
}
public void play(){//播放本地的时候可以不用异步准备
player.reset();
try {
player.setDataSource("sdcard/test.mp3");
//player.setDataSource("http://localhost:8080/test.mp3");
//player.prepare();//同步准备
player.prepareAsync();//异步准备
player.setOnPreparedListener(new OnPreparedListener() {
//准备完毕时调用
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
addTimer();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public void pause(){
player.pause();
}
public void continuePlay(){
player.start();
}
public void seekTo(int progress){
player.seekTo(progress);
}
public void addTimer(){
if(timer == null){//注意,如果多次点击播放,就会创建很多的timer,这里判断下,为null才创建,就可以解决了
timer = new Timer();
//设置计时任务
timer.schedule(new TimerTask() {
//这个run方法也是在子线程执行
@Override
public void run() {
int duration = player.getDuration();//获取播放总时长
int currentPosition = player.getCurrentPosition(); //获取当前播放进度
Message msg = MainActivity.handler.obtainMessage();
//把数据封装至消息对象
Bundle data = new Bundle();
data.putInt("duration", duration);
data.putInt("currentPosition", currentPosition);
msg.setData(data);
MainActivity.handler.sendMessage(msg);
}
//计时任务开始5毫秒后,run方法执行,每500毫秒执行一次
}, 5, 500);
}
}
}
7、音乐播放器-在线:
在线只需要注意 使用异步准备,因为你不知道网络的状态,所以需要侦听什么时候准备好
player.setDataSource("http://localhost:8080/test.mp3");
player.setOnPreparedListener(new OnPreparedListener() {
//准备完毕时调用
@Override
public void onPrepared(MediaPlayer mp) {
player.start();
}
});
其它的代码可以复用上面的。
8、视频播放器:
使用SurfaceView
对画面的实时更新要求较高
双缓冲技术:内存中有两个画布,A画布显示至屏幕,B画布在内存中绘制下一帧画面,绘制完毕后B显示至屏幕,A在内存中继续绘制下一帧画面
关键的代码:自带的播放器支持的格式少(3gp mp4)
SurfaceView sv = (SurfaceView) findViewById(R.id.sv);
SurfaceHolder sh = sv.getHolder();
MediaPlayer player = new MediaPlayer();
player.reset();
try {
player.setDataSource("sdcard/test.3gp");
player.setDisplay(sh);
player.prepare();
} catch (Exception e) {
e.printStackTrace();
}
player.start();
SurfaceView是重量级组件,可见时才会创建
SurfaceView一旦不可见,就会被销毁,一旦可见,就会被创建,销毁时停止播放,再次创建时再开始播放
代码演示:
public class MainActivity extends Activity {
private MediaPlayer player;
int progress = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView sv = (SurfaceView) findViewById(R.id.sv);
//获取surfaceview的控制器,我们是操作holder
final SurfaceHolder holder = sv.getHolder();
holder.addCallback(new Callback() {//设置一个回调
//surfaceview销毁时会调用
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if(player != null){
//停止之前先保存播放进度,比如你按home或者back键退出了
progress = player.getCurrentPosition();//设置这个全局变量,不然再进的时候progress就变了
player.stop();
player.release();//释放播放器资源
player = null;//将播放器置空
}
}
//surfaceview创建时会调用
@Override
public void surfaceCreated(SurfaceHolder holder) {
if(player == null){
player = new MediaPlayer();//创建surfaceView的时候就创建播放器
player.reset();
try {
player.setDataSource("sdcard/test.3gp");
player.setDisplay(holder);//指定视频播放在哪个surfaceview
player.prepare();
player.seekTo(progress);//跳转到上一次停止的地方继续播放
player.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//surfaceview结构改变时会调用,暂时没有用到
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
});
}
}
温馨提示:我们再网上可以下载开源免费的资源
FFMPEG:开源免费音视频编解码器
vitamio 多媒体开发框架
9、播放视频也可以用VideoView
VideoView是SurfaceView的一个子类,里面的实现细节我们可以不用管,使用起来比上面的要简单很多
三行代码搞定:
VideoView vv = (VideoView) findViewById(R.id.vv);
vv.setVideoPath("sdcard/test.3gp");
vv.start();
10、摄像头
我们可以调用系统的摄像头的activity去拍照也可以我们自己去实现一个拍照的功能
启动系统提供的拍照程序:
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//隐式启动系统提供的拍照Activity
File file = new File(Environment.getExternalStorageDirectory(), "test.jpg"); //设置照片的保存路径
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(intent, 0);
启动系统提供的摄像程序:
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory(), "test.3gp");
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);//设置保存视频文件的质量
startActivityForResult(intent, 0);
代码演示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click1(View v){
//启动拍照的Activity
Intent intent = new Intent();
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File("sdcard/haha.jpg")));
startActivityForResult(intent, 10);//设置一个整数判断是否成功调用,如果成功调起来我们可以做一个逻辑判断
}
public void click2(View v){
//启动拍照的Activity
Intent intent = new Intent();
intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File("sdcard/haha2.3gp")));
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
startActivityForResult(intent, 20);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 10){
Toast.makeText(this, "拍照完毕", 0).show();
}
else if(requestCode == 20){
Toast.makeText(this, "摄像完毕", 0).show();
}
}
}
备注:我们也可以自己写一个activity去实现拍照的功能,甚至我们不用让界面显示出来就可以在后台进行拍照功能
具体的实现方法可以查阅API文档,里面有详细的介绍。按照步骤照抄就可以了。