Android實現可拖拽的懸浮框
前言:
最近遇到一個開發需求,機器人在使用ASR(語音識別)時,需要將使用者說的話,在機器人胸前的互動螢幕上展示出來,也就是展示出相應的字幕。關鍵有一個要求就是可將字幕進行拖拽。。。(怎麼樣,這個需求夠變態吧,雖從正常互動的角度認為這樣完全沒必要,並簡單交涉了下,結果很無奈,你懂得。。。),既然如此,那就幹吧。
補充一點,我要實現的效果和音樂播放器的桌面歌詞效果不太一樣啊,異同如下:
共同點:都可進行拖拽
不同點:桌面歌詞的效果是歌詞固定(不會左右滾動),而是通過歌詞後面的背景色的走勢來顯示歌詞的進度;而我要實現的效果則是字幕可以上下左右在螢幕上進行拖動。
關於桌面歌詞的實現效果,網上有一大堆的應用示例,大家可以下載下來,自己研究下,注意我說的是研究---拿過來直接用,可不叫研究啊。(正所謂,不僅要知其然還要知其所以然!好了,不裝逼了)
友情提示:我執行的效果是在機器人的顯示螢幕的(screenWidth>screenHeight),如果你想在手機上看看效果,儘量在清單檔案中設定螢幕橫屏,不然可能效果比較醜。
概述:
本著“撒網必須逮到魚”原則。下面將分為兩部分,來帶領大家共同探索下有關自定義TextView和WindowManager(視窗管理器)的相關知識。
第一部分:(不可拖拽的跑馬燈效果)
1. 通過在xml佈局檔案中繫結自定義TextView控制元件的方式來實現不可拖拽的的跑馬燈效果
2. 可以控制跑馬燈的的關閉與開啟
3. 可以更改跑馬燈文字的字型大小
4. 可以更改跑馬燈文字的字型顏色
5.
效果演示:
第二部分:(可拖拽的懸浮框效果)
1.通過在java程式碼中直接新建自定義TextView+WindowManager的方式實現可拖拽的懸浮框效果
2.可以控制跑馬燈的的關閉與開啟
3.可以更改跑馬燈文字的字型大小
4.可以更改跑馬燈文字的字型顏色
5.可以更改跑馬燈文字的的滾速度
6.可進行拖拽
7.隱藏懸浮框(第一部分中的跑馬燈的隱藏很簡單,可直接通過控制元件的顯示與隱藏進行控制,和此部分的懸浮框的隱藏有區別)
效果演示:
使用步驟:
看前須知:
1.下面貼出的將會是完整的程式碼塊,且我添加了非常完整的註釋,尤其是對於一些略微難理解的地方,註釋得更為詳細,保證你能看懂。
2.上面說到的兩種效果,都在同一套程式碼中進行展示,只不過為了方便不影響大家閱讀,把可拖拽懸浮框的實現程式碼部分註釋掉了。大家在使用的時候,直接將自定義TextView以及主程式中相應的註釋放開,將主程式中通過findByIdView()方式的繫結的控制元件給註釋掉即可。所以看到其中的被註釋掉的程式碼,千萬別罵我程式碼寫的爛啊。(雖然確實不咋地,哈哈)
在清單檔案中註冊許可權:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
1.自定義TextView:
package com.avatarmind.testdemo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.WindowManager;
public class MarqueeTextView extends android.support.v7.widget.AppCompatTextView {
private static final String TAG = "MarqueeTextView";
/**
* 機器人螢幕寬度(固定的,當前你也可以打印出你所使用裝置的螢幕寬度)
*/
private static final int SCREEN_WIDTH = 1920;
/**
* 字幕預設的大小
*/
private final float DEF_TEXT_SIZE = 20.0F;
/**
* 字幕滾動的速度
*/
private float mSpeed = 10.0F;
/**
* 用於標記是否可以滾動(預設不可以滾動)
*/
private boolean isCanScroll = false;
private Context mContext;
private Paint mPaint;
/**
* 用於展示的視窗文字
*/
private String mText;
/**
* 用於標記待設定的字型大小
*/
private float mTextSize;
/**
* 字幕文字的顏色
*/
private int mTextColor = Color.parseColor("#0000ff");
/**
* 用於繪製text文字的x座標軸起始座標
*/
private float mCoordinateX;
/**
* 用於繪製text文字的y座標軸起始座標
*/
private float mCoordinateY;
/**
* 用於待顯示的文字的寬度
*/
private float mTextWidth;
/**
* 用於待顯示的文字的高度
*/
private int mViewWidth;
private WindowManager wm;
public WindowManager.LayoutParams params;
private float startX;
private float startY;
private float float_x;
private float float_y;
/**
* 用於標記是否是第一次建立MarqueeTextView的例項
*/
private boolean isFirst = true;
public MarqueeTextView(Context context) {
super(context);
init(context);
}
public MarqueeTextView(Context context, WindowManager wm, WindowManager.LayoutParams params) {
super(context);
this.wm = wm;
this.params = params;
//用於設定懸浮視窗的背景色,如果不想要直接註釋掉就行
this.setBackgroundColor(Color.argb(100, 140, 160, 150));
init(context);
}
public MarqueeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化相關引數
*
* @param context
*/
private void init(Context context) {
this.mContext = context;
if (TextUtils.isEmpty(mText)) {
mText = "";
}
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(DEF_TEXT_SIZE);
}
/**
* 設定懸浮視窗的文字
*
* @param text
*/
public void setText(String text) {
mText = text;
if (TextUtils.isEmpty(mText)) {
mText = "";
}
requestLayout();
invalidate();
// setWindowWidthAccorindTextWidth();
}
/**
* 根據文字長度來設定懸浮框的寬度
* 當待顯示的文字寬度>螢幕寬度,則設定滾動;
* 當待顯示的文字寬度<螢幕寬度(不滿一行),則設定不允許滾動;
*/
public void setWindowWidthAccorindTextWidth() {
//根據文字長度來確定懸浮框的寬度
int textWidth = (int) mPaint.measureText(mText);// 得到總體長度
if (textWidth >= SCREEN_WIDTH) {
//可滾動
isCanScroll = true;
//懸浮框寬度為等於螢幕寬度
params.width = SCREEN_WIDTH;
} else {
//不可滾動
isCanScroll = false;
//懸浮框寬度為等於實際的文字寬度
params.width = textWidth;
//初始化待顯示文字的x座標,避免出現設定不同text時,出現的文字沒有從頭開始繪製的情況
mCoordinateX = getPaddingLeft();
}
wm.updateViewLayout(this, params);
}
/**
* 設定字型的大小,如果size<0,則使用default size
*
* @param textSize
*/
public void setTextSize(float textSize) {
this.mTextSize = textSize;
mPaint.setTextSize(mTextSize <= 0 ? DEF_TEXT_SIZE : mTextSize);
requestLayout();
invalidate();
}
public void setTextColor(int textColor) {
this.mTextColor = textColor;
mPaint.setColor(mTextColor);
invalidate();
}
/**
* 設定文字滾動速度,如果值<0,設定為預設值為0
*
* @param speed 如果這個值是0,那麼停止滾動
*/
public void setTextSpeed(float speed) {
this.mSpeed = speed < 0 ? 0 : speed;
//作用:請求View樹進行重繪
invalidate();
}
/**
* 獲取文字的滾動速度
*
* @return
*/
public float getTextSpeed() {
return mSpeed;
}
/**
* 設定懸浮框文字是否可以滾動
*
* @param isScroll true,可滾動;false,不可滾動
*/
public void setCanScroll(boolean isScroll) {
this.isCanScroll = isScroll;
invalidate();
}
/**
* 設定懸浮框文字是否可以滾動
*
* @return
*/
public boolean isCanScroll() {
return isCanScroll;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "onMeasure: -------------00000");
//獲取文字的實際寬度
mTextWidth = mPaint.measureText(mText);
//第一次執行之後,就不要再執行mCoordinateX = getPaddingLeft();
//不然在拖動的過程中會文字會重複從頭滾動,而不是繼續滾動
// if (isFirst) {
// isFirst = false;
mCoordinateX = getPaddingLeft();
// }
mCoordinateY = getPaddingTop() + Math.abs(mPaint.ascent());
mViewWidth = measureWidth(widthMeasureSpec);
int mViewHeight = measureHeight(heightMeasureSpec);
setMeasuredDimension(mViewWidth, mViewHeight);
}
/**
* 測量用於繪製 text的寬度
*
* @param measureSpec
* @return
*/
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = (int) mPaint.measureText(mText) + getPaddingLeft()
+ getPaddingRight();
//給定實際測量寬度值和實際測量值中最小的一個
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
/**
* 用於繪製用於測量待繪製的text的高度
*
* @param measureSpec
* @return
*/
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = (int) mPaint.getTextSize() + getPaddingTop()
+ getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mText, mCoordinateX, mCoordinateY, mPaint);//mCoordinateY
//新增判斷:當文字長度不滿一行時候,不允許滾動
if (isCanScroll) {
mCoordinateX -= mSpeed;
/**
* 說明:
* mCoordinateX < 0,當文字左邊向左劃出螢幕時候,就會觸發繪製工作從螢幕右側向左邊滑動
* 所以新增輔助限制條件:
* Math.abs(mCoordinateX) > mTextWidth:當文字的最右側向左滑出螢幕時候,滿足條件
*/
if (Math.abs(mCoordinateX) > mTextWidth && mCoordinateX < 0) {
mCoordinateX = mViewWidth;
}
invalidate();
}
}
//-----------------------------------用於新增懸浮框的拖動邏輯----------------------------------------------------
// @Override
// public boolean onTouchEvent(MotionEvent event) {
// // 觸控點相對於螢幕左上角座標
// float_x = event.getRawX();
// float_y = event.getRawY();
// switch (event.getAction()) {
// case MotionEvent.ACTION_DOWN:
// startX = event.getX();
// startY = event.getY();
// break;
// case MotionEvent.ACTION_MOVE:
// updatePosition();
// break;
// case MotionEvent.ACTION_UP:
// updatePosition();
// startX = startY = 0;
// break;
// }
// return true;
// }
//
// /**
// * 更新浮動視窗位置引數
// */
// private void updatePosition() {
//
// params.x = (int) (float_x - startX);
// params.y = (int) (float_y - startY);
//
// wm.updateViewLayout(this, params);
// }
//
// @Override
// public boolean isFocused() {
//
// return true;
// }
}
程式碼中註釋掉的內容,都是為了實現可拖拽懸浮框的程式碼,沒有一處是多餘的,使用的時候,直接把註釋放開就行(記住是把所有註釋掉的程式碼放開。)另外,我上面已經說了,略微難理解的地方,我都添加了比較詳細的註釋,如果你還是難以理解,那就把demo跑起來,把自己不理解的程式碼部分註釋掉再看看效果,你就知道了。
2.佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<com.avatarmind.testdemo.MarqueeTextView
android:id="@+id/marquee_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="left|center_vertical" />
<Button
android:id="@+id/random_show_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="開啟懸浮框(隨機展示懸浮文字)" />
<Button
android:id="@+id/start_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="開啟跑馬燈滾動" />
<Button
android:id="@+id/set_scroll_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="設定跑馬燈顏色" />
<Button
android:id="@+id/close_suspension"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="關閉懸浮框" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="設定跑馬燈字型大小:"
android:textSize="15sp" />
<SeekBar
android:id="@+id/set_scroll_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="80"
android:progress="20" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="設定跑馬燈速度::"
android:textSize="15sp" />
<SeekBar
android:id="@+id/set_scroll_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="60"
android:progress="10" />
</LinearLayout>
<Button
android:id="@+id/finish_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="銷燬" />
</LinearLayout>
佈局檔案也很簡單,當實現可拖拽懸浮框效果時,我們就用不到xml佈局檔案中的自定義MarqueeTextView控制元件了。畢竟既然可拖拽,自然就不能定死在佈局檔案中了。
3.主程式:
package com.avatarmind.testdemo;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android