(Camera对象 + Canvas + BrokenView插件 + 声音播放实现)
该游戏主要通过从话筒吹气使屏幕“起雾”,用手指把雾擦除和用手指按住屏幕, 使屏幕显示“碎裂”。App主要功能有: 更换屏幕相框、 调整摄像头焦距、 吹气起雾及碎屏等功能。
主要知识点:1、Camera相机的使用, 2、AudioRecordManager 话筒的使用
3、BrokenView 插件控件的使用 4、GestureDetector 手势的使用
5、画图工具的使用 6、单击事件的应用
7、BroadcastReceiver 的使用 8、线程与消息处理
控件:TextView、 ImageView、 SeekBar 拖动条控件。
事件处理: OnTouchListener 触摸事件、 OnClickListener 单击事件。
多媒体:Camera 摄像头应用、 SoundPool 声音播放。
资源访问: Drawable 图片资源、 Styles 演示资源、 Colors 颜色资源、 Strings 字符串资源、 Dimens 尺寸资源。
布局管理器: LinearLayout 、RelativeLayout 布局管理器、布局管理器的嵌套。
图形图像: 画笔和画布、 补间动画
系统文件夹结构图:
一、主活动 + 主布局
主布局:
效果图:
主布局分为以下几个部分:
1、SurfaceView: 主界面最底层的显示区域, 内嵌了一个Surface, SurfaceView提供了一个可见区域,用来显示 摄像头设置的内容。
2、PictureView: 自定义控件, 在此控件上完成控件镜框更换的功能, 布满整个显示区域。
3、FuncationView: 自定义控件, 功能组合控件, 将系统帮助、 选择相框和亮度调节等3个功能组合到一起, 形成主界面顶部功能区。
4、底部焦距调节功能区域: 使用线性布局展示, 包括放大、缩小及缩放进度条展示
5、DrawView: 吹气起雾和擦除雾气图层
关于SurfaceView 更多的信息,可以点击这里进行了解:点击打开链接
接着开始了解 PictureView, FuncationView 和 DrawView 自定义控件。
1、PictureView:
新建PictureView 类并使其继承 ImageView,要完成镜框显示的界面布局, 就需要完成PictureView 类的基本构造函数。
2、FuncationView:
新建FuncationView类,其主要功能是实现界面顶部功能, 包括: 增加亮度、 减少亮度、 系统帮助、 和镜框选择等, FuncarionView 是由按钮控件组成, 所以FuncationView 类需要实现 单击事件的监听接口, 因为FuncationView类 使用线性布局, 所以需要继承线性布局"extends LinearLayout"。
3、DrawView:
新建DrawView类, 主要用于手机屏幕画布绘画功能,完成对着话筒吹气使手机屏幕起雾以及手指擦除雾气时更换绘图层的功能。 使其继承 View类 并创建构造方法。
由此可以看得出,先完成安卓应用的所有View 界面的初始创建 然后再去完成各个View之间的需求, 更加使得软件开发过程更加有条理性和代码递进的逻辑性。
摄像头: 此软件需要获取前置摄像头, 打开前置摄像头并设置手机支持的屏幕尺寸、 最大和最小焦距及摄像头照相的方向等属性。 获取摄像头操作方法的调用时序图如下:
在主活动类中添加变量, 声明相机拍摄预览surfaceView 区域及焦距对象等:
private static final String TAG = MainActivity.class.getSimpleName(); //获得类名
private SurfaceHolder holder;//显示相机拍摄的内容
private SurfaceView surfaceView;
private PictureView pictureView;
private FunctionView functionView;
private SeekBar seekBar;
private ImageView add;
private ImageView minus;
private LinearLayout bottom;
private DrawView drawView;
添加initViews() 方法, 获取各个子view 和控件:
//初始化子布局和控件
private void initViews() {
surfaceView = (SurfaceView) findViewById(R.id.surface);
pictureView = (PictureView) findViewById(R.id.picture);
functionView = (FunctionView) findViewById(R.id.function);
seekBar = (SeekBar) findViewById(R.id.seekbar);
add = (ImageView) findViewById(R.id.add);
minus = (ImageView) findViewById(R.id.minus);
bottom = (LinearLayout) findViewById(R.id.bottom_bar);
drawView = (DrawView) findViewById(R.id.draw_glasses);
}
获取摄像头:在主活动中添加摄像头操作将使用到的全局变量和对象等。
private int currentCamIndex;
private boolean haveCamera;
private int minFocus; //最小焦距
private int maxFocus;
private int everyFoucs; //用于调整焦距
private int nowFoucs; //当前的焦距值
private int ROTATE;//旋转值 private Camera camera;
检测是否有摄像头:
//判断是否有摄像头
private boolean checkCameraHardware() {
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
// this device has a camera
return true;
} else {
// no camera on this device
return false;
}
}
设置摄像头方向:
//设置摄像头方向,调整摄像头旋转90度,成竖屏
private void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
Camera.CameraInfo info = new Camera.CameraInfo(); //实例化相机对象
Camera.getCameraInfo(cameraId, info); //获得相机对象
//获得旋转角度
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation){
case Surface.ROTATION_0: //未旋转
degrees = 0;
break;
case Surface.ROTATION_90: //旋转90度
degrees = 90;
break;
case Surface.ROTATION_180: //旋转180度
degrees = 180;
break;
case Surface.ROTATION_270: //旋转270度
degrees = 270;
break;
}
int result = 0;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
//前置摄像头旋转算法
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360;
} else {
//后置摄像头旋转算法
result = (info.orientation - degrees + 360) % 360;
}
ROTATE = result + 180;
camera.setDisplayOrientation(result);//设置相机拍摄角度
}
设置摄像头:
//设置摄像头
private void setCamera() {
if (checkCameraHardware()){
//打开前置摄像头
camera = openFrontFacingCameraGingerbread();
//设置摄像头方向,调整摄像头旋转90度,成竖屏
setCameraDisplayOrientation(this,currentCamIndex,camera); //设置相机的相关参数
Camera.Parameters parameters = camera.getParameters();
parameters.setPictureFormat(ImageFormat.JPEG); //设置照片格式为JPG
//获取支持的对焦模式
List<String> list = parameters.getSupportedFocusModes();
for (String str : list){
Log.d(TAG, "支持的对焦模式:" + str);
}
//手机支持的图片尺寸集合
List<Camera.Size> pictureList = parameters.getSupportedPictureSizes();
//手机支持的预览尺寸集合
List<Camera.Size> previewList = parameters.getSupportedPreviewSizes();
//设置为当前使用手机的最大尺寸
parameters.setPictureSize(pictureList.get(0).width, pictureList.get(0).height);
parameters.setPreviewSize(previewList.get(0).width, previewList.get(0).height);
minFocus = parameters.getZoom(); //获得最小焦距
maxFocus = parameters.getMaxZoom(); //获得最大焦距
everyFoucs = 1; //每次增加及减少值
nowFoucs = minFocus; //当前焦距值为最小
seekBar.setMax(maxFocus); //设置最大焦距值
Log.d(TAG, "当前镜头距离:" + minFocus + "\t\t获取最大距离: " + maxFocus); //打印日志
camera.setParameters(parameters); //设置相机参数
}
}
显示摄像头图像:
使主活动类实现 SurfaceHolder.Callback 接口, 重写3个方法,完成头像显示:
在surfaceCreated() 方法中调用摄像头, 设置摄像头预览图像显示在surfaceHolder 对象中, 并开始预览:
//在surfaceView被显示时重写surfaceCreated方法, 此处显示照相机显示前置摄像头内容
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.e("surfaceCreated", "绘制开始");
try{
setCamera(); //设置摄像头
camera.setPreviewDisplay(holder); //设置预览显示的surfacehholder,holder中保存着surface对象
camera.startPreview(); //开始预览
} catch (IOException e) {
camera.release(); //相机释放
camera = null; //清空对象
e.printStackTrace();
}
在surfaceChanged() 方法中绘制图像改变:
//摄像头图像绘制改变,在surfaceholder 对象改变时重新加载相机显示区域, 先通知相机预览, 然后重新预览改变后的holder对象
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.e("surfaceChanged", "绘制改变");
try {
camera.stopPreview(); //停止预览
camera.setPreviewDisplay(holder); //设置相机预览显示区域
camera.startPreview(); //启动预览
} catch (IOException e) {
e.printStackTrace();
}
}
在surfaceDestoryed()方法中完成图像绘画结束:
//摄像头图像绘制结束,需要完成相机资源释放
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.e("surfaceDestoryed", "绘制结束");
toRelease(); //释放相机资源
}
private void toRelease() {
camera.setPreviewCallback(null); //停止相机视频,这个方法必须在前面, 否则会报错
camera.stopPreview(); //停止预览
camera.release(); //释放资源
camera = null; //清空对象
}
获取前置摄像头和预览函数已经完成, 接下来需要在主界面上调取摄像头:
private void setViews() {
holder = surfaceView.getHolder(); //获取SurfaceHolder对象
holder.addCallback(this); //通过SurfaceHolder 设置Surface的生命周期回调方法
}
在onCreate() 方法中调用setViews() 方法, 实现主界创建的同时调用摄像头显示图像功能。
焦距调节:
获取当前焦距:
//设置相机参数,通过seekbar 控件改变进度设置相机焦距
private void setZoomValues(int want){
Camera.Parameters parameters = camera.getParameters(); //获取相机参数
seekBar.setProgress(want); //设置进度
parameters.setZoom(want); //设置焦距参数
camera.setParameters(parameters); //设置相机参数-焦距
}
//获取相机参数和当前焦距
private int getZoomValues(){
Camera.Parameters parameters = camera.getParameters();
int values = parameters.getZoom(); //获取当前焦距
return values; //返回焦距值
}
调节焦距:
//增大焦距
public void addZoomValues(){
if (nowFoucs > maxFocus){
Log.e(TAG, "大于最大焦距是不可能的!");
}else if (nowFoucs == maxFocus){
}
else {
setZoomValues(getZoomValues() + everyFoucs);
}
}
//缩小焦距
public void minusZoomValues(){
if (nowFoucs < 0){
Log.e(TAG, "小于0是不可能的!");
}else if (nowFoucs == 0){
} else {
setZoomValues(getZoomValues() - everyFoucs);
}
}
在主活动中添加SeekBar 进度值变化的方法onProgressChanged(), 实现拖动进度条时, 根据进度条的变化调节焦距的功能:
//SeekBar 进度值变化的方法onProgressChangde() 实现拖动进度条调节焦距
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//0-99 99级
Camera.Parameters parameters = camera.getParameters();
nowFoucs = progress; //进度值赋值给焦距
parameters.setZoom(progress); //设置焦距
camera.setParameters(parameters); //设置相机
}
实现单击调节焦距功能:
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.add:
addZoomValues();
break;
case R.id.minus:
minusZoomValues();
break;
case R.id.picture:
gestureDetector.onTouchEvent(event); //手势识别事件
break;
default:
break;
}
return true;
}
在setViews() 方法中 添加按钮及 SeekBar 的监听事件代码:
add.setOnTouchListener(this);
minus.setOnTouchListener(this);
seekBar.setOnSeekBarChangeListener(this); //进度条监听
顶部功能栏的功能实现:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_marginTop="10dp"
android:background="@drawable/back_shape_white"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="18sp"
android:text="调节亮度"
android:layout_width="match_parent"
android:layout_height="55dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_vertical"
/>
<TextView
android:id="@+id/i_know"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/an"
android:gravity="center"
android:layout_gravity="center"
android:textColor="@android:color/white"
android:textSize="18dp"
android:text="我知道了"/>
<TextView
android:layout_width="match_parent"
android:layout_height="55dp"
android:background="@drawable/back_shape_white"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="18dp"
android:layout_marginBottom="10dp"
android:text="调节焦距"/>
</LinearLayout>
创建HintActivity类:
public class HintActivity extends AppCompatActivity{
private TextView know;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
//不显示状态栏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hint);
know = (TextView) findViewById(R.id.i_know); //"我知道了"控件
know.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
使主活动实现FuncationView.onFuncationItemClickListener 接口, 实现接口的4个方法:
此时就可以完成hint() 方法:
@Override
public void hint() {
Intent intent = new Intent(this, HintActivity.class);
startActivity(intent); //界面跳转到系统帮助页面
}
private onFunctionViewItemClickListener listener;
private ImageView down;
private ImageView hint;
private ImageView choose;
private ImageView up;
public interface onFunctionViewItemClickListener{
void hint(); //帮助按钮方法
void choose(); //选择镜框按钮方法
void down(); //减少亮度方法
void up(); //增加亮度方法
}
//调用设置组件,给四个控件注册点击事件
public void setView(){
hint.setOnClickListener(this);
choose.setOnClickListener(this);
down.setOnClickListener(this);
up.setOnClickListener(this);
}
//在此类中,增加自定义控件FunctionViewe中的控件按钮的按键监听对象方法
public void setOnFunctionViewItemClickListener(onFunctionViewItemClickListener monFunctionViewItemClickListener){
this.listener = monFunctionViewItemClickListener; //设置监听对象
}
选择镜框:
新增PhotoFrameActivity 中声明显示镜框图片的网格视图和图片id数组、 图片名称数组等变量:
private GridView gridView; //镜框网格
private TextView textView; //返回键
private int[] photo_styles; //图片的数组
private String[] photo_name; //图片的名称数组
private Bitmap[] bitmaps; //镜框的集合
增加初始化数据方法initDatas() 设置图片样式、 名称数组及获取图片资源到Bitmap对象中:
public void initDatas(){
//图片样式
photo_styles = new int[]{R.mipmap.mag_0001,R.mipmap.mag_0003,R.mipmap.mag_0005,R.mipmap.mag_0006,R.mipmap.mag_0007,
R.mipmap.mag_0008,R.mipmap.mag_0009,R.mipmap.mag_0011,R.mipmap.mag_0012,R.mipmap.mag_0014};
//图片名称
photo_name = new String[]{"Beautiful", "Special", "Wishes", "Forever", "Journey", "Love", "River", "Wonderful", "Birthday", "Nice"};
bitmaps = new Bitmap[photo_styles.length];
for (int i = 0; i < photo_styles.length; i++){
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), photo_styles[i]);
bitmaps[i] = bitmap;
}
}
再添加适配器类:
class PhotoFrameAdapter extends BaseAdapter{
@Override
public int getCount() {
return photo_name.length;
}
@Override
public Object getItem(int position) {
return photo_name[position];
}
@Override
public long getItemId(int position) {
return position; //返回图片position
}
/**
* 获取item对象
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null){
viewHolder = new ViewHolder();
convertView = getLayoutInflater().inflate(R.layout.item_gridview, null);
viewHolder.imageView = (ImageView) findViewById(R.id.item_pic);
viewHolder.txt = (TextView) findViewById(R.id.item_txt);
convertView.setTag(viewHolder);
}else {
viewHolder = (ViewHolder) convertView.getTag();
}
setData(viewHolder, position);
return convertView;
}
}
//设置数据
private void setData(ViewHolder viewHolder, int position) {
Log.e("bitmap[position] :", String.valueOf(bitmaps[position]));
viewHolder.imageView.setImageBitmap(bitmaps[position]);
viewHolder.txt.setText((photo_name[position]));
}
private class ViewHolder {
ImageView imageView;
TextView txt;
}
最后添加图片选择单击事件onItemClick() 方法, 实现选取镜框的动作:
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = new Intent();
intent.putExtra("POSITION", position);
setResult(RESULT_OK, intent);
finish();
}
在onCreate() 方法中进行控件和数据初始化:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); //满屏显示
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo_frame);
//初始化控件
textView = (TextView) findViewById(R.id.back_to_main);
gridView = (GridView) findViewById(R.id.photo_frame_list);
initDatas(); //初始化数据
textView.setOnClickListener(this);
PhotoFrameAdapter adapter = new PhotoFrameAdapter(); //创建适配器
gridView.setAdapter(adapter);
gridView.setOnItemClickListener(this);//执行子项单击监听事件
}
完成PhotoFrameActivity后, 在主活动类中找到choose() 方法, 实现选择镜框界面跳转功能, 并返回选择ID值:
//实现选择镜框页面跳转功能
@Override
public void choose() {
Intent intent = new Intent(this, PhotoFrameActivity.class);
startActivityForResult(intent, PHOTO); //跳转页面并获得选择镜框ID的返回值
Toast.makeText(this, "选择相框!", Toast.LENGTH_SHORT).show();
}
初始化镜框图片:
frame_index = 0;
frame_index_ID = new int[]{R.mipmap.mag_0001,R.mipmap.mag_0003,R.mipmap.mag_0005,R.mipmap.mag_0006,R.mipmap.mag_0007,
R.mipmap.mag_0008,R.mipmap.mag_0009,R.mipmap.mag_0011,R.mipmap.mag_0012,R.mipmap.mag_0014};
添加onActivityResult() 方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); //获取选择镜框界面的data
Log.e(TAG, "返回值:" + resultCode + "\t\t请求值:" + requestCode);
if (resultCode == RESULT_OK && requestCode == PHOTO){
int position = data.getIntExtra("POSITION", 0); //从返回数据获得POSITION值
frame_index = position; //图片位置赋值
Log.e(TAG, "返回镜框类别:" + position);
pictureView.setPhotoFrame(frame_index); //传递position值,设置镜框
}
}
完善PictureView类的镜框选择功能:
变量声明:
private int[] bitmap_ID_Array; //图片资源ID的数组
private Canvas mCanvas; //画布
private int draw_Width; //要画的长度
private int draw_Height; //要画的高度
private Bitmap mBitmap; //镜框
private int bitmap_index; //图片标记
添加initBitmaps() 完成初始化图片集:
public void initBitmaps(){
//获取mipmap资源中的图片
bitmap_ID_Array = new int[]{R.mipmap.mag_0001, R.mipmap.mag_0003,R.mipmap.mag_0005,R.mipmap.mag_0006,R.mipmap.mag_0007,
R.mipmap.mag_0008,R.mipmap.mag_0009,R.mipmap.mag_0011,R.mipmap.mag_0012,R.mipmap.mag_0014};
}
添加getTheWindowSize(), 获取屏幕尺寸:
//获取屏幕属性, 得到屏幕的分辨率
private void getTheWindowSize(Activity activity){
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
draw_Height = dm.heightPixels;
draw_Width = dm.widthPixels;
Log.e("1、 屏幕宽度:", draw_Width + "\t\t 屏幕高度:" + draw_Height);
}
添加init() 初始化画布, 实例化Canvas对象:
//初始化控件
private void init(){
initBitmaps(); //初始化图片集合
bitmap_index = 0; //图片索引
//初始化画布
mBitmap = Bitmap.createBitmap(draw_Width, draw_Height, Bitmap.Config.ARGB_8888); //创建指定格式、大小的位图
mCanvas = new Canvas(mBitmap); //实例化Canvas对象
mCanvas.drawColor(Color.TRANSPARENT); //设置背景色为透明色
}
添加setPhotoFrame() 方法, 获取选择要更换镜框的ID:
public void setPhotoFrame(int index){
bitmap_index = index; //ID传递
invalidate(); //窗口无效, 需要重绘 通知view组件重绘,再次调用onDraw()
}
添加getPhotoFrame(),获取更换镜框id:
public int getPhotoFrame(){
return bitmap_index; //选择返回的镜框ID
}
添加getNewBitmap(), 获取更新的Bitmap对象, 用于更换镜框:
//获取更新的Bitmap对象, 用于更换镜框
private Bitmap getNewBitmap(){
//根据bitmap_index 获取图片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), bitmap_ID_Array[bitmap_index])
.copy(Bitmap.Config.ARGB_8888, true);
//根据长、宽设置图片
bitmap = Bitmap.createScaledBitmap(bitmap, draw_Width, draw_Height, true); //从当前存在的位图,按一定的比例创建一个新的位图。
return bitmap; //返回获取的图片对象
}
添加重写方法onDraw(), 完成图片绘制:
//初始化控件后,调用该方法,实现图片绘制的功能
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); //父类方法
canvas.drawColor(Color.TRANSPARENT); //设置画布颜色
canvas.drawBitmap(mBitmap, 0, 0, null);
Rect rect1 = new Rect(0, 0, this.getWidth(), this.getHeight()); //区域大小
canvas.drawBitmap(getNewBitmap(), null, rect1, null);//绘制图片
}
最后在构造方法中调用方法:
getTheWindowSize((Activity) context);//获得屏幕尺寸
init(); //初始化
在主活动类中调用“pictureView.setPhotoFrame(frame_index); ”完成镜框选择:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); //获取选择镜框界面的data
Log.e(TAG, "返回值:" + resultCode + "\t\t请求值:" + requestCode);
if (resultCode == RESULT_OK && requestCode == PHOTO){
int position = data.getIntExtra("POSITION", 0); //从返回数据获得POSITION值
frame_index = position; //图片位置赋值
Log.e(TAG, "返回镜框类别:" + position);
pictureView.setPhotoFrame(frame_index); //传递position值,设置镜框
}
}