前言
目前在Android開發(fā)的時(shí)候有一種需求:
保持設(shè)備朝向鎖定的時(shí)候,需要旋轉(zhuǎn)控件.
比如說,當(dāng)前手機(jī)是豎屏鎖定的,但是界面上顯示的所有的布局和控件,都要是橫屏的朝向,因?yàn)橐層脩舾杏X到App已經(jīng)切換到了橫屏.
麻煩點(diǎn)
對(duì)于普通界面上的一個(gè)View,直接View.setRotation(rotation)就ok.
可是如果這是Dialog的話,則會(huì)有一下2點(diǎn)難以實(shí)現(xiàn)的地方:
-
getWindow()拿到window對(duì)象之后,無法直接旋轉(zhuǎn). 如果通過獲取getDecorView(),然后旋轉(zhuǎn)DecorView也無法產(chǎn)生效果. -
getWindow().getAttributes().screenOrientation的屬性,就算設(shè)置了橫屏或者豎屏,對(duì)結(jié)果沒有影響,無法產(chǎn)生效果.
實(shí)現(xiàn)辦法
在無法直接旋轉(zhuǎn)Window以后,我想到可以重新設(shè)置window的大小,然后配合旋轉(zhuǎn)window里面根布局,最后重新設(shè)置根View的大小,以填充整個(gè)window, 來達(dá)到類似效果.
效果如下圖:


