Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
398 lines (323 sloc) 12.6 KB

前言

坑坑坑, 终于可以下班了,
终于一个人 完成了 这个功能!!!
毫无 bug!!! 高仿 各大公司 视频播放器!!!
明天 贴代码!!!

2016-06-28 补充如下:

关于 Activity 方向旋转, 网上有很多说明.
主流操作 有几种:

  1. 重写 Activity.onSaveInstanceState(Bundle outState)Activity.onRestoreInstanceState(Bundle savedInstanceState)
  2. 重写 Activity.onConfigurationChanged(Configuration newConfig)
  3. 参考 http://www.curious-creature.com/2009/02/25/android-layout-trick-2-include-to-reuse/ 的 <include /> 设计

目前 CertainActivity 布局 中 有一个 VideoFragment 用来 播放视频,
VideoFragment 中 有一个 控制 CertainActivity 横屏并全屏的 代码.

<FrameLayout
	android:id="@+id/lytVideoContainer"
	android:layout_width="match_parent"
	android:layout_height="@dimen/monitor_video_height"
	android:background="#222222" />

第一种方式 我不是很喜欢,
尤其是业务要求 还得根据 手机 的 真实物理方向 进行 旋转.
所以 选择了第二种.
没选择 第三种 是因为, 第二种 已经写完了, 才被看到的 =_ =,
有时间 试一下去 可不可行.
第三种 方法 已测试, 不是很方便, 毕竟用了 ButterKnife 7,
需要加 @Nullable非空判断 ...

第二种 的 思路 大致是:
"劫持" 系统的 旋转 Activity 的 重构 (处理 运行时改变, 参看 ),
判断 当前物理方向, 通过 Java 代码 控制 Activity.setRequestedOrientation().
具体用法 可以参考 Activity.onConfigurationChanged(Configuration newConfig).

这里 全屏的本质 并不是 切换 layout-portlayout-land 中的 同名文件.
因为 这样与 onConfigurationChanged 本事 "运行时改变" 的 初衷相抵.
所以 使用 VideoFragment 变换 两个 View (横屏与竖屏).
实际做法是 引入 两个 <include />, 后面 有贴 代码 的.

直接上代码

首先要 处理运行时改变:
参考 https://developer.android.com/guide/topics/manifest/activity-element.html#config:
为什么 要写 screenSize, 里面也有 提及到.

<activity
    android:name=".CertainActivity"
    android:configChanges="orientation|screenSize"
    android:screenOrientation="sensor"/>

其次, CertainActivity 也要处理一下:

private boolean currentIsFull;

@Override
public void onConfigurationChanged(Configuration newConfig) {
	super.onConfigurationChanged(newConfig);

	boolean currentIsFullTemp = false;

	switch (newConfig.orientation) {
		// 因为 在 fullscreenViews(boolean changeToFull) 中 已经判断 真实设备方向,
		// 所以不用在这里考虑 REVERSE_LANDSCAPE
		case Configuration.ORIENTATION_LANDSCAPE:
			currentIsFullTemp = true;
			break;
		case Configuration.ORIENTATION_PORTRAIT:
			currentIsFullTemp = false;
			break;
	}

	// 因为 旋转之后 势必 要执行这个方法,
	// 所以 要堵住 "旋转死循环"
	if (this.currentIsFull == currentIsFullTemp) {
		return;
	}

	videoFragment.toggleFullScreen();
}

为了 保证 Activity 的方向 根据 手机物理方向 旋转,
需要加入 传感器 的相关代码,
注意, 这里 没有加入 REVERSE_LANDSCAPE 模式 (旋转 180°):

private int screenHeight;
private int screenWidth;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);

	initData();

	initView();

	initOrientation();
}

private void initData() {
//  WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
//  Display curDisplay = wm.getDefaultDisplay();
//  screenHeight = curDisplay.getHeight();
//  screenWidth = curDisplay.getWidth();

	DisplayMetrics dm = getResources().getDisplayMetrics();
	screenHeight = dm.heightPixels;
	screenWidth = dm.widthPixels;
}

private void initView() {
}

