Android 详解音视频开发

1. Android 音视频开发?

答:

2. 怎样打开 Android 手机的开发者选项中的 USB 调试?

答:

  • Android 4.2 系统之前,设置、开发者选项、USB 调试。
  • Android 4.2 系统之后,开发者选项默认是隐藏的,关于手机 –> 连续点击版本号 –> 开发者选项 –> USB 调试。

3. 通知(Notification)使用的场景?

答:

  • Notification 既可以在 Activity 里创建,也可以在 BroadcastReceiver 里创建,还可以在 Service 里创建。
  • 相比于 BroadcastReceiveService,在 Activity 里创建 Notification 的场景还是比较少的,因为一般只有当程序进入到后台的时候我们才需要使用 Notification

4. 【笔试题】手写一个使用 Notification 的 Demo?

答:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button sendNotice = (Button) findViewById(R.id.send_notice);
        sendNotice.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.send_notice:
                // PendingIntent 和 Intent 都可以用于启动活动、启动服务以及发送广播
                // 不同的是,Intent 更加倾向于去立即执行某个动作,而 PendingIntent 更加倾向于在某个合适的时机去执行某个动作
                // 即可以把 PendingIntent 理解为延迟执行的 Intent
                Intent intent = new Intent(this, NotificationActivity.class); // 点击通知启动另一个活动
                PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0); 

                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                Notification notification = new NotificationCompat.Builder(this) // 使用 support-v4 库中的 NotificationCompat 类来解决因为 Andoird 团队经常对通知功能的修改导致的 API 不稳定的问题
                        .setContentTitle("This is content title") // 指定通知的标题内容
                        .setContentText("This is content text") // 指定通知的正文内容
                        .setWhen(System.currentTimeMillis()) // 指定通知被创建的时间,这里指定的时间会显示在相应的通知上
                        .setSmallIcon(R.mipmap.ic_launcher) // 设置通知的小图标,这里只能使用纯 alpha 图层的图片进行设置,会显示在系统状态栏上
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) // 设置通知的大图标,下拉状态栏时可以看到
                        .setContentIntent(pi) // 给通知增加点击功能
                        .setAutoCancel(true) // 点击通知后会自动取消。另一种方式是:manager.cancel(1),其中参数 1 正是 notify() 方法中的第一个参数
                        .setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg"))) // 进阶技巧1:在通知发出时候播放一段音频
                        .setVibrate(new long[] { 0, 1000, 1000, 1000 }) // 进阶技巧2:在通知到来的时候让手机进行振动,注意此时需要在 AdroidManifest.xml 文件中声明权限:<uses-permission android:name="android.permissions.VIBRATE" />
                        .setLights(Color.GREEN, 1000, 1000) // 进阶技巧3:当通知到来时,如果手机又处于锁屏状态,则控制手机的前置 LED 灯使其闪烁。也可以调用 .setDefaults(NotificationCompat.DEFAULT_ALL) 使用默认效果,它会根据当前手机的环境来决定播放什么铃声,以及如何振动
                        .setStyle(new NotificationCompat.BigTextStyle().bigText("很长很长的文本")) // 高级功能1-1:让通知内容是长文本的富文本并完全显示,否则使用 setContentText() 的话,多余的内容会用省略号代替
                        .setStyle(new NotificationCompat.BitPictureStyle().bigPicture(BitmapFactory.decodeResource(getResource(), R.drawable.big_image))) // 高级功能1-2:让通知内容是富文本的大图
                        .setPriority(NotificationCompat.PRIORITY_MAX) // 高级功能2:设置通知的优先级。
                        // 这里一共有 5 个常量值可选,PRIORITY_DEFAULT 表示默认的优先级,和不设置的效果是一样的
                        // PRIORITY_MIN 表示最低的优先级,系统可能只会在特定的场景才显示这条通知,比如用户下拉状态栏的时候
                        // PRIORITY_LOW 表示较低的优先级,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在优先级更高的通知之后
                        // PRIORITY_HIGH 表示较高的优先级,系统可能会将这类通知放大,或改变其显示顺序,将其排在比较靠前的位置
                        // PRIORITY_MAX 表示最高的优先级,这类通知必须要让用户立刻看到,甚至需要用户做出响应操作,此时的通知不是在系统状态栏显示一个小图标了,而是弹出了一个横幅,并附带了通知的详细内容,表示这是一条非常重要的通知,不管用户在玩游戏还是在看电影,这条通知都会显示在最上方 ,以此引起用户的注意。使用优先级最高的通知一定要谨慎,不能让用户反感
                        .build();
                manager.notify(1, notification); // 第一个参数是 id,要保证为每个通知所指定的 id 都是不同的
                break;
            default:
                break;
        }
    }
}