代碼已上傳到Github;
Demo下載
文末也會(huì)附上2個(gè)Base類源碼.
具體思路
如Google所說,使用DialogFragment來實(shí)現(xiàn)對(duì)話框.所以這里的實(shí)現(xiàn),都是繼承自DialogFragment. 但是對(duì)于Dialog來說,分為2種:
- 直接使用AlertDialog顯示簡單的信息.
- 自定義布局實(shí)現(xiàn)復(fù)雜的界面的DialogFragment.
由于AlertDialog的默認(rèn)布局使用的是
window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
所以,對(duì)于第一種情況, 在默認(rèn)方向和旋轉(zhuǎn)180°方向上, 使用系統(tǒng)默認(rèn)的wrap_content的寬高,只需要旋轉(zhuǎn)里面根View的方向即可.而在向左或向右旋轉(zhuǎn)90°的時(shí)候,需要自定義一個(gè)寬高值, 并將此值作為window的寬高,和根布局旋轉(zhuǎn)后的寬高(也就是根布局的"高"和"寬",因?yàn)樾D(zhuǎn)之后會(huì)和window重合).
第二種情況同理. 但是需要在四個(gè)方向上都自定義寬高. 即用如下代碼設(shè)置,
getDialog().getWindow().setLayout(width, height);
同時(shí)在旋轉(zhuǎn)的同時(shí)進(jìn)行x軸和y軸的平移, 將旋轉(zhuǎn)后的左上角移動(dòng)到window的左上角,使之重合.
實(shí)現(xiàn)
確定4個(gè)方向,分別用如下4個(gè)值來表示
//手機(jī)豎直朝向
android.view.Surface#ROTATION_0
//手機(jī)豎直往右旋轉(zhuǎn)90°朝向
android.view.Surface#ROTATION_90
//手機(jī)豎直且旋轉(zhuǎn)180°朝向
android.view.Surface#ROTATION_180
//手機(jī)豎直往左旋轉(zhuǎn)90°朝向
android.view.Surface#ROTATION_270
而在接下來的具體旋轉(zhuǎn)中,都是依據(jù)這4個(gè)方向來確定的.
因?yàn)橛?種布局,所以定義了2個(gè)基類
- BaseDefaultContentDialog 使用系統(tǒng)布局的Dialog
- BaseCustomContentDialog 使用自定義布局的Dialog
而這2個(gè)基類繼承自BaseDialogFragment, 這個(gè)基類很簡單,代碼如下
public abstract class BaseDialogFragment extends DialogFragment {
protected int mRotation;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
// request a window without the title
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
return dialog;
}
/**
* 設(shè)置旋轉(zhuǎn).Dialog根據(jù)rotation的角度來旋轉(zhuǎn)方向
*
* @param rotation 旋轉(zhuǎn)的方向,只能為以下四個(gè)值之一:
*
* @see android.view.Surface#ROTATION_0
* @see android.view.Surface#ROTATION_90
* @see android.view.Surface#ROTATION_180
* @see android.view.Surface#ROTATION_270
*/
public abstract void setRotation(int rotation);
/**
* 顯示Dialog
* @param rotation 當(dāng)前希望顯示的方向
*/
public void show(FragmentManager manager,int rotation) {
mRotation = rotation;
super.show(manager, getClass().getSimpleName());
}
}
可以看到,這個(gè)基類需要兩件事情
1.設(shè)置無標(biāo)題模式
2.設(shè)置當(dāng)前Dialog的mRotation的數(shù)值. 子類在顯示的時(shí)候,需要依此來確定初始方向.
BaseDefaultContentDialog
在使用系統(tǒng)布局的Dialog上面, 根布局采用的是AlertDialogLayout的父布局mContent = window.findViewById(android.R.id.content);.
由于是系統(tǒng)默認(rèn)布局,使用的是ViewGroup.LayoutParams.WRAP_CONTENT所以直接用獲取window的寬高, 所以用mContent來獲取. 而這需要在DialogInterface.OnShowListener里面才能獲取到(onResume()的時(shí)候還未顯示),所以有如下代碼
getDialog().setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
Window window = getDialog().getWindow();
if (window != null) {
mContent = window.findViewById(android.R.id.content);
mBeginDialogWidth = mContent.getWidth();
mBeginDialogHeight = mContent.getHeight() + dp2px(24);
/*
* 由于showListener的調(diào)用時(shí)間比onResume還晚,所以需要在顯示的時(shí)候,手動(dòng)調(diào)用一次旋轉(zhuǎn).
*/
setRotation(mRotation);
}
}
});
獲取到window的寬高之后,可以用mBeginDialogWidth,mBeginDialogHeight保存起來,在后面重新分配窗口大小的時(shí)候,可以用這個(gè)數(shù)值.+dp2px(24)是因?yàn)?code>mContent的大小不完全等于真實(shí)的Dialog的大小,這中間可能有些Margin之類的,但是大概是24dp左右, 所以這里直接加上這個(gè)值.
在旋轉(zhuǎn)方法public abstract void setRotation(int rotation);的實(shí)現(xiàn)上,根據(jù)當(dāng)前角度分別處理.
當(dāng)rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270的時(shí)候,此時(shí)顯示效果為橫屏,比如我這里定義為
w = (int) (windowSize.getHeight() * 0.70 + 0.5f);
h = mBeginDialogHeight - 20;
即寬為初始方向上window的高的70%,高為初始方向上dialog的高度. 后面的-20是因?yàn)樵谛麓笮∠? 布局填充下有一點(diǎn)點(diǎn)的大小誤差, 所以手動(dòng)調(diào)整(你也可以根據(jù)需要調(diào)整到其他值).此時(shí)可以設(shè)置window的大小
window.setLayout(h + 80, w + 100);
當(dāng)rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180的時(shí)候,顯示效果為豎屏, 此時(shí)Dialog的寬高就是初始時(shí)未旋轉(zhuǎn)的寬高,也就是上文保存的mBeginDialogWidth,mBeginDialogHeight. 所以他的寬高可以表示為
w = mBeginDialogWidth;
h = mBeginDialogHeight - dp2px(24); //減去上面多加的24dp
使用默認(rèn)WRAP_CONTENT來設(shè)置window的大小
window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
確定window的大小了之后, 就可以設(shè)置mContent的大小了
mContent.getLayoutParams().width = w;
mContent.getLayoutParams().height = h;
mContent.setLayoutParams(mContent.getLayoutParams());
最后執(zhí)行旋轉(zhuǎn)mContent:
mContent.animate()
.rotation(90 * (rotation))
.translationX(tranX)
.translationY(tranY)
.setDuration(duration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isFirstCreateDialog = false;
}
});
其中tranX和tranY表示為x軸和y軸的偏移量. 這個(gè)是因?yàn)橐粋€(gè)矩形繞中心點(diǎn)旋轉(zhuǎn)90°或者-90°后, 它的四個(gè)頂點(diǎn)位置不一定重合, 所以需要在兩個(gè)方向是偏移一定的距離才可以重合在左上角, 而這個(gè)偏移量等于(width - height) / 2.
BaseCustomContentDialog
這個(gè)類為使用自定義布局的Dialog的基類, 與上面BaseDefaultContentDialog類似, 只不過這里使用自定義布局的根布局來作為mContent, 可以在onViewCreated()的時(shí)候獲取
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRootView = view;
}
具體的旋轉(zhuǎn)邏輯實(shí)現(xiàn)如下
@Override
public void setRotation(int rotation) {
if (getDialog() == null) {
return;
}
if (getDialog().getWindow() == null) {
return;
}
onWindowDisplayPosition(getDialog().getWindow(), rotation);
Size size = getFilledViewSize(rotation);
setViewSize(size.getWidth(), size.getHeight());
rotateRootView(mRootView, rotation);
}
可以看到分為3步:
1.定義在四個(gè)方向上的window的大小和位置
2.重新設(shè)置mRootView的大小,只是在旋轉(zhuǎn)后能夠充滿整個(gè)window
3.旋轉(zhuǎn)mRootView
基本原理和上面的BaseDefaultContentDialog類似, 我就不寫了, 看了文末的源碼差不多就懂了.
部分源碼
BaseDefaultContentDialog 使用系統(tǒng)布局的Dialog
public class BaseDefaultContentDialog extends BaseDialogFragment {
private View mContent;
private int mBeginDialogWidth;
private int mBeginDialogHeight;
protected boolean isFirstCreateDialog = true; // 表示第一次初始化本DialogFragment
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getDialog().setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
Window window = getDialog().getWindow();
if (window != null) {
mContent = window.findViewById(android.R.id.content);
mBeginDialogWidth = mContent.getWidth();
mBeginDialogHeight = mContent.getHeight() + dp2px(24);
/*
* 由于showListener的調(diào)用時(shí)間比onResume還晚,所以需要在顯示的時(shí)候,手動(dòng)調(diào)用一次旋轉(zhuǎn).
*/
setRotation(mRotation);
}
}
});
}
@Override
public void setRotation(int rotation) {
Size windowSize = WindowUtil.getWindowSize();
if (getDialog() == null) {
return;
}
Window window = getDialog().getWindow();
if (window == null) {
Log.e("TAG", "setRotation: window = null");
return;
}
if (mContent == null) {
return;
}
int w, h;
int tranX, tranY;
if (rotation == 1 || rotation == 3) {//橫屏
w = (int) (windowSize.getHeight() * 0.70 + 0.5f);
h = mBeginDialogHeight - 20;
tranX = (h - w) / 2;
tranY = (w - h) / 2;
window.setLayout(h + 80, w + 100);
} else {
w = mBeginDialogWidth;
h = mBeginDialogHeight - dp2px(24);
tranX = 0;
tranY = 0;
window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
mContent.getLayoutParams().width = w;
mContent.getLayoutParams().height = h;
mContent.setLayoutParams(mContent.getLayoutParams());
int duration = isFirstCreateDialog ? 0 : 200;
mContent.animate()
.rotation(90 * (rotation))
.translationX(tranX)
.translationY(tranY)
.setDuration(duration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isFirstCreateDialog = false;
}
});
}
private int dp2px(int dp) {
return DipPixelUtil.dip2px(getActivity(), dp);
}
}
BaseCustomContentDialog 使用自定義布局的Dialog
public abstract class BaseCustomContentDialog extends BaseDialogFragment {
protected boolean isFirstCreateDialog = true; // 表示第一次初始化本DialogFragment
protected View mRootView;
private int mTransOffset;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRootView = view;
}
@Override
public void onResume() {
super.onResume();
setRotation(mRotation);
}
@Override
public void setRotation(int rotation) {
if (getDialog() == null) {
return;
}
if (getDialog().getWindow() == null) {
return;
}
onWindowDisplayPosition(getDialog().getWindow(), rotation);
Size size = getFilledViewSize(rotation);
setViewSize(size.getWidth(), size.getHeight());
rotateRootView(mRootView, rotation);
}
/**
* 旋轉(zhuǎn)View到正確的位置
*/
private void rotateRootView(View rootView,int rotation) {
int degree = 90 * rotation;
int transX, transY;
if (rotation == 1 || rotation == 3) { //橫屏
transX = -mTransOffset;
transY = mTransOffset;
} else { //豎屏
transX = 0;
transY = 0;
}
int duration = isFirstCreateDialog ? 0 : 200;
rootView.animate()
.rotation(degree)
.translationX(transX)
.translationY(transY)
.setInterpolator(new DecelerateInterpolator())
.setDuration(duration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isFirstCreateDialog = false;
}
});
}
/**
* 獲取旋轉(zhuǎn)后充滿{@link #mRootView}的Size大小
* 分兩種情況:
* 豎屏?xí)r,使用當(dāng)前的window寬高
* 橫屏?xí)r,交換當(dāng)前window的寬高作為字view的旋轉(zhuǎn)后的大小
*/
private Size getFilledViewSize(int rotation) {
WindowManager.LayoutParams attributes = getDialog().getWindow().getAttributes();
int width, height;
if (rotation == 1 || rotation == 3) {
width = attributes.height;
height = attributes.width;
} else {
width = attributes.width;
height = attributes.height;
}
return new Size(width, height);
}
/**
* 重新設(shè)置子View的大小,使布局充滿整個(gè)window的size
*/
private void setViewSize(int width, int height) {
mRootView.getLayoutParams().width = width;
mRootView.getLayoutParams().height = height;
mRootView.setLayoutParams(mRootView.getLayoutParams());
//重新根據(jù)rootView的大小,設(shè)置旋轉(zhuǎn)后需要的偏移量
mTransOffset = width / 2 - height / 2;
}
/**
* 設(shè)置窗口的顯示位置和大小.
* 以下為測試數(shù)據(jù).
* 子類可以通過復(fù)寫此方法,已重新設(shè)置相應(yīng)的位置和大小.
*/
protected void onWindowDisplayPosition(Window window,int rotation) {
// 設(shè)置彈窗顯示位置
window.setGravity(Gravity.CENTER);
// 設(shè)置窗口大小
Size windowSize = WindowUtil.getWindowSize();
int w, h;
if (rotation == 1 || rotation == 3) {
w = windowSize.getHeight() / 3;
h = 4 * windowSize.getWidth() / 5;
} else {
w = 4 * windowSize.getWidth() / 5;
h = windowSize.getHeight() / 3;
}
window.setLayout(w, h);
}
}
其他問題
目前還不知道如果讓軟鍵盤按照指定方向彈出, 望知道的大佬指點(diǎn)一哈..