集册 Android&Java 技术笔记 Building Apps with Multimedia

Building Apps with Multimedia

—— 安卓官方开发指南

欢马劈雪     最近更新时间:2020-08-04 05:37:59

103

Building Apps with Multimedia

  • Managing Audio Playback

    • 控制音量和播放
      • Audio Stream:安卓系统为不同的用途维护了不同的audio stream,便于用户控制不同类型声音的音量
        • music
        • alarm
        • notification
        • 来电话
        • system sound
        • 打电话过程中的声音
        • DTMF tones
      • 通过设置setVolumeControlStream(),系统将在Activity/Fragment仍在界面上时,自动响应设备的音量操作键,增大或减小设置类型媒体的音量
      • 当用户通过耳机等外设,按下播放控制按键时,例如:播放/暂停、上一曲/下一曲,系统将发送一个action为android.intent.action.MEDIA_BUTTON的广播,如下BroadcastReceiver可以响应处理这一广播(需要在AndroidManifest.xml中声明):
          public class RemoteControlReceiver extends BroadcastReceiver {
              @Override
              public void onReceive(Context context, Intent intent) {
                  if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
                      KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
                      if (KeyEvent.KEYCODE_MEDIA_PLAY == event.getKeyCode()) {
                          // Handle key press.
                      }
                  }
              }
          }
    • Managing Audio Focus

      • 请求/释放audio focus

          AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
          ...
        
          // Request audio focus for playback
          int result = am.requestAudioFocus(afChangeListener,
                                          // Use the music stream.
                                          AudioManager.STREAM_MUSIC,
                                          // Request permanent focus.
                                          AudioManager.AUDIOFOCUS_GAIN);
        
          if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
              am.registerMediaButtonEventReceiver(RemoteControlReceiver);
              // Start playback.
          }
        
          ...
          // Abandon audio focus when playback complete    
          am.abandonAudioFocus(afChangeListener);
    • requestAudioFocus的最后一个参数可以设为AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,用于请求短暂的audio focus,允许其他app在失去audio focus时继续播放音乐(但应该降低音量)
    • 监听audio focus状态改变
      OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
          public void onAudioFocusChange(int focusChange) {
              if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT
                  // Pause playback
              } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                  // Resume playback 
              } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                  am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
                  am.abandonAudioFocus(afChangeListener);
                  // Stop playback
              }
          }
      };
    • 响应临时失焦且允许重音的情形
      OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
          public void onAudioFocusChange(int focusChange) {
              if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                  // Lower the volume
              } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                  // Raise it back to normal
              }
          }
      };
    • 检查声音播放设备
      if (audioManager.isBluetoothA2dpOn()) {
          // A2DP audio routing to the Bluetooth headset
      } else if (audioManager.isSpeakerphoneOn()) {
          // Adjust output for Speakerphone.
      } else if (audioManager.isWiredHeadsetOn()) {
          // Adjust output for headsets
      } else if (audioManager.isBluetoothScoOn()) {
          // SCO is used for communications
      } else { 
          // If audio plays and noone can hear it, is it still playing?
      }
    • 一旦耳机/蓝牙耳机断开连接,系统将继续使用默认设备(扬声器)播放,同时系统会发送一个AudioManager.ACTION_AUDIO_BECOMING_NOISY广播,可以通过以下代码进行响应

      private class BroadcastReceiver myNoisyAudioStreamReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
                  // Pause the playback
              }
          }
      };
      
      private void startPlayback() {
          registerReceiver(myNoisyAudioStreamReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
      }
      
      private void stopPlayback() {
          unregisterReceiver(myNoisyAudioStreamReceiver);
      }
  • Capturing Photos

    • 使用已有相机APP拍照

      • 声明使用相机的feature,注意,并非权限,便于google play等应用商店确定设备是否可以安装本应用
          <manifest ... >
              <uses-feature android:name="android.hardware.camera" android:required="true" />
              ...
          </manifest>
      • 也可以设置required为false,手动检查设备是否有相机:packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
      • 发送Intent调起已有相机APP拍照,发送Intent之前需要检查是否有其他APP可以响应此Intent,如没有却调用了startActivity,将会抛出异常

          static final int REQUEST_IMAGE_CAPTURE = 1;
        
          private void dispatchTakePictureIntent() {
              Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
              if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
                  startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
              }
          }

        在发送takePictureIntent之前,可以手动设置要照片要保存的位置takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageFileUri);,拍照成功返回之后,可以直接访问该uri。

      • 获取拍照结果缩略图
          @Override
          protected void onActivityResult(int requestCode, int resultCode, Intent data) {
              if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
                  Bundle extras = data.getExtras();
                  Bitmap imageBitmap = (Bitmap) extras.get("data");
                  mImageView.setImageBitmap(imageBitmap);
              }
          }
      • 获取完整照片
        照片保存在外置存储卡的公开区域,需要声明权限,WRITE_EXTERNAL_STORAGE包含了READ_EXTERNAL_STORAGE权限,目录路径通过Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)函数获得

          <manifest ...>
              <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
              ...
          </manifest>

        如果要保存在APP私有目录下,API 18之后,将不用声明该权限,目录路径通过context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)函数获得,app访问自己对应的该目录,从API 19起,将不需要任何权限,但是访问其他APP对应的目录时,需要WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE权限,该目录不一定任何时候都可以访问,也不具备安全性

          <manifest ...>
              <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                              android:maxSdkVersion="18" />
              ...
          </manifest>

        拍照设置保存文件

          String mCurrentPhotoPath;
        
          private File createImageFile() throws IOException {
              // Create an image file name
              String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
              String imageFileName = "JPEG_" + timeStamp + "_";
              File storageDir = Environment.getExternalStoragePublicDirectory(
                      Environment.DIRECTORY_PICTURES);
              File image = File.createTempFile(
                  imageFileName,  /* prefix */
                  ".jpg",         /* suffix */
                  storageDir      /* directory */
              );
        
              // Save a file: path for use with ACTION_VIEW intents
              mCurrentPhotoPath = "file:" + image.getAbsolutePath();
              return image;
          }
        
          static final int REQUEST_TAKE_PHOTO = 1;
        
          private void dispatchTakePictureIntent() {
              Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
              // Ensure that there's a camera activity to handle the intent
              if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
                  // Create the File where the photo should go
                  File photoFile = null;
                  try {
                      photoFile = createImageFile();
                  } catch (IOException ex) {
                      // Error occurred while creating the File
                      ...
                  }
                  // Continue only if the File was successfully created
                  if (photoFile != null) {
                      takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                              Uri.fromFile(photoFile));
                      startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
                  }
              }
          }
      • 加入Gallery,当保存路径设为context.getExternalFilesDir(type)时,将无法加入Gallery
          private void galleryAddPic() {
              Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
              File f = new File(mCurrentPhotoPath);
              Uri contentUri = Uri.fromFile(f);
              mediaScanIntent.setData(contentUri);
              this.sendBroadcast(mediaScanIntent);
          }
      • 获取压缩后的图片,用于显示在ImageView上

          private void setPic() {
              // Get the dimensions of the View
              int targetW = mImageView.getWidth();
              int targetH = mImageView.getHeight();
        
              // Get the dimensions of the bitmap
              BitmapFactory.Options bmOptions = new BitmapFactory.Options();
              bmOptions.inJustDecodeBounds = true;
              BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
              int photoW = bmOptions.outWidth;
              int photoH = bmOptions.outHeight;
        
              // Determine how much to scale down the image
              int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
        
              // Decode the image file into a Bitmap sized to fill the View
              bmOptions.inJustDecodeBounds = false;
              bmOptions.inSampleSize = scaleFactor;
              bmOptions.inPurgeable = true;
        
              Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
              mImageView.setImageBitmap(bitmap);
          }
    • 使用已有相机应用录制视频,与拍照类似,需要发送的Intent初始化为Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);onActivityResult返回的intent的data数据,就是录制视频的Uri,intent.getData()
    • 直接使用相机

      • Camera API

        • 权限
          <uses-permission android:name="android.permission.CAMERA" />  
          <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
          <uses-feature android:name="android.hardware.camera" android:required="true" />
        • 在onResume中inflate SurfaceView,动态添加到layout中,在onPause中停止预览,移除SurfaceView,以解决界面onPause后再onResume就无法预览的问题。在SurfaceView的surfaceCreated回调中打开相机,开始预览

          @Override
          public void surfaceCreated(SurfaceHolder holder) {
              mSurfaceHolder = holder;
              initPreview(holder);
          }
          
          @Override
          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
          
          }
          
          @Override
          public void surfaceDestroyed(SurfaceHolder holder) {
              releaseResources();
          }
          
          @Override  
          protected void onResume() {  
              mSurfaceView = (SurfaceView) LayoutInflater.from(getActivity())
                      .inflate(R.layout.ui_surface_view, null);
              flContainer.addView(mSurfaceView, 0);
              isPreview = true;
              SurfaceHolder mSurfaceHolder = mSurfaceView.getHolder();
              mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
              mSurfaceHolder.setKeepScreenOn(true);
              mSurfaceHolder.addCallback(this);
          }
        • 打开相机,开始预览

          private boolean initPreview(SurfaceHolder holder) {
              int mSupportVideoFormat[] = {ImageFormat.NV21, ImageFormat.YV12};
              try {
                  cameraid = getCameraId(isFrontCamera);
                  mCamera = Camera.open(cameraid);
              } catch (Exception e) {
                  e.printStackTrace();
                  return false;
              }
          
              if (mCamera == null) {
                  ToastUtils.toastResId(R.string.error_init_player_fail);
                  return false;
              }
          
              // change to portrait record
              setCameraDisplayOrientation(cameraid, mCamera);
              try {
                  mCamera.setPreviewDisplay(holder);
              } catch (IOException e) {
                  ToastUtils.toastResId(R.string.error_IO_error);
                  e.printStackTrace();
                  return false;
              }
              Camera.Parameters parameters = mCamera.getParameters();
              parameters.setPreviewSize(width, height);
              parameters.setPictureSize(width, height);
              if (isLightOn) {
                  parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
              }
          
              int actualFormat = 0;
              List<Integer> list = parameters.getSupportedPreviewFormats();
              for (int format : mSupportVideoFormat) {
                  for (Integer i : list) {
                      Timber.i("startVideoCapture " + "suport format:" + i);
                      if (format == i.intValue()) {
                          actualFormat = format;
                          break;
                      }
                  }
                  if (actualFormat != 0) {
                      break;
                  }
              }
              if (actualFormat == 0) {
                  Timber.e("startVideoCapture" + " no suport format be found");
                  return false;
              }
              parameters.setPreviewFormat(actualFormat);// ImageFormat.YV12
              try {
                  mCamera.setParameters(parameters);
                  mCamera.startPreview();
              } catch (Exception e) {
                  e.printStackTrace();
                  return false;
              }
          
              return true;
          }
      • Google Camera2 Sample,简版

        • 权限
          <uses-sdk  
              android:minSdkVersion="21"  
              android:targetSdkVersion="21" />  
          <uses-permission android:name="android.permission.CAMERA" />  
          <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
          <uses-feature android:name="android.hardware.camera2.full" /> 
        • layout

          <RelativeLayout 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"
              android:paddingBottom="@dimen/activity_vertical_margin"
              android:paddingLeft="@dimen/activity_horizontal_margin"
              android:paddingRight="@dimen/activity_horizontal_margin"
              android:paddingTop="@dimen/activity_vertical_margin"
              tools:context="com.example.camera2te.MainActivity" >
          
              <TextureView
                  android:id="@+id/texture"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:layout_alignParentStart="true"
                  android:layout_alignParentTop="true" />
          </RelativeLayout>
        • 设置TextureView回调,在onSurfaceTextureAvailable回调中打开相机

          private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener(){  
          
              @Override  
              public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {  
                  Log.e(TAG, "onSurfaceTextureAvailable, width="+width+",height="+height);  
                  openCamera();  
              }  
          
              @Override  
              public void onSurfaceTextureSizeChanged(SurfaceTexture surface,  
                      int width, int height) {  
          
              }  
          
              @Override  
              public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {  
                  return false;  
              }  
          
              @Override  
              public void onSurfaceTextureUpdated(SurfaceTexture surface) {  
          
              }  
          
          };
          
          @Override  
          protected void onResume() {  
              ...  
              mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
              ...
          }
        • 打开相机,在相机回调中开始预览

          private void openCamera() {  
              CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); 
              try {  
                  String cameraId = manager.getCameraIdList()[0];  
                  CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);  
                  StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);  
                  mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0];  
          
                  manager.openCamera(cameraId, mStateCallback, null);  
              } catch (CameraAccessException e) {  
                  e.printStackTrace();  
              }
          }  
          
          private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {  
          
              @Override  
              public void onOpened(CameraDevice camera) {    
                  mCameraDevice = camera;  
                  startPreview();  
              }  
          
              @Override  
              public void onDisconnected(CameraDevice camera) {  
          
              }  
          
              @Override  
              public void onError(CameraDevice camera, int error) {  
          
              }  
          
          };  
        • 开启预览

          protected void startPreview() {  
              if(null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {  
                  Log.e(TAG, "startPreview fail, return");  
                  return;  
              }  
          
              SurfaceTexture texture = mTextureView.getSurfaceTexture();  
              if(null == texture) {  
                  Log.e(TAG,"texture is null, return");  
                  return;  
              }  
          
              texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());  
              Surface surface = new Surface(texture);  
          
              try {  
                  mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);  
              } catch (CameraAccessException e) {  
          
                  e.printStackTrace();  
              }  
              mPreviewBuilder.addTarget(surface);  
          
              try {  
                  mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {  
          
                      @Override  
                      public void onConfigured(CameraCaptureSession session) {  
          
                          mPreviewSession = session;  
                          updatePreview();  
                      }  
          
                      @Override  
                      public void onConfigureFailed(CameraCaptureSession session) {  
          
                          Toast.makeText(MainActivity.this, "onConfigureFailed", Toast.LENGTH_LONG).show();  
                      }  
                  }, null);  
              } catch (CameraAccessException e) {  
          
                  e.printStackTrace();  
              }  
          }  
          
          protected void updatePreview() {  
              if(null == mCameraDevice) {  
                  Log.e(TAG, "updatePreview error, return");  
              }  
          
              mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);  
              HandlerThread thread = new HandlerThread("CameraPreview");  
              thread.start();  
              Handler backgroundHandler = new Handler(thread.getLooper());  
          
              try {  
                  mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, backgroundHandler);  
              } catch (CameraAccessException e) {  
          
                  e.printStackTrace();  
              }  
          }  
  • Printing Content, >= API 19

    • 打印图片
      PrintHelper photoPrinter = new PrintHelper(getActivity());
      photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
      Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
              R.drawable.droids);
      photoPrinter.printBitmap("droids.jpg - test print", bitmap);
    • ScaleMode
      • SCALE_MODE_FIT:等比例缩放图片,使之可以在打印区域内显示
      • SCALE_MODE_FILL:充满打印区域,上下/左右可能会有部分内容无法打印
    • 打印HTML文档

      private WebView mWebView;
      
      private void doWebViewPrint() {
          // Create a WebView object specifically for printing
          WebView webView = new WebView(getActivity());
          webView.setWebViewClient(new WebViewClient() {
      
                  public boolean shouldOverrideUrlLoading(WebView view, String url) {
                      return false;
                  }
      
                  @Override
                  public void onPageFinished(WebView view, String url) {
                      Log.i(TAG, "page finished loading " + url);
                      createWebPrintJob(view);
                      mWebView = null;
                  }
          });
      
          // Generate an HTML document on the fly:
          String htmlDocument = "<html><body><h1>Test Content</h1><p>Testing, " +
                  "testing, testing...</p></body></html>";
          webView.loadDataWithBaseURL(null, htmlDocument, "text/HTML", "UTF-8", null);
      
          // Keep a reference to WebView object until you pass the PrintDocumentAdapter
          // to the PrintManager
          mWebView = webView;
      }
      
      private void createWebPrintJob(WebView webView) {
      
          // Get a PrintManager instance
          PrintManager printManager = (PrintManager) getActivity()
                  .getSystemService(Context.PRINT_SERVICE);
      
          // Get a print adapter instance
          PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();
      
          // Create a print job with name and adapter instance
          String jobName = getString(R.string.app_name) + " Document";
          PrintJob printJob = printManager.print(jobName, printAdapter,
                  new PrintAttributes.Builder().build());
      
          // Save the job object for later status checking
          mPrintJobs.add(printJob);
      }
    • 自定义文档(内容)打印:
      • 可以先把View画到bitmap中,然后打印bitmap
      • 也可以实现PrintDocumentAdapter,打印自定义内容
展开阅读全文