目录
第一章:音频管理
第二章:视频播放
一、使用系统中已安装的播放器app(使用Intent方式播放)。
二、使用VideoView配合MediaController播放。
三、使用SurfaceView配合MediaPlayer播放
第三章 拍照功能实现及应用
一、调用系统相机
二、使用 Camera API
第四章 音频录制与播放
一、音频录制
二、音频播放
三、动态权限申请
权限的分类
权限检查与申请
MediaPlayer的生命周期
MediaPlayer官方文档:https://developer.android.com/reference/android/media/MediaPlayer
MediaPlayer详细介绍:https://blog.csdn.net/u014606081/article/details/79927057
使用Service播放音乐的例子:https://blog.csdn.net/qq_33200967/article/details/77263068
视频播放的实现方式一般有三种:
1、使用系统中已安装的播放器app。通过Intent播放视频,这种方式有一些坑,比如在Android6.0以上的版本中,SDCard中视频播放需要权限申请,在Android7.0以上还要考虑FileProvider带来的一些适配的问题。
2、使用VideoView配合MediaController实现。这种方式实现非常简单,缺点是MediaController操作栏固定。
3、使用SurfaceView配合MediaPlayer实现。可以自己写控制视频的控件,具有非常高的可定制性。
private static final int REQUEST_CODE_EXTERNAL_STORAGE = 0x110;
private void checkPermissionAndPlayVideo() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},REQUEST_CODE_EXTERNAL_STORAGE);
}else{
playVideoUseIntent();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
playVideoUseIntent();
} else {
Toast.makeText(this, "该功能需要SDCard权限", Toast.LENGTH_SHORT).show();
}
return;
}
}
}
public void playVideoUseIntent() {
File file = new File(Environment.getExternalStorageDirectory().getPath(), "lalala.mp4");
Intent intent = new Intent(Intent.ACTION_VIEW);
if(Build.VERSION.SDK_INT>=24){
Uri contentUri = FileProvider.getUriForFile(this,
"com.administrator.videodemo.fileprovider", file);
intent.setDataAndType(contentUri, "video/*");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}else {
intent.setDataAndType(Uri.fromFile(file), "video/*");
}
startActivity(intent);
}
manifests.xml文件配置
file_paths.xml文件
参考Android官方文档:
权限申请:https://developer.android.com/training/permissions/requesting
FileProvider:https://developer.android.com/reference/android/support/v4/content/FileProvider?hl=en
activity_video_view.xml:
java源码
private VideoView mVideoView;
private MediaController mController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_view);
mVideoView = findViewById(R.id.id_videoview);
File file = new File(Environment.getExternalStorageDirectory().getPath(), "lalala.mp4");
mVideoView.setVideoPath(file.getAbsolutePath());
mController=new MediaController(this);
mVideoView.setMediaController(mController);
}
前后台切换、横竖屏切换保持:
private int mCurrentPos;
private boolean mIsPause;
private static final String KEY_CUR_POS="key_cur_pos";
private static final String KEY_IS_PAUSE="key_is_pause";
@Override
protected void onResume() {
mVideoView.seekTo(mCurrentPos);
if(!mIsPause) {
mVideoView.start();
}
super.onResume();
}
@Override
protected void onPause() {
mCurrentPos=mVideoView.getCurrentPosition();
mIsPause=!mVideoView.isPlaying();
super.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(KEY_CUR_POS,mCurrentPos);
outState.putBoolean(KEY_IS_PAUSE,mIsPause);
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
mCurrentPos = savedInstanceState.getInt(KEY_CUR_POS);
mIsPause=savedInstanceState.getBoolean(KEY_IS_PAUSE);
super.onRestoreInstanceState(savedInstanceState);
}
activity_media_player.xml
java源码
public class MediaPlayerActivity extends AppCompatActivity {
private RelativeLayout mRlContainer;
private SurfaceView mSurfaceView;
private SeekBar mSeekBar;
private Button mBtnPlay;
private MediaPlayer mMediaPlayer;
private boolean mIsPause;
private Handler mHandler=new Handler();
private Runnable mRunnable=new Runnable() {
@Override
public void run() {
if(mMediaPlayer==null){
return;
}
int duration=mMediaPlayer.getDuration();
int currentPos=mMediaPlayer.getCurrentPosition();
if(mSeekBar!=null && duration>0) {
int progress = (int) (currentPos * 1.0f / duration * 1000);
mSeekBar.setProgress(progress);
if(mMediaPlayer.isPlaying()) {
mHandler.postDelayed(mRunnable, 1000);
}
}
}
};
private float mRatioHW;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_player);
initViews();
initMediaPlayer();
initEvents();
}
@Override
protected void onResume() {
super.onResume();
if(!mIsPause) {
mMediaPlayer.start();
}
}
@Override
protected void onPause() {
super.onPause();
if(!mIsPause) {
mMediaPlayer.pause();
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ViewTreeObserver observer = mRlContainer.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mRlContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
ViewGroup.LayoutParams lp = mRlContainer.getLayoutParams();
lp.height = (int) (mRatioHW * mRlContainer.getWidth());
mRlContainer.setLayoutParams(lp);
}
});
}
private void initEvents() {
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mMediaPlayer.setDisplay(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mHandler.removeCallbacks(mRunnable);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int targetPos = (int) (seekBar.getProgress() * 1.0f / 1000 * mMediaPlayer.getDuration());
mMediaPlayer.seekTo(targetPos);
if(mMediaPlayer.isPlaying()){
mHandler.post(mRunnable);
}
}
});
mBtnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mIsPause){
mMediaPlayer.start();
mIsPause=false;
mHandler.post(mRunnable);
mBtnPlay.setText("暂停");
}else{
mMediaPlayer.pause();
mIsPause=true;
mHandler.removeCallbacks(mRunnable);
mBtnPlay.setText("播放");
}
}
});
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
ViewGroup.LayoutParams layoutParams = mRlContainer.getLayoutParams();
mRatioHW = height * 1.0f / width;
layoutParams.height= (int) (mRlContainer.getWidth()*1.0f/width*height);
mRlContainer.setLayoutParams(layoutParams);
}
});
}
private void initMediaPlayer() {
mMediaPlayer=new MediaPlayer();
File file = new File(Environment.getExternalStorageDirectory().getPath(), "lalala.mp4");
try {
mMediaPlayer.setDataSource(file.getAbsolutePath());
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
mIsPause=false;
mBtnPlay.setText("暂停");
mHandler.post(mRunnable);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void initViews() {
mRlContainer = findViewById(R.id.id_rl_container);
mSurfaceView = findViewById(R.id.id_surface_view);
mSeekBar=findViewById(R.id.id_seekbar);
mBtnPlay=findViewById(R.id.id_btn_play);
}
public static void start(Context context){
Intent intent=new Intent(context,MediaPlayerActivity.class);
context.startActivity(intent);
}
}
manifests文件
补充:内存不足时activity的恢复
参考1:https://www.jianshu.com/p/04a8d85807ca
参考2:https://blog.csdn.net/u013777094/article/details/78617475
照相机的使用一般有两种方式:调用系统相机和使用 Camera API
1、设置权限
2、请求系统相机
//使用Intent请求相机
Intent intent=new Intent();
//设置请求条件
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.addCategory(Intent.CATEGORY_DEFAULT);
//图片保存位置
File file=new File(FILE_APTH);
if(file.exists()){
file.delete();
}
//图片转成Uri
Uri uri=Uri.fromFile(file);
intent.putExtra(MediaStore.EXTRA_OUTPUT,uri);
startActivityForResult(intent, REQUEST_CODE);
3、获取图片
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if(REQUEST_CODE==requestCode){
File file=new File(FILE_APTH);
Uri uri=Uri.fromFile(file);
mImageView.setImageURI(uri);
}
}
1、设置权限
1.5、检查手机是否有相机
/** Check if this device has a camera */
private boolean checkCameraHardware(Context context) {
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
// this device has a camera
return true;
} else {
// no camera on this device
return false;
}
}
2、打开相机
/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(){
Camera c = null;
try {
c = Camera.open(); // attempt to get a Camera instance
}
catch (Exception e){
// Camera is not available (in use or does not exist)
}
return c; // returns null if camera is unavailable
}
3、创建自定义类继承自SurfaceView
/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
private Camera mCamera;
public CameraPreview(Context context, Camera camera) {
super(context);
mCamera = camera;
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed.
mHolder = getHolder();
mHolder.addCallback(this);
// deprecated setting, but required on Android versions prior to 3.0
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
// The Surface has been created, now tell the camera where to draw the preview.
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// empty. Take care of releasing the Camera preview in your activity.
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// If your preview can change or rotate, take care of those events here.
// Make sure to stop the preview before resizing or reformatting it.
if (mHolder.getSurface() == null){
// preview surface does not exist
return;
}
// stop preview before making changes
try {
mCamera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
// start preview with new settings
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
}
4、向布局中添加自定义类
public class CameraActivity extends Activity {
private Camera mCamera;
private CameraPreview mPreview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Create an instance of Camera
mCamera = getCameraInstance();
// Create our Preview view and set it as the content of our activity.
mPreview = new CameraPreview(this, mCamera);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
}
}
5、获取图片
private PictureCallback mPicture = new PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null){
Log.d(TAG, "Error creating media file, check storage permissions");
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
}
}
};
// Add a listener to the Capture button
Button captureButton = (Button) findViewById(R.id.button_capture);
captureButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
// get an image from the camera
mCamera.takePicture(null, null, picture);
}
}
);
参考:https://developer.android.com/guide/topics/media/camera#considerations
录制音频的方法很多,这里只介绍一种MediaRecorder。MediaRecorder可以用来录制音频和视频。
//录制
MediaRecorder mRecorder=new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
mVoiceFile = File.createTempFile("myvoice",".mp3");
mRecorder.setOutputFile(mVoiceFile.getPath());
mRecorder.prepare();
mRecorder.start();
//停止
mRecorder.stop();
mRecorder.release();
MediaPlayer mPlayer=new MediaPlayer();
mPlayer.setDataSource(FILE_PATH);
mPlayer.prepare();
mPlayer.start();
mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mPlayer.stop();
mPlayer.release();
}
});
补充:
1、ListView更新后自动滚动
mListView.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
2、Button手势相应--抬起
mBtnRecord.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_UP){
...
动态权限指从Android6.0(API23)开始,当app运行时用户所授予的权限,而不是在安装程序的时候。
分为normal和dangerous两类。
1、普通权限,对于用户隐私没有危险的,只需要在AndroidManifest.xml文件中申请就可以直接授权。
2、危险权限,需要访问用户的隐私信息等权限,即使在AndroidManifest.xml文件中注册,也需要在运行时通过用户授权。
1、检查程序是否拥有某项权限
int permission = ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTECTS)
返回值:
如果是6.0以下的手机,该方法的返回值会始终为PERMISSION_GRANTED,因此你并不需要动态申请权限,直接做你想做的;如果是6.0以上的手机,情况就分为两种:如果是normal权限,该方法的返回值会始终为PERMISSION_GRANTED,如果是dangerous权限,动态请求。
2、请求权限
ActivityCompat.requestPermissions(thisActivity,new String[]{Manifest.permission.READ_CONTECTS},REQUEST_CODE);
参数1:activity对象
参数2:需要被授权的权限
参数3:请求码
调用此方法会弹出对话框让用户选择,第一次出现:允许、拒绝两个按钮,第二次出现允许、拒绝两个按钮和拒绝后不再询问选择框,选择拒绝后不再询问选择框并点击拒绝按钮后,再次运行程序将不再出现对话框。
3、提示权限
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
参数1:在requestPermissions()传递的请求码。
参数2:在requestPermissions()传递的需要请求的权限。
参数3:申请权限后返回的结果,与requestPermissions()权限数组一一对应。返回值为:PackageManager.PERMISSION_GRANTED、PackageManager.PERMISSION_DENIED(或者其他值?)
请求权限后的回调方法。可以用来处理用户拒绝权限后的操作,例如定义一个全局变量保存用户拒绝相机访问的权限,在需要访问相机的地方提醒用户没有权限。
4、解释权限
boolean should = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,Manifest.permission.READ_CONTECTS)
参数1:activity对象(this)
参数2:需要解释的权限
返回值:布尔类型。返回值为false的情况:第一次被用户拒绝权限时;再次被用户拒绝并且选择“拒绝后不再询问”复选框。返回值为true的情况:第二次起被用户拒绝权限时(拒绝后不再询问”复选框不能被选中)。
是否要向用户解释请求权限的行为,告诉用户如果权限被拒绝可能导致的结果(在程序第一次运行并且被用户拒绝权限后,再次运行程序才需要向用户解释为什么需要权限,如果用户都没有拒绝还解释那么多干什么)。
代码
public static int storageWritePermission = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//检查权限
initPermission();
//初始化控件
initViews();
//添加监听器
addListeners();
}
private void initPermission() {
int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if(permission!= PackageManager.PERMISSION_GRANTED){
boolean should = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if(should){
explainDialog();
return;
}
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1001);
}
}
private void explainDialog() {
AlertDialog.Builder builder=new AlertDialog.Builder(this)
.setMessage("XX功能需要读写磁盘权限!是否授权?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1001);
}
})
.setNegativeButton("取消",null);
builder.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if(requestCode==1001){
if(grantResults[0]==PackageManager.PERMISSION_DENIED){
storageWritePermission =0;
}else{
storageWritePermission =1;
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}