第十章 多媒体应用

Android 多媒体框架支持捕获和编码各种常见的音频和视频格式。 

10.1 MediaRecorder概述

用于录制音频和视频的一个类。

10.1.1 状态转换图

Android 使用popupwindow_xml

说明:

下面是关于MediaRecorder状态图的各个状态的介绍:

Initial:初始状态,当使用new()方法创建一个MediaRecorder对象或者调用了reset()方法时,该MediaRecorder对象处于Initial状态。在设定视频源或者音频源之后将转换为Initialized状态。另外,在除Released状态外的其它状态通过调用reset()方法都可以使MediaRecorder进入该状态。

Initialized:已初始化状态,可以通过在Initial状态调用setAudioSource()或setVideoSource()方法进入该状态。在这个状态可以通过setOutputFormat()方法设置输出格式,此时MediaRecorder转换为DataSourceConfigured状态。另外,通过reset()方法进入Initial状态。

DataSourceConfigured:数据源配置状态,这期间可以设定编码方式、输出文件、屏幕旋转、预览显示等等。可以在Initialized状态通过setOutputFormat()方法进入该状态。另外,可以通过reset()方法回到Initial状态,或者通过prepare()方法到达Prepared状态。

Prepared:就绪状态,在DataSourceConfigured状态通过prepare()方法进入该状态。在这个状态可以通过start()进入录制状态。另外,可以通过reset()方法回到Initialized状态。

Recording:录制状态,可以在Prepared状态通过调用start()方法进入该状态。另外,它可以通过stop()方法或reset()方法回到Initial状态。

Released:释放状态(官方文档给出的词叫做Idle state 空闲状态),可以通过在Initial状态调用release()方法来进入这个状态,这时将会释放所有和MediaRecorder对象绑定的资源。

Error:错误状态,当错误发生的时候进入这个状态,它可以通过reset()方法进入Initial状态。

提示:使用MediaRecorder录音录像时需要严格遵守状态图说明中的函数调用先后顺序,在不同的状态调用不同的函数,否则会出现异常。

10.1.2 使用MediaRecorder的使用

1、添加权限

<!-- 授予该程序录制声音的权限 -->
	<uses-permission android:name="android.permission.RECORD_AUDIO"/>
	<!-- 授予该程序使用摄像头的权限 -->	
	<uses-permission android:name="android.permission.CAMERA"/>

2、MainActivity.java(含申请权限) 

import android.Manifest;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();
    }

    public void init(){
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA},
                1000);

        Button btn_record = findViewById(R.id.btn_record);
        Button btn_playVideo = findViewById(R.id.btn_playVideo);
        Button btn_playAudio = findViewById(R.id.btn_playAudio);
        Button btn_playVideoVV = findViewById(R.id.btn_playVideoVV);

        btn_record.setOnClickListener(this);
        btn_playVideo.setOnClickListener(this);
        btn_playAudio.setOnClickListener(this);
        btn_playVideoVV.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.btn_record:
                startActivity(new Intent(this, RecordActivity.class));
                break;
            case R.id.btn_playVideo:
                startActivity(new Intent(this, VideoActivity.class));
                break;
            case R.id.btn_playVideoVV:
                startActivity(new Intent(this, VideoViewActivity.class));
                break;
            case R.id.btn_playAudio:
                startActivity(new Intent(this, RecordActivity.class));
                break;
        }
    }
}

3、RecordActivity.java(问题:声音和图像分开播放,未解决)

import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.IOException;

public class RecordActivity extends AppCompatActivity implements View.OnClickListener {
//    private SurfaceView surfaceView;
    private TextureView textureView;
    private Button btn_opt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_record);

//        surfaceView = findViewById(R.id.surfaceView);
        textureView = findViewById(R.id.textureView);

        btn_opt = findViewById(R.id.btn_opt);
        btn_opt.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        CharSequence btn_optText = btn_opt.getText();
        MediaRecorder mediaRecorder = null;
        Camera camera = Camera.open();
        if (TextUtils.equals("开始", btn_optText)){
            btn_opt.setText("结束");
            camera.setDisplayOrientation(90); //竖屏
            camera.unlock();
            mediaRecorder = new MediaRecorder();
            mediaRecorder.setCamera(camera);
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); //设置音频源 麦克风
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //设置视频源 麦克风
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //设置视频输出格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置音频编码格式
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP); //设置视频格式
            mediaRecorder.setOrientationHint(90); //设置文件正向
            mediaRecorder.setOutputFile(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath()); //设置文件输出路径
            mediaRecorder.setVideoSize(640, 480); //设置视频显示大小
