1. 程式人生 > >Android實現可拖拽的懸浮框

Android實現可拖拽的懸浮框

前言:

  最近遇到一個開發需求,機器人在使用ASR(語音識別)時,需要將使用者說的話,在機器人胸前的互動螢幕上展示出來,也就是展示出相應的字幕。關鍵有一個要求就是可將字幕進行拖拽。。。(怎麼樣,這個需求夠變態吧,雖從正常互動的角度認為這樣完全沒必要,並簡單交涉了下,結果很無奈,你懂得。。。),既然如此,那就幹吧。

  補充一點,我要實現的效果和音樂播放器的桌面歌詞效果不太一樣啊,異同如下:
共同點:都可進行拖拽
不同點:桌面歌詞的效果是歌詞固定(不會左右滾動),而是通過歌詞後面的背景色的走勢來顯示歌詞的進度;而我要實現的效果則是字幕可以上下左右在螢幕上進行拖動。

  關於桌面歌詞的實現效果,網上有一大堆的應用示例,大家可以下載下來,自己研究下,注意我說的是研究---拿過來直接用,可不叫研究啊。(正所謂,不僅要知其然還要知其所以然!好了,不裝逼了)

  友情提示:我執行的效果是在機器人的顯示螢幕的(screenWidth>screenHeight),如果你想在手機上看看效果,儘量在清單檔案中設定螢幕橫屏,不然可能效果比較醜。

概述:

  本著“撒網必須逮到魚”原則。下面將分為兩部分,來帶領大家共同探索下有關自定義TextViewWindowManager(視窗管理器)的相關知識。

第一部分:(不可拖拽的跑馬燈效果)

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