/**
 * 当前实时物理的方向
 */
@OrientationUtils.SurfaceRotation
private int currentOrientation;
/**
 * 用来记录上次的物理方向
 */
@OrientationUtils.SurfaceRotation
private int lastCurrentOrientation;

private OrientationEventListener myOrientationEventListener;

private void initOrientation() {
	lastCurrentOrientation = OrientationUtils.getRotation(this);

	myOrientationEventListener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL) {
		public void onOrientationChanged(int orientation) {
			currentOrientation = OrientationUtils.getCurrentOrientationByDegree(orientation);

			if (lastCurrentOrientation != currentOrientation) {
				lastCurrentOrientation = currentOrientation;

				setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
			}
		}
	};

	if (myOrientationEventListener.canDetectOrientation()) {
		myOrientationEventListener.enable();
	}
}

@Override
protected void onDestroy() {
	super.onDestroy();

	if (myOrientationEventListener.canDetectOrientation()) {
		myOrientationEventListener.disable();
	}
}

贴一下 我自己写的 OrientationUtils.java:

import android.app.Activity;
import android.support.annotation.IntDef;
import android.view.Surface;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Deal with real-time rotate orientation of device, <br/>
 * <font color=red>** NOT **</font> for `Screen Configuration Orientation`.
 *
 * @author imknown on 2016/5/31.
 */
public class OrientationUtils {

    /**
     * imknown: copied from {@link Surface}'s Rotation
     */
    @IntDef({Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SurfaceRotation {
    }

    /**
     * unit in Degree
     */
    private static final int SENSOR_DEGREE_DEFAULT_EDGE_RANGE = 45;

    /**
     * device orientation degree for ** ROTATE **, unit in Degree
     */
    private static final int SENSOR_ROTATE_DEGREE_90 = 90, SENSOR_ROTATE_DEGREE_180 = 180, SENSOR_ROTATE_DEGREE_270 = 270, SENSOR_ROTATE_DEGREE_360 = 360;

    @SurfaceRotation
    public static int getCurrentOrientationByDegree(int orientation) {
        int currentOrientation = Surface.ROTATION_0;

        if (orientation >= SENSOR_ROTATE_DEGREE_360 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE || orientation < SENSOR_ROTATE_DEGREE_90 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE) {
            currentOrientation = Surface.ROTATION_90;
        } else if (orientation >= SENSOR_ROTATE_DEGREE_90 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE && orientation < SENSOR_ROTATE_DEGREE_180 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE) {
            currentOrientation = Surface.ROTATION_180;
        } else if (orientation >= SENSOR_ROTATE_DEGREE_180 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE && orientation < SENSOR_ROTATE_DEGREE_270 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE) {
            currentOrientation = Surface.ROTATION_270;
        } else if (orientation >= SENSOR_ROTATE_DEGREE_270 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE && orientation < SENSOR_ROTATE_DEGREE_360 - SENSOR_DEGREE_DEFAULT_EDGE_RANGE) {
            currentOrientation = Surface.ROTATION_0;
        }

        return currentOrientation;
    }

    /**
     * current device real ** surface rotation ** orientation
     */
    @SurfaceRotation
    public static int getRotation(Activity activity) {
        return activity.getWindowManager().getDefaultDisplay().getRotation();
    }
}

然后是 VideoFragment 回调 CertainActivity 的 实现方法, 这里 其实 写早了,
因为 VideoFragment 代码 还冇有 贴出来.
当然 CertainActivity 就需要 实现 VideoFragment.IVideoControlListener:

/** 用来 存放 VideoFragment 的容器 */
FrameLayout lytVideoContainer;

@Override
public void fullscreenViews(boolean changeToFull) {
	// 因为 用户也可以 点击 全屏按钮,
	// 来进行 "全屏横屏" 或者 "恢复到竖屏" 的变换,
	// 所以 要堵住 "旋转死循环"
	if (this.currentIsFull == changeToFull) {
		return;
	}

	this.currentIsFull = changeToFull;

	if (changeToFull) {
		int rotation = OrientationUtils.getRotation(this);
		switch (rotation) {
			case Surface.ROTATION_0:
			case Surface.ROTATION_90:
				setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
				break;
			case Surface.ROTATION_270:
				setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
				break;
			case Surface.ROTATION_180:
				break;
		}

		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

		// 隐藏除了视频外的其他控件(View.GONE)
		xxxView.setVisibility(View.GONE);

		// 调整容器大小
		LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(screenHeight, screenWidth);
		lytVideoContainer.setLayoutParams(layoutParams);
	} else {
		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

		getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

		// 显示除了视频外的其他控件(View.VISIBLE)
		xxxView.setVisibility(View.VISIBLE);

		// 调整容器大小
		LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, oriVideoContainerHeight);
		lytVideoContainer.setLayoutParams(layoutParams);
	}
}

当然, VideoFragment 这一头 也有 要事 要处理:
首先 xml 布局 大概如下:

<?xml version="1.0" encoding="utf-8"?>

<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:background="@color/colorBlack">