//            mediaRecorder.setPreviewDisplay(surfaceView.getHolder().getSurface()); //低版本不行
            mediaRecorder.setPreviewDisplay(new Surface(textureView.getSurfaceTexture()));
            try {
                mediaRecorder.prepare(); //录制准备
            } catch (IOException e) {
                e.printStackTrace();
            }
            mediaRecorder.start(); //开始录制
        } else {
            btn_opt.setText("开始");
            mediaRecorder.stop();
            mediaRecorder.release(); //释放mediaRecorder
            camera.stopPreview();
            camera.release();
        }
    }
}

4、activity_record.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RecordActivity">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/btn_opt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始"
        app:layout_constraintBottom_toBottomOf="@+id/textureView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

10.2 MediaPlayer简介

MediaPlayer类是媒体框架最重要的组成部分之一。此类的对象能够获取、解码以及播放音频和视频,而且只需极少量设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部URI,例如您可能从内容解析器那获取的URI·外部网址(流式传输)

媒体格式列表:https://developer.android.google.cn/guide/topics/media/media-formats?hl=zh_cn

10.2.1 状态转换图

Android 使用popupwindow_xml_02

10.2.2 自定义

1、VideoActivity.java

import android.media.MediaPlayer;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.IOException;

public class VideoActivity extends AppCompatActivity implements View.OnClickListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {
    private TextureView textureView;
    private Button btn_opt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video);

        textureView = findViewById(R.id.textureView);

        btn_opt = findViewById(R.id.btn_opt);
        btn_opt.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        CharSequence btn_optText = btn_opt.getText();
        MediaPlayer mediaPlayer = null;
        if (TextUtils.equals("开始", btn_optText)) {
            btn_opt.setText("结束");
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setOnPreparedListener(this); //加载完成监听
            mediaPlayer.setOnCompletionListener(this); //播放完成监听
            try {
                mediaPlayer.setDataSource(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
            }
            mediaPlayer.setSurface(new Surface(textureView.getSurfaceTexture())); //播放画布
            mediaPlayer.prepareAsync(); //异步加载
        } else {
            btn_opt.setText("开始");
            mediaPlayer.stop();
            mediaPlayer.release();
        }
    }

    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        mediaPlayer.start(); //播放
    }

    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        btn_opt.setText("开始");
        mediaPlayer.stop();
        mediaPlayer.release();
    }
}

2、activity_video.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RecordActivity">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

10.2.3 Android自带的VideoView

1、VideoViewActivity.java

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.MediaController;
import android.widget.VideoView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;

public class VideoViewActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_videoview);

        VideoView videoView = findViewById(R.id.videoView);

        MediaController mediaController = new MediaController(this);
        mediaController.setPrevNextListeners(this, this); //上一曲、下一曲
        videoView.setMediaController(mediaController);
        videoView.setVideoPath(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath());
        videoView.start();
    }

    @Override
    public void onClick(View view) {
        Log.i("VideoView", "======");
    }
}

2、activity_videoview.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".VideoViewActivity">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

10.3 SoundPool音效播放

10.3.1 SoundPool简介

MediaPlayer虽然也能播放音频,但是它有资源占用量较高、延迟时间较长、不支持多个音频同时播放等缺点。这些缺点决定了MediaPlayer在某些场合的使用情况不会很理想,例如在对时间精准度要求相对较高的场景。而SoundPool一般用来播放密集、急促而又短暂的音效,比如:"滴滴一下,马上出发”。

10.3.2 使用SounPool

1、Music.java

public class Music {
    private String name;
    private int musicId;

    public Music() {
    }

