編輯:關於Android編程
在我們玩手機游戲時能看到,很多游戲的登錄界面兩側往往會有一個小小的懸浮窗,可以提供相應功能菜單項,簡潔實用且不影響游戲體驗。具體效果如下圖所示。這篇博客將帶大家開發一個可以直接用在項目中的懸浮窗實例。



1.懸浮窗可在屏幕范圍內自由拖動;
2.懸浮窗拖動到屏幕中任意位置後放開手指,它會向近的一側移動並停靠;
3.停靠在屏幕兩側時,經過一定時間後能自動切換成半隱藏狀態;
4.點擊半隱藏狀態的懸浮窗,顯示懸浮窗,再次點擊則顯示懸浮窗側拉菜單。
1.WindowManager懸浮窗控制類
2.點擊觸摸事件處理
3.簡單的多線程編程
4.PopupWindow
(1)實現懸浮窗布局
我通過自定義一個擴展自LinearLayout的類來實現懸浮窗
public class MainFloatWindow extends LinearLayout
懸浮窗的布局很簡單,直接就是兩張圖片,對應兩種狀態(半隱藏和顯示),所以我直接用Java代碼實現(資源在我的源碼中有)
private void initWindowView(){
ivDefaultWindow = new ImageView(mCtx);
this.ivDefaultWindow.setImageResource(ResourceUtil.getDrawableId(mCtx, "uac_gameassist_photo_default"));
this.ivDefaultWindow.setBackgroundResource(ResourceUtil.getDrawableId(mCtx, "uac_gameassist_photo_bg"));
ivHideWindow = new ImageView(mCtx);
ivHideWindow.setImageResource(ResourceUtil.getDrawableId(mCtx, "uac_gameassist_photo_brand_left"));
//顯示ivDefaultWindow,隱藏ivHideWindow
setWindowHide(false);
}
然後在MainFloatWindow的構造函數中把這些初始化的類addview到MainFloatWindow,從而實現布局
this.addView(ivDefaultWindow);
this.addView(ivHideWindow);
(2)通過WindowManager讓它顯示出來
懸浮窗權限
初始化WindowManager的參數
WindowManager mWindowManager = (WindowManager) mCtx.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.type = WindowManager.LayoutParams.TYPE_PHONE; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; params.format = PixelFormat.TRANSLUCENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.gravity = Gravity.LEFT |Gravity.TOP; //懸浮窗初始位置 params.y = WindowUtil.getScreenHeight(mCtx)/2;
把之前實現的MainFloatWindow作為懸浮窗視圖,WindowManager控制顯示隱藏。其實就是簡單的addView和removeView方法。
public void showFloatWindow(){
if(isShow){
return;
}
mWindowManager.addView(mainFloatWindow,params);
isShow = true;
}
public void hideFloatWindow(){
if(!isShow){
return;
}
mWindowManager.removeView(mainFloatWindow);
isShow = false;
}
至此,我們就可以在屏幕中看到一個固定不動的懸浮窗了。
(1)更新懸浮窗位置用到了WindowManager的updateViewLayout方法,通過改變傳入的params實例的x、y參數,實現位置改變。
mParams.x = (int) x; mParams.y = (int) y; mWindowManager.updateViewLayout(MainFloatWindow.this, mParams);
(2)重寫onTouchEvent()方法,對監聽的動作進行處理
這裡需要講個注意的地方,MotionEvent.getX()是獲得相對當前觸摸的視圖的x左邊,而MotionEvent.getRawX()是獲得相對屏幕的x坐標。
/**相對於View的x、y坐標 **/
private float xInView, yInView;
/**在屏幕中的x、y坐標 **/
private float xDown, yDown;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
xInView = event.getX();
yInView = event.getY();
xDown = event.getRawX();
yDown = event.getRawY() - WindowUtil.getStatusBarHeight(mCtx);
break;
case MotionEvent.ACTION_MOVE:
//更新懸浮窗位置
Message message = new Message();
message.what = MSG_UPDATE_POS;
message.arg1 = (int) (event.getRawX() - xInView);
message.arg2 = (int) (event.getRawY() - WindowUtil.getStatusBarHeight(mCtx));
mHandler.sendMessage(message);
break;
case MotionEvent.ACTION_UP:
if(xDown == event.getRawX() && yDown == event.getRawY() - WindowUtil.getStatusBarHeight(mCtx)){
//點擊事件
onClick();
}
break;
}
return true;
}
因為重寫的onTouchEvent()返回true,所以會覆蓋了該視圖的點擊監聽事件(具體原因不在此贅述,可以看Android觸摸事件處理機制),我們需要自行判斷動作監聽點擊事件。
/**
* 自動平移到兩側停靠
*/
private void autoMoveToSide() {
new Thread() {
@Override
public void run() {
//保存x、y坐標
int[] location = new int[2];
getLocationOnScreen(location);
isOnLeft = location[0]+getWidth()/2 < WindowUtil.getScreenWidth(mCtx)/2;
mSubFloatWindow.setOnLeft(isOnLeft);
int moveParam = isOnLeft ? -10 : 10; //每次移動10個像素
while (true) {
location[0] = location[0] + moveParam;
int newX = location[0];
int newY = location[1];
if (isOnLeft && newX<=0) { //已移至最左側
newX = 0;
Message message = new Message();
message.what = MSG_UPDATE_POS;
message.arg1 = newX;
message.arg2 = newY;
mHandler.sendMessage(message);
break;
}else if(!isOnLeft && newX>=WindowUtil.getScreenWidth(mCtx)){ //已移至最右側
newX = WindowUtil.getScreenWidth(mCtx);
Message message = new Message();
message.what = MSG_UPDATE_POS;
message.arg1 = newX;
message.arg2 = newY;
mHandler.sendMessage(message);
break;
} else {
Message message = new Message();
message.what = MSG_UPDATE_POS;
message.arg1 = newX;
message.arg2 = newY;
mHandler.sendMessage(message);
}
try {
Thread.sleep(100/ SPEED);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
開啟一條子線程計算自動平移時新狀態的位置,並通過Thread.sleep()模擬每隔一段時間移動一段距離的。
當懸浮窗停靠在屏幕兩側時,隔一段時間沒有進行操作時,需要自動切換成半隱藏狀態。半隱藏狀態不能被拖動,點擊此狀態下的懸浮窗,可以顯示出完整的懸浮窗。
實現此功能需要為懸浮窗添加兩個狀態判斷標識,第一個是懸浮窗能否隱藏標識,當懸浮窗菜單打開時不能隱藏,當懸浮窗被拖動時不能隱藏;第二個是懸浮窗是否處於隱藏狀態,當處於隱藏狀態時,保證懸浮窗不能被拖動。
/**
* 等待一定時間後隱藏懸浮窗
*/
private void waitToHideWindow(){
if(!canHide){
return;
}
new Thread(){
@Override
public void run() {
try {
Thread.sleep(WAIT_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(canHide) {
mHandler.sendEmptyMessage(MSG_WINDOW_HIDE);
}
}
}.start();
}
此處在子線程啟動前判斷能否隱藏,然後在線程運行時再判斷一次能否隱藏,做雙重鎖,是為了防止線程睡眠前懸浮窗可以隱藏,但等線程睡眠結束後處於不能隱藏的狀態,卻把懸浮窗切換成隱藏狀態了。舉一個實際情況,當線程停靠在兩側,應用調用waitToHideWindow()方法等待一段時候後隱藏懸浮窗,但等待過程中用戶拖動了懸浮窗,等待結束後還處於被拖動狀態,這時按需求是不應該隱藏懸浮窗的,故需要對狀態加以判斷。
子菜單我是采用PopupWindow來做的,原因是想到了它可以在顯示在父視圖的特定位置,懸浮窗和菜單分開處理,便於操作。
public SubFloatWindow(Context context){
mCtx = context;
setContentView(getWindowView());
setWidth(WindowUtil.dip2px(mCtx, 220));
setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
setFocusable(true);
setOutsideTouchable(true);
// 實例化一個ColorDrawable顏色為半透明
ColorDrawable dw = new ColorDrawable(0000000000);
// 點back鍵和其他地方使其消失,設置了這個才能觸發OnDismisslistener ,設置其他控件變化等操作
this.setBackgroundDrawable(dw);
}
PopupWindow的初始化很簡單,設置幾個基本屬性即好。
if (isOnLeft) {
mSubFloatWindow.showAtLocation(this, Gravity.CENTER | Gravity.START, getWidth(), 0);
ivDefaultWindow.setBackgroundResource(ResourceUtil.getDrawableId(mCtx, "uac_gameassist_photo_right_bg"));
} else {
mSubFloatWindow.showAtLocation(this, Gravity.CENTER | Gravity.END, getWidth(), 0);
ivDefaultWindow.setBackgroundResource(ResourceUtil.getDrawableId(mCtx, "uac_gameassist_photo_left_bg"));
}
子菜單的顯示,需要用到該方法
public void showAtLocation(android.view.View parent,
int gravity,
int x,
int y)
需要注意的是在左側時,gravity參數要為
Gravity.CENTER | Gravity.START在右側時,則是
Gravity.CENTER | Gravity.END對齊方式不同,子菜單的位置也會有所改變。
至此,懸浮窗的關鍵實現都基本寫完,如有寫得不好的地方,歡迎指教~
Android中常見的內存洩漏匯總
集合類洩漏集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變量 (比如類中的靜態屬性,全局性的 map 等即有靜態引用或 f
UITextView的使用
UITextView與UITextField功能類似,也是字符輸入的視圖控件。區別在於:1、UITextView是多行字符輸入,可通過回車鍵進行換行輸入2、也可以設置固定
Android中View自定義組合控件的基本編寫方法
有很多情況下,我們只要運用好Android給我提供好的控件,經過布局巧妙的結合在一起,就是一個新的控件,我稱之為“自定義組合控件”。那麼,這種自定義組合控件在什麼情況下用
Android筆記之:onConfigurationChanged詳解
從事Android開發,免不了會在應用裡嵌入一些廣告SDK,在嵌入了眾多SDK後,發現幾乎每個要求在AndroidManifest.xml申明Activity的廣告SDK