    <!--视频播放界面-->
    <SurfaceView
        android:id="@+id/viewVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ProgressBar
        android:id="@+id/progressVideo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

    <include
        android:id="@+id/icd_videoPortrait"
        layout="@layout/include_video_portrait"
        android:visibility="visible" />

    <include
        android:id="@+id/icd_VideoLandscape"
        layout="@layout/include_video_landscape"
        android:visibility="gone" />

</RelativeLayout>

然后 VideoFragment.java 核心的逻辑:

RelativeLayout icd_videoPortrait;
RelativeLayout icd_VideoLandscape;

/** 非全屏(竖屏) 状态下 SurfaceView 的宽高 */
private int normalVideoWidth;
private int normalVideoHeight;

/** 当前是否 处于 全屏状态(横屏) */
private boolean currentIsFullScreen = false;

/**
 * 与Activity通信的接口
 */
interface IVideoControlListener {
	/**
	 * 旋转至全屏 或者 恢复到竖屏
	 */
	void fullscreenViews(boolean changeToFull);
}

@Override
public void onAttach(Context context) {
	super.onAttach(context);
	
	this.videoControlListener = (IVideoControlListener) context;
}

/**
 * 全屏切换
 */
@OnClick(R.id.lytFullScreen)
public void toggleFullScreen() {
	if (currentIsFullScreen) {
		// 当前是全屏, 恢复到竖屏

		icd_VideoLandscape.setVisibility(View.GONE);
		icd_videoPortrait.setVisibility(View.VISIBLE);

		videoView.setLayoutParams(new RelativeLayout.LayoutParams(normalVideoWidth, normalVideoHeight));
		
		currentIsFullScreen = false;
	} else {
		// 当前是竖屏, 切换到全屏
		
		icd_VideoLandscape.setVisibility(View.VISIBLE);
		icd_videoPortrait.setVisibility(View.GONE);

		// 调整 SurfaceView 大小, 以适应 视频分辨率
		normalVideoWidth = videoView.getWidth();
		normalVideoHeight = videoView.getHeight();
		int videoWidth = mMediaPlayer.getVideoWidth();
		int videoHeight = mMediaPlayer.getVideoHeight();
		int resultWidth = videoWidth;
		int resultHeight = videoHeight;
		float wRatio = (float) videoWidth / screenHeight;
		float hRatio = (float) videoHeight / screenWidth;
		float ratio = Math.max(wRatio, hRatio);
		resultWidth = (int) Math.ceil((float) videoWidth / ratio);
		resultHeight = (int) Math.ceil((float) videoHeight / ratio);

		RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(resultWidth, resultHeight);
		layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
		videoView.setLayoutParams(layoutParams);
		
		currentIsFullScreen = true;
	}

	if (videoControlListener != null) {
		videoControlListener.fullScreen(currentIsFullScreen);
	}
}

OK, 核心逻辑 处理完毕. 物理旋转用户点击 基本完美~

参考: https://developer.android.com/guide/topics/resources/runtime-changes.html?hl=en

Update 2016-09-06:
https://lankton.github.io/2016/07/09/【Android】自定义相机的实现(支持连续拍照、前后摄像头切换、连续对焦)/