前言:
最近遇到一个开发需求,机器人在使用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.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