Android開發筆記(一百一十八)自定義懸浮窗
WindowManager
在前面《Android開發筆記(六十六)自定義對話方塊》中,我們提到每個頁面都是一個Window視窗,許多的Window物件需要一個管家來打理,這個管家我們稱之為WindowManager視窗管理。在手機螢幕上新增或刪除頁面視窗,都可以歸結為WindowManager的操作,下面是該管理類的常用方法說明:getDefaultDisplay : 獲取預設的螢幕資訊。通常用該方法獲取螢幕解析度,詳情參見《Android開發筆記(三)螢幕解析度》。
addView : 往視窗新增檢視。第二個引數為WindowManager.LayoutParams物件。
updateViewLayout : 更新指定檢視的佈局引數。第二個引數為WindowManager.LayoutParams物件。
removeView : 往視窗移除指定檢視。
下面是視窗布局引數WindowManager.LayoutParams的常用屬性說明:
format : 視窗的畫素點格式。取值見PixelFormat類中的常量定義,一般取值PixelFormat.RGBA_8888。
type : 視窗的顯示型別,常用的型別說明如下:
--TYPE_SYSTEM_ALERT : 系統警告提示。
--TYPE_SYSTEM_ERROR : 系統錯誤提示。
--TYPE_SYSTEM_OVERLAY : 頁面頂層提示。
--TYPE_SYSTEM_DIALOG : 系統對話方塊。
--TYPE_STATUS_BAR : 狀態列
--TYPE_TOAST : 短暫通知Toast
flags : 視窗的行為準則,常用的標誌位如下說明(對於懸浮窗來說,一般只需設定FLAG_NOT_FOCUSABLE):
--FLAG_NOT_FOCUSABLE : 不能搶佔焦點,即不接受任何按鍵或按鈕事件。
--FLAG_NOT_TOUCHABLE : 不接受觸控式螢幕事件。懸浮窗一般不設定該標誌,因為一旦設定該標誌,將無法拖動懸浮窗。
--FLAG_NOT_TOUCH_MODAL : 當視窗允許獲得焦點時(即沒有設定FLAG_NOT_FOCUSALBE標誌),仍然將視窗之外的按鍵事件傳送給後面的視窗處理。否則它將獨佔所有的按鍵事件,而不管它們是不是發生在視窗範圍之內。
-- :
--FLAG_LAYOUT_IN_SCREEN : 允許視窗占滿整個螢幕。
--FLAG_LAYOUT_NO_LIMITS : 允許視窗擴充套件到螢幕之外。
--FLAG_WATCH_OUTSIDE_TOUCH : 如果設定了FLAG_NOT_TOUCH_MODAL標誌,則當按鍵動作發生在視窗之外時,將接收到一個MotionEvent.ACTION_OUTSIDE事件。
alpha : 視窗的透明度,取值為0-1。
gravity : 取值同View的setGravity方法。
x : 視窗左上角的X座標。
y : 視窗左上角的Y座標。
width : 視窗的寬度。
height : 視窗的高度。
靜態懸浮窗
懸浮窗有點類似對話方塊,它們都是獨立於Activity頁面的視窗,但是懸浮窗又有一些與眾不同的特性,例如:1、懸浮窗是可以拖動的,對話方塊則不能;
2、懸浮窗不妨礙使用者觸控窗外的區域,對話方塊則不讓使用者操作框外的控制元件;
3、懸浮窗獨立於Activity頁面,即當頁面退出後,懸浮窗仍停留在螢幕上;而對話方塊與Activity頁面是共存關係,一旦頁面退出則對話方塊也消失了;
基於懸浮窗的以上特性,我們要實現視窗的懸浮效果,就不僅僅是呼叫WindowManager的addView方法那麼簡單了,而是需要做一系列的自定義處理,具體步驟如下:
1、在AndroidManifest.xml中宣告系統視窗許可權,即增加下面這句:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
2、在自定義的懸浮窗控制元件中,要設定觸控監聽器,並根據使用者的手勢滑動來相應調整視窗位置,以實現懸浮窗的拖動功能;3、合理設定懸浮窗的視窗引數,主要是把視窗引數的顯示型別設定為TYPE_SYSTEM_ALERT或者TYPE_SYSTEM_ERROR,另外要設定標誌位FLAG_NOT_FOCUSABLE;
4、在構造懸浮窗例項時,要傳入Application的上下文Context,這是為了保證即使退出Activity,也不會關閉懸浮窗。因為Application物件在app執行過程中是始終存在著的,而Activity物件只在開啟頁面時有效,一旦退出頁面則Activity的上下文就立刻回收(這會導致依賴於該上下文的懸浮窗也一塊被回收了)。
下面是一個靜態懸浮窗的效果截圖:
下面是自定義懸浮窗的示例程式碼:
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
public class FloatView extends View {
private final static String TAG = "FloatView";
private Context mContext;
private WindowManager wm;
private static WindowManager.LayoutParams wmParams;
public View mContentView;
private float mRelativeX;
private float mRelativeY;
private float mScreenX;
private float mScreenY;
private boolean bShow = false;
public FloatView(Context context) {
super(context);
wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wmParams == null) {
wmParams = new WindowManager.LayoutParams();
}
mContext = context;
}
public void setLayout(int layout_id) {
mContentView = LayoutInflater.from(mContext).inflate(layout_id, null);
mContentView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
mScreenX = event.getRawX();
mScreenY = event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mRelativeX = event.getX();
mRelativeY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
updateViewPosition();
break;
case MotionEvent.ACTION_UP:
updateViewPosition();
mRelativeX = mRelativeY = 0;
break;
}
return true;
}
});
}
private void updateViewPosition() {
wmParams.x = (int) (mScreenX - mRelativeX);
wmParams.y = (int) (mScreenY - mRelativeY);
wm.updateViewLayout(mContentView, wmParams);
}
public void show() {
if (mContentView != null) {
wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
wmParams.format = PixelFormat.RGBA_8888;
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmParams.alpha = 1.0f;
wmParams.gravity = Gravity.LEFT | Gravity.TOP;
wmParams.x = 0;
wmParams.y = 0;
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 顯示自定義懸浮視窗
wm.addView(mContentView, wmParams);
bShow = true;
}
}
public void close() {
if (mContentView != null) {
wm.removeView(mContentView);
bShow = false;
}
}
public boolean isShow() {
return bShow;
}
}
下面是開啟/關閉懸浮窗的頁面程式碼:
import com.example.exmfloat.widget.FloatView;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class StaticActivity extends Activity implements OnClickListener {
private FloatView mFloatView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static);
Button btn_static_open = (Button) findViewById(R.id.btn_static_open);
Button btn_static_close = (Button) findViewById(R.id.btn_static_close);
btn_static_open.setOnClickListener(this);
btn_static_close.setOnClickListener(this);
mFloatView = new FloatView(MainApplication.getInstance());
mFloatView.setLayout(R.layout.float_static);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_static_open) {
if (mFloatView!=null && mFloatView.isShow()==false) {
mFloatView.show();
}
} else if (v.getId() == R.id.btn_static_close) {
if (mFloatView!=null && mFloatView.isShow()==true) {
mFloatView.close();
}
}
}
}
下面是自定義Application的程式碼例子:
import android.app.Application;
public class MainApplication extends Application {
private static MainApplication mApp;
public static MainApplication getInstance() {
return mApp;
}
@Override
public void onCreate() {
super.onCreate();
mApp = this;
}
}
動態懸浮窗
在實際開發中,懸浮窗的展示內容是變化的,畢竟一個內容不變的懸浮窗對使用者來說沒什麼用處。具體的應用例子有很多,比如說時鐘、天氣、實時流量、股市指數等等,下面就以實時流量與股市指數兩個例子,來詳細說明動態懸浮窗的實際應用。要想實時重新整理懸浮窗,這得通過服務Service來實現,所以動態懸浮窗要在Service服務中建立和更新,頁面只負責啟動/停止服務。對於手機的實時流量,可以通過TrafficStats類的相關方法計算得到,該類的詳細說明參見《Android開發筆記(七十九)資源與許可權校驗》。
下面是實時流量懸浮窗的效果截圖:
下面是實時流量懸浮窗的頁面程式碼:
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.example.exmfloat.service.TrafficService;
public class TrafficActivity extends Activity implements OnClickListener {
private final static String TAG = "TrafficActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_traffic);
Button btn_traffic_open = (Button) findViewById(R.id.btn_traffic_open);
Button btn_traffic_close = (Button) findViewById(R.id.btn_traffic_close);
btn_traffic_open.setOnClickListener(this);
btn_traffic_close.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_traffic_open) {
Intent intent = new Intent(this, TrafficService.class);
intent.putExtra("type", TrafficService.OPEN);
startService(intent);
} else if (v.getId() == R.id.btn_traffic_close) {
Intent intent = new Intent(this, TrafficService.class);
intent.putExtra("type", TrafficService.CLOSE);
startService(intent);
}
}
}
下面是實時流量懸浮窗的服務程式碼:
import com.example.exmfloat.MainApplication;
import com.example.exmfloat.R;
import com.example.exmfloat.util.FlowUtil;
import com.example.exmfloat.widget.FloatView;
import android.app.Service;
import android.content.Intent;
import android.net.TrafficStats;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.TextView;
public class TrafficService extends Service {
private final static String TAG = "TrafficService";
public static int OPEN = 0;
public static int CLOSE = 1;
private long curRx;
private long curTx;
private FloatView mFloatView;
private TextView tv_traffic;
private final int delayTime = 2000;
private Handler mHandler = new Handler();
private Runnable mRefresh = new Runnable() {
public void run() {
if (mFloatView != null && mFloatView.isShow() == true &&
(TrafficStats.getTotalRxBytes()>curRx || TrafficStats.getTotalTxBytes()>curTx)) {
long a = ((TrafficStats.getTotalRxBytes() - curRx) + (TrafficStats
.getTotalTxBytes() - curTx)) / 2;
String desc = String.format("當前流量: %s/S", FlowUtil.BToShowStringNoDecimals(a));
tv_traffic.setText(desc);
curRx = TrafficStats.getTotalRxBytes();
curTx = TrafficStats.getTotalTxBytes();
}
mHandler.postDelayed(this, delayTime);
}
};
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if (mFloatView == null) {
mFloatView = new FloatView(MainApplication.getInstance());
mFloatView.setLayout(R.layout.float_traffic);
tv_traffic = (TextView) mFloatView.mContentView.findViewById(R.id.tv_traffic);
}
curRx = TrafficStats.getTotalRxBytes();
curTx = TrafficStats.getTotalTxBytes();
mHandler.postDelayed(mRefresh, 0);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
int type = intent.getIntExtra("type", OPEN);
if (type == OPEN) {
if (mFloatView != null && mFloatView.isShow() == false) {
mFloatView.show();
}
} else if (type == CLOSE) {
if (mFloatView != null && mFloatView.isShow() == true) {
mFloatView.close();
}
stopSelf();
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mRefresh);
}
}
對於股市指數的展示,可以通過呼叫財經網站的實時指數查詢介面得到,比如新浪財經與騰訊財經均提供了上證指數與深圳成指的查詢介面。
下面是實時股指懸浮窗的效果截圖:
下面是實時股指懸浮窗的頁面程式碼:
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.example.exmfloat.service.StockService;
public class StockActivity extends Activity implements OnClickListener {
private final static String TAG = "StockActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stock);
Button btn_stock_open = (Button) findViewById(R.id.btn_stock_open);
Button btn_stock_close = (Button) findViewById(R.id.btn_stock_close);
btn_stock_open.setOnClickListener(this);
btn_stock_close.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_stock_open) {
Intent intent = new Intent(this, StockService.class);
intent.putExtra("type", StockService.OPEN);
startService(intent);
} else if (v.getId() == R.id.btn_stock_close) {
Intent intent = new Intent(this, StockService.class);
intent.putExtra("type", StockService.CLOSE);
startService(intent);
}
}
}
下面是實時股指懸浮窗的服務程式碼:
import com.example.exmfloat.MainApplication;
import com.example.exmfloat.R;
import com.example.exmfloat.http.HttpReqData;
import com.example.exmfloat.http.HttpRespData;
import com.example.exmfloat.http.HttpUrlUtil;
import com.example.exmfloat.widget.FloatView;
import android.app.Service;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.widget.TextView;
public class StockService extends Service {
private final static String TAG = "StockService";
public static int OPEN = 0;
public static int CLOSE = 1;
private FloatView mFloatView;
private TextView tv_sh_stock, tv_sz_stock;
private final int delayTime = 5000;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//上證指數,3019.9873,-5.6932,-0.19,1348069,14969598
String desc = (String) msg.obj;
String[] array = desc.split(",");
String stock = array[1];
float distance = Float.parseFloat(array[2]);
String range = array[3];
String text = String.format("%s %s%%", stock, range);
int type = msg.what;
if (type == SHANGHAI) {
tv_sh_stock.setText(text);
if (distance > 0) {
tv_sh_stock.setTextColor(Color.RED);
} else {
tv_sh_stock.setTextColor(Color.GREEN);
}
} else if (type == SHENZHEN) {
tv_sz_stock.setText(text);
if (distance > 0) {
tv_sz_stock.setTextColor(Color.RED);
} else {
tv_sz_stock.setTextColor(Color.GREEN);
}
}
}
};
private Runnable mRefresh = new Runnable() {
@Override
public void run() {
if (mFloatView != null && mFloatView.isShow() == true ) {
new StockThread(SHANGHAI).start();
new StockThread(SHENZHEN).start();
}
mHandler.postDelayed(this, delayTime);
}
};
private static int SHANGHAI = 0;
private static int SHENZHEN = 1;
private class StockThread extends Thread {
private int mType;
public StockThread(int type) {
mType = type;
}
@Override
public void run() {
HttpReqData req_data = new HttpReqData();
if (mType == SHANGHAI) {
req_data.url = "http://hq.sinajs.cn/list=s_sh000001";
} else if (mType == SHENZHEN) {
req_data.url = "http://hq.sinajs.cn/list=s_sz399001";
}
HttpRespData resp_data = HttpUrlUtil.getData(req_data);
//var hq_str_s_sh000001="上證指數,3019.9873,-5.6932,-0.19,1348069,14969598";
String desc = resp_data.content;
Message msg = Message.obtain();
msg.what = mType;
msg.obj = desc.substring(desc.indexOf("\"")+1, desc.lastIndexOf("\""));
mHandler.sendMessage(msg);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if (mFloatView == null) {
mFloatView = new FloatView(MainApplication.getInstance());
mFloatView.setLayout(R.layout.float_stock);
tv_sh_stock = (TextView) mFloatView.mContentView.findViewById(R.id.tv_sh_stock);
tv_sz_stock = (TextView) mFloatView.mContentView.findViewById(R.id.tv_sz_stock);
}
mHandler.postDelayed(mRefresh, 0);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
int type = intent.getIntExtra("type", OPEN);
if (type == OPEN) {
if (mFloatView != null && mFloatView.isShow() == false) {
mFloatView.show();
}
} else if (type == CLOSE) {
if (mFloatView != null && mFloatView.isShow() == true) {
mFloatView.close();
}
stopSelf();
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mRefresh);
}
}
點此檢視Android開發筆記的完整目錄
相關文章
- Android 懸浮窗Android
- 懸浮窗的一種實現 | Android懸浮窗Window應用Android
- Android懸浮窗的學習Android
- Android 懸浮窗 System Alert WindowAndroid
- 懸浮窗開發設計實踐
- Android仿微信文章懸浮窗效果Android
- Android 攝像頭預覽懸浮窗Android
- JavaFx Tooltip懸浮提示使用及自定義Java
- 【轉載】使用WindowManage實現Android懸浮窗Android
- Android 輔助許可權與懸浮窗Android
- iOS學習筆記--PresentedVC自定義彈窗iOS筆記
- Android中的懸浮框Android
- 下沉式通知的一種實現 | Android懸浮窗Window應用Android
- QPM 之懸浮窗設定資訊
- QPM 之懸浮窗助力效能優化優化
- Android開發 - 使用自定義介面在新視窗中傳回資料Android
- Android懸浮框的實現Android
- 使用自定義 View 繪製一個懸浮式可拖拽按鈕View
- 記一次懸浮窗的上線以及坑點總結
- Android:會呼吸的懸浮氣泡Android
- Logseq001筆記類--影片懸浮外掛--HeliumGse筆記
- Android開發筆記Android筆記
- 直播app開發,推出語音聊天室時保持懸浮窗存在狀態APP
- 直播平臺製作,Android 懸浮窗延時5秒返回APP問題AndroidAPP
- 學習筆記(二十八):ArkUi-自定義彈窗 (CustomDialog)筆記UI
- HarmonyOS NEXT開發之ArkTS自定義元件學習筆記元件筆記
- 小米 TYPE_TOAST 懸浮窗無效的原因AST
- react-native 仿原生自定義彈窗|iOS/Android 彈窗效果ReactiOSAndroid
- Android通過WindowManager實現懸浮框Android
- Android懸浮框的適配問題Android
- 百度地圖新增懸浮窗搜尋功能地圖
- 直播原始碼,懸浮窗滾動漸變色效果原始碼
- Android懸浮窗怎麼簡單實現?這樣用 kotlin編寫輕鬆搞定!AndroidKotlin
- Android 攝像頭預覽懸浮窗,可拖動,可顯示在其他app上方AndroidAPP
- 直播軟體開發,工具類的自定義彈窗效果
- [轉]Android輕鬆實現RecyclerView懸浮條AndroidView
- avalonia自定義彈窗
- Android 開發學習筆記Android筆記
- android短視訊開發,自定義下拉選單Android