    public Music(String name, int musicId) {
        this.name = name;
        this.musicId = musicId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getMusicId() {
        return musicId;
    }

    public void setMusicId(int musicId) {
        this.musicId = musicId;
    }

    @Override
    public String toString() {
        return "Music{" +
                "name='" + name + '\'' +
                ", musicId=" + musicId +
                '}';
    }
}

2、创建SoundActivity

activity_sound.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".SoundActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/teal_200"/>

</androidx.constraintlayout.widget.ConstraintLayout>

3、music_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@color/red">

    <TextView
        android:id="@+id/tv_musicName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"/>
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/black"/>

</LinearLayout>

4、MusicAdamter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class MusicAdapter extends RecyclerView.Adapter<MusicAdapter.ViewHolder> {
    private List<Music> musicList;
    private Context context;

    public MusicAdapter(List<Music> musicList, Context context){
        this.musicList = musicList;
        this.context = context;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        //用来创建ViewHolder实例,再将加载好的布局传入构造函数,最后返回ViewHolder实例
//        View view = View.inflate(context, R.layout.music_item, null);
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.music_item, parent, false); //解决宽度不能铺满
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = view.getVerticalScrollbarPosition();
                Toast.makeText(context, musicList.get(position).getName(), Toast.LENGTH_SHORT).show();
            }
        });
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.textView.setText(musicList.get(position).getName());
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return musicList.size();
    }

    public interface OnItemClickListener{
        void onItemClick(View view, int position);
    }
    private OnItemClickListener onItemClickListener;
    public void setOnItemClickListener(MusicAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }


    public class ViewHolder extends RecyclerView.ViewHolder{
        protected TextView textView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv_musicName);
        }
    }
}

5、SoundActivity.java

import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class SoundActivity extends AppCompatActivity {

    private List<Music> musicList = new ArrayList<>();
    private SoundPool soundPool;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sound);

        initMusics();

        RecyclerView recyclerView = findViewById(R.id.recyclerView);

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);  //设置方向
        recyclerView.setLayoutManager(layoutManager);
        MusicAdapter musicAdapter = new MusicAdapter(musicList, this);
        musicAdapter.setOnItemClickListener(new MusicAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(SoundActivity.this, "点击了"+musicList.get(position).getName(), Toast.LENGTH_SHORT).show();
                Music music = musicList.get(position);
                AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
                int volumem = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
                int volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                soundPool.play(music.getMusicId(), volumem, volume, 1, 1, 1);
            }
        });
        recyclerView.setAdapter(musicAdapter);
    }

    private void initMusics() {
        soundPool = new SoundPool.Builder().setMaxStreams(6).build();
        for (int i = 0; i < 6; i++) {
            musicList.add(new Music("a"+(i+1), soundPool.load(this, R.raw.a1+i, 1))); //不建议
        }

//        musicList.add(new Music("a2", soundPool.load(this, R.raw.a2, 1)));
//        musicList.add(new Music("a3", soundPool.load(this, R.raw.a3, 1)));
//        musicList.add(new Music("a4", soundPool.load(this, R.raw.a4, 1)));
//        musicList.add(new Music("a5", soundPool.load(this, R.raw.a5, 1)));
//        musicList.add(new Music("a6", soundPool.load(this, R.raw.a6, 1)));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        for (Music music : musicList) {
            soundPool.unload(music.getMusicId());
        }
        soundPool.release();
    }

音效资源链接:https://pan.baidu.com/s/1yIY5l7xQf0kG7lO_U6A4uQ 
提取码:neyt

第十一章 项目发布

11.1 项目安全

11.1.1 加固

为什么应用需要加固:防止应用被逆向分析、反编译、二次打包,防止嵌入各类病毒、广告等恶意代码,从源头保护数据安全和开发者利益。

11.1.2 设置多渠道

1、统计各个渠道包的情况,例如哪个渠道的下载量更大,哪个渠道下载的客户活跃度或者粘性更高等信息。
2、针对不同的渠道做一些不同的操作。
目前常用的多渠道打包工具有三种:

  • 友盟
  • 美团
  • 360

11.1.3 生成release apk

 

Android 使用popupwindow_ide_03

常用:【Build】-->【Generate Signed Bundle / APK...】-->【APK】-->【Next】--> 【Key story path】-->【Next】-->【使用加固工具进行加固】 -->【应用发布】