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.widget.SeekBar;

import java.util.Random;

public class MainActivity extends Activity implements
        View.OnClickListener,
        SeekBar.OnSeekBarChangeListener {

    private static final String TAG = "MainActivity";

    /**
     * 用于标记是否悬浮框隐藏
     */
    private static boolean isSuspensionHidden = false;

    private Button mBtnFinish;

    private MarqueeTextView mTvMarquee;

    private Button mScrollStart;

    private Button mScrollColorSet;

    private SeekBar mScrollSpeedSet;

    private SeekBar mScrollSizeSet;

    private WindowManager mWindowManager;

    private WindowManager.LayoutParams params;

    private Button mLightClose;

    private LinearLayout mViewRoot;


    private boolean isCloseLight = false;

    /**
     * 用于悬浮框展示的文本
     */
    private String[] textArray = {
            "四月南风大麦黄,枣花未落桐叶长。",
            "寒日萧萧上锁窗,梧桐应恨夜来霜。酒阑更喜团茶苦,梦断偏宜瑞脑香。秋已尽,日犹长,仲宣怀远更凄凉。不如随分尊前醉,莫负东篱菊蕊黄",
            "青山朝别暮还见,嘶马出门思旧乡。",
            "拨灯书尽红笺也,依旧无聊。玉漏迢迢,梦里寒花隔玉箫。几竿修竹三更雨,叶叶萧萧。分付秋潮,莫误双鱼到谢桥。",
            "陈侯立身何坦荡,虬须虎眉仍大颡。"
    };
    private Button mShowTextRandom;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {

        mBtnFinish = findViewById(R.id.finish_btn);
        mTvMarquee = findViewById(R.id.marquee_tv);

        mScrollStart = findViewById(R.id.start_scroll);
        mScrollColorSet = findViewById(R.id.set_scroll_color);
        mScrollSizeSet = findViewById(R.id.set_scroll_size);
        mScrollSpeedSet = findViewById(R.id.set_scroll_speed);
        mLightClose = (Button) findViewById(R.id.close_suspension);
        mViewRoot = (LinearLayout) findViewById(R.id.root_view);
        mShowTextRandom = (Button) findViewById(R.id.random_show_text);


        mScrollStart.setOnClickListener(this);
        mScrollColorSet.setOnClickListener(this);
        mBtnFinish.setOnClickListener(this);
        mScrollSpeedSet.setOnSeekBarChangeListener(this);
        mScrollSizeSet.setOnSeekBarChangeListener(this);
        mLightClose.setOnClickListener(this);
        mShowTextRandom.setOnClickListener(this);

    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.finish_btn:
                finish();
                break;

            case R.id.random_show_text://随机展示悬浮文本
                int i = new Random().nextInt(textArray.length);
                mTvMarquee.setText(textArray[i]);
//                showSuspension(textArray[i]);
                break;

            case R.id.start_scroll://设置滚动
                mTvMarquee.setCanScroll(true);
                break;

            case R.id.set_scroll_color://设置悬浮框字体颜色
                mTvMarquee.setTextColor(Color.parseColor("#ff0000"));
                break;

            case R.id.close_suspension://关闭悬浮框
                hideSuspension();
                break;

            default:
                break;
        }
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

        switch (seekBar.getId()) {
            case R.id.set_scroll_size:
                Log.i(TAG, "onProgressChanged: --size------progress::" + progress);
                mTvMarquee.setTextSize((float) progress);
                break;

            case R.id.set_scroll_speed:
                Log.i(TAG, "onProgressChanged: -----speed---progress::" + progress);
                mTvMarquee.setTextSpeed((float) progress);
                break;

            default:
                break;
        }

    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {

    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {

    }


    /**
     * 隐藏悬浮框
     */
    protected void hideSuspension() {

        if (!isSuspensionHidden) {
            if (mWindowManager != null) {
                isSuspensionHidden = true;
                mWindowManager.removeView(mTvMarquee);
            }
        }

    }

    /**
     * 显示悬浮框
     */
    private void showSuspension(String str) {

        //隐藏视图,防止重复添加view
        if (!isSuspensionHidden) {
            hideSuspension();
        }

        isSuspensionHidden = false;


        Rect frame = new Rect();

        //获取整个视图部分,注意,如果你要设置标题样式,这个必须出现在标题样式之后,否则会出错
        getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);

        if (mWindowManager == null) {

            //山下文对象使用getApplicationContext()即使退出程序,依然可以显示
            mWindowManager = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE);
        }

        /**
         *TYPE_SYSTEM_ALERT:系统提示。它总是出现在应用程序窗口之上
         *TYPE_SYSTEM_OVERLAY:系统顶层窗口。显示在其他一切内容之上。此窗口不能获得输入焦点,否则影响锁屏
         *
         *FLAG_NOT_TOUCH_MODAL:表明这个window不会和软键盘进行交互
         *FLAG_NOT_FOCUSABLE:设置之后window永远不会获取焦点,所以用户不能给此window发送点击事件
         *
         */
        if (params == null) {
            params = new WindowManager.LayoutParams();
            params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
                    | WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

            params.width = 1920;//机器人屏幕宽度
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.alpha = 80;
            params.format = PixelFormat.TRANSPARENT;

            //以屏幕左上角为原点设置初始值
            params.gravity = Gravity.LEFT | Gravity.TOP;

            // 设置x、y初始值
            params.x = 0;
            params.y = 0;
        }

        if (mTvMarquee == null) {
            mTvMarquee = new MarqueeTextView(getApplicationContext(), mWindowManager, params);
        }

        mWindowManager.addView(mTvMarquee, params);
        mTvMarquee.setText(str);
    }

}

  好了,主程序代码也很简单,我就不在赘述了。
需要注意:

/**
     * 用于标记是否悬浮框隐藏
     */
    private static boolean isSuspensionHidden = false;

  这个布尔值是用来用于标记是否悬浮框隐藏。之所以这样做是因为:我们用到的MarqueeTextView实例和WindowManager的实例只有一个,因此必须保证在使用

 mWindowManager.addView(mTvMarquee, params);

addView时候,必须存在同一个MarqueeTextView的实例,不然会报“view视图已经被添加过,不能重复添加"的错误提示。

同样的,当我们为隐藏悬浮框使用

 mWindowManager.removeView(mTvMarquee);

removeView时,必须有同一个MarqueeTextView的实例,不然会报
没有已经依附的view视图”的错误提示。

  好了,最后附上示例链接,如果小伙伴在使用的过程中有任何疑问,请留言,我们共同探索!!

示例链接:

https://download.csdn.net/download/zhangqunshuai/10706689




参考文章:
https://blog.csdn.net/qq_27302873/article/details/50756215

https://blog.csdn.net/u010142437/article/details/17509587

猜你喜欢

转载自blog.csdn.net/zhangqunshuai/article/details/82971087