5. 【笔试题】手写一个调用手机摄像头进行拍照的 Demo?

答:

public class MainActivity extends AppCompatActivity {
    public static final int TAKE_PHOTO = 1;
    private ImageView picture;
    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button takePhoto = (Button) findViewById(R.id.take_photo);
        picture = (ImageView) findViewById(R.id.picture);
        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 创建 File 对象,用于存储拍照后的图片
                // 这里把 图片命名为 output_image.jgp,并把它存放在手机 SD 卡的应用关联缓存目录下
                // 应用关联缓存目录是指 SD 卡中专门用于存放当前应用缓存数据的位置,调用 getExternalCache() 方法就可以得到这个目录,具体的路径是 /sdcard/Android/data/<package name>/cache
                // 之所以要使用应用关联缓存目录来存放图片,是因为从 Android 6.0 系统开始,读写 SD 卡被列为了危险权限,如果将图片存放在 SD 卡的任何其他目录,都要进行运行时权限处理,而使用应用关联目录可以跳过运行时权限处理的这一步
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
                     if(outputImage.exist()) {
                         outputImage.delete();
                     }
                     outputImage.createNewFile();
                } catch(IOException e) {
                    e.printStackTrace();
                }
                if(Build.VERSION.SDK_INT >= 24) { // Android 7.0
                    // getUriForFile() 方法将 File 对象转换成一个封装过的 Uri 对象
                    // 之所以要进行这样一层转换,是因为从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常
                    // 而 FileProvider 则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。此时,需要在 AndroidManifest.xml 文件中注册 FileProvider 这个内容提供器
                    imageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.cameraalbumtest.fileprovider", outputImage);
                } else {
                     // 这个 Uri 对象标识着 output_image.jgp 这张图片的本地真实路径
                    imageUri = Uri.fromFile(outputImage);
                }
                // 启动相机程序
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); // 使用的是隐式 Intent,系统会找出能够响应这个 Intent 的活动去启动
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent, TAKE_PHOTO);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case TAKE_PHOTO:
                if(resultCode == RESULT_OK) {
                    try {
                        // 将拍摄的照片显示出来
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        picture.setImageBitmap(bitmap);
                    } catch(FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
            default:
                break;
        }
    }
}

// AndroidManifest.xml 清单文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cameraalbumtest">
    // 在 Andrioid 4.4 系统之前,访问 SD 卡的应用关联目录也是要声明权限的,从 4.4 系统开始不再需要权限声明。这里为了兼容老版本系统的手机,还是声明一下访问 SD 卡的权限
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...
        <provider
            andorid:name="android.support.v4.content.FileProvider" // name 属性的值是固定的
            android:authorities="com.example.cameraalbumtest.fileprovider" // authorities 属性的值必须要和上面 FileProvider.getUriForFile() 方法中的第二个参数一致
            andorid:exported="false"
            android:grantUriPermissions="true">
            <meta-data // 指定 Uri 的共享路径,并引用了一个 @xml/file_paths 资源,这个资源需要手动创建
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

// 在 res 目录中创建一个 xml 目录,里面创建一个 file_paths.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="" /> // 指定 Uri 共享的,name 属性值可以随便填,path 属性值表示共享的具体路径。这里设置空值表示将整个 SD 卡进行共享,当然也可以仅共享存放 output_image.jpg 这张图片的路径
</paths>

6. 【笔试题】手写一个从相册中选择照片的 Demo?

答:

public class MainActivity extends AppCompatActivity {
    ...
    public static final int CHOOSE_PHOTO = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button takePhoto = (Button)  findViewById(R.id.take_photo);
        Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
        ...
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTENAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                    // 相册中的照片都是存储在 SD 卡上的,要从 SD 卡中读取照片就需要申请这个权限。WRITE_EXTERNAL_STORAGE 表示同时授予 SD 卡读和写的能力
                    ActivityCompat.requestPermissions(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1); 
                } else {
                    openAlbum();
                }
            }
        });
    }

    private void openAlbum() {
        Intent intent = new Intent("android.intent.action.GET_CONTENT");
        intent.setType("image/*");
        startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
    }

    @Override
    public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case 1:
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    openAlbum();
                } else {
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            ...
            case CHOOSE_PHOTO:
                if(resultCode == RESULT_OK) {
                    // 判断手机系统版本号
                    // 因为Android 系统从 4.4 版本开始,选取相册中的图片不再返回图片真实的 Uri 了,而是一个封装过的 Uri,因此如果是 4.4 版本以上的手机就需要对这个 Uri 进行解析才行
                    if(Build.VERSION.SDK_INT >= 19) {
                        handleImageOnKitKat(data); // 4.4 及以上系统使用这个方法处理图片
                     } else {
                         handleImageBeforeKitKat(data); // 4.4 以下系统使用这个方法处理图片
                     }
                }
                break;
            default:
                break;
        }
    }

    @TargetApi(19)
    private void handleImageOnKitKat(Intent data) { // 解析封装过的 Uri
        String imagePath = null;
        Uri uri = data.getData();
        if(DocumentContract.isDocumentUri(this, uri)) {
            // 如果是 document 类型的 uri,则通过 document id 处理
            String docId = DocumentsContract.getDocumentId(uri);
            if("com.android.providers.media.documents".equals(uri.getAuthrity())) {
                String id = docId.split(":")[1]; // 解析出数字格式的 id
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection); // 获取图片的真实路径
            } else if("com.android.provider.downloads.documents".equals(uri.getAuthority)) {
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                imagePath = getImagePath(contentUri, null);
            }
        } else if("content".equalsIgnoreCase(uri.getScheme())) {
            // 如果是 content 类型的 Uri,则使用普通方式处理
            imagePath = getImagePath(uri, null);
        } else if("file".equalsIgnoreCase(uri.getScheme())) {
            // 如果是 file 类型的 Uri,直接获取图片路径即可
            imagePath = uri.getPath();
        }
        displayImage(imagePath); // 根据图片路径显示图片
    }

    private void handleImageBeforeKitKat(Intent data) {
        Uri uri = data.getData();
        String imagePath = getImagePath(uri, null); // 不需要任何解析获取图片的真实路径
        displayImage(imagePath);
    }

    private void String getImagePath(Uri uri, String selection) {
        String path = null;
        // 通过 Uri 和 selection 来获取真实的图片路径
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if(cursor != null) {
            if(cursor.moveToFirst()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }

    private void displayImage(String imagePath) {
        if(imagePath != null) {
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            picture.setImageBitmap(bitmap);
        } else {
            Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
        }
    }
}

// TODO: 上面的实现还不算完美,因为某些照片即使经过裁剪后体积仍然很大,直接加载到内存中有可能会导致程序崩溃。更好的做法是根据项目的需求先对照片进行适当的压缩,然后再加载到内存中

7. MediaPlayer 类中较为常用的控制方法有哪些?

答:

方法名 功能描述
setDataSource() 设置要播放的音频的位置
prepare() 在开始之前调用这个方法完成准备工作
start() 开始或继续播放音频
pause() 暂停播放音频
reset() 将 MP 对象重置到刚刚创建的状态
seekTo() 从指定的位置开始播放音频
stop() 停止播放音频,调用这个方法后的 MP 对象无法再播放音频
release() 释放掉与 MP 对象相关的资源
isPlaying() 判断当前 MP 对象是否正在播放音频
getDuration() 获取载入的音频文件的时长

8. 【笔试题】手写一个播放音频的 Demo?

答:

// 首先,需要在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.pemission.WRITE_EXTERNAL_STORAGE" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private MediaPlayer mediaPlayer = new MediaPlayer();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button play = (Button) findViewById(R.id.play);
        Button pause = (Button) findViewByid(R.id.pause);
        Button stop = (Button) findViewById(R.id.stop);
        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        stop.setOnClickListener(this);
        // 动态申请 WRITE_EXTERNAL_STORAGE 权限,因为会在 SD 卡中放置一个音频文件,程序为了播放这个音频文件必须拥有访问 SD 卡的权限才行
        if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermission(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORATE }, 1);
        } else {
            initMediaPlayer(); // 初始化 MediaPlayer
        }
    }

    private void initMediaPlayer() {
        try {
            File file = new File(Environment.getExternalStorageDirectory(), "music.mp3");
            mediaPlayer.setDataSource(file.getPath()); // 指定音频文件的路径
            mediaPlayer.prepare(); // 让 MediaPlayer 进入到准备状态
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case 1:
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    initMediaPlayer();
                } else {
                    Toast.makeText(this, "拒绝权限将无法使用程序”, Toast.LENGTH_SHORT).show();
                    finish(); // 如果用户拒绝了权限申请,那么就调用 finish() 方法将程序直接关掉,因为如果没有 SD 卡的访问权限,这个程序什么也干不了
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId() {
            case R.id.play:
                if(!mediaPlayer.isPlaying()) {
                    mediaPlayer.start(); // 开始播放
                }
                break;
            case R.id.pause:
                if(mediaPlayer.isPlaying()) {
                    mediaPlayer.pause(); // 暂停播放
                }
                break;
            case R.id.stop:
                if(mediaPlayer.isPlaying()) {
                    mediaPlayer.reset(); // 停止播放
                    initMediaPlayer();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mediaPlayer != null) {
            mediaPlayer.stop(); // 将与 MediaPlayer 相关的资源释放掉
            mediaPlayer.release(); // 将与 MediaPlayer 相关的资源释放掉
        }
    }
}

9. VideoView 中常用的方法有哪些?

答:

方法名 功能描述
setVideoPath() 设置要播放的视频文件的位置
start() 开始或继续播放视频
pause() 暂停播放视频
resume() 将视频从头开始播放
seekTo() 从指定的位置开始播放视频
isPlaying() 判断当前是否正在播放视频
getDuration() 获取载入的视频文件的时长

10. 【笔试题】手写一个播放视频的 Demo?

答:

// 首先,需要在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.pemission.WRITE_EXTERNAL_STORAGE" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private VideoView videoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        videoView = (VideoView) findViewById(R.id.video_view);
        Button play = (Button) findViewById(R.id.play);
        Button pause = (Button) findViewById(R.id.pause);
        Button replay = (Button) findViewById(R.id.replay);
        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        replay.setOnClickListener(this);
        // 因为你视频文件会放在 SD 卡上,所以需要动态申请权限
        if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STOREAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
        } else {
            initVideoPath(); // 初始化 VideoView
        }
    }

    private void initVideoPath() {
        File file = new File(Environment.getExternalStorageDirectory(), "movie.mp4");
        videoView.setVideoPath(file.getPath()); // 指定视频文件的路径
    }

    @Override
    public void onRequestPermissionResult(int requestCod, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case 1:
                if(grantResult.length > 0 && grantResults[0] == PackageManage.PERMISSION_GRANTED) {
                    initVideoPath();
                } else {
                    Toast.makeText(ths, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.play:
                if(!videoView.isPlaying()) {
                    videoView.start(); // 开始播放
                }
                break;
            case R.id.pause:
                if(videoView.isPlaying()) {
                    videoView.pause(); // 暂停播放
                }
                break;
            case R.id.replay:
                if(videoView.isPlayint()) {
                    videoView.resume(); // 从头播放
                }
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(videoView != null) {
            videoView.suspend(); // 将 VideoView 所占用的资源释放掉
        }
    }
}

11. 怎样理解 VideoView(场景、优缺点、底层原理)?

答:

  • VideoView 的底层仍然是使用 MediaPlayer 来对文件进行控制的,只不过 VideoView 做了一层很好的封装。
  • VideoView 并不是一个万能的视频播放工具类,它在视频格式的支持以及播放效率方面都存在着较大的不足
  • 所以,如果想要仅仅使用 VideoView 就编写出一个功能非常强大的视频播放器是不太现实的。但是如果只是用于播放一些游戏的片头动画,或者某个应用的视频宣传,使用 VideoView 还是绰绰有余的
-------------本文结束感谢您的阅读-------------