当前位置: 首页> 房产> 建筑 > 工程建设管理条例_推广app收益排行榜_seo排名快速上升_实体店铺引流推广方法

工程建设管理条例_推广app收益排行榜_seo排名快速上升_实体店铺引流推广方法

时间:2025/7/15 5:41:37来源:https://blog.csdn.net/WriteBug001/article/details/146091659 浏览次数:1次
工程建设管理条例_推广app收益排行榜_seo排名快速上升_实体店铺引流推广方法

前言

一个可以横向滚动和纵向滚动的自定义文字跑马灯View,支持水平和垂直滚动、多段文本展示、点击事件回调等功能。

该View 由 ScrollTextView,改版而来,效果如下:
在这里插入图片描述

1. 功能介绍


ScrollTextView 是基于 SurfaceView 的自定义视图,功能包括:

1.1 水平滚动

  • 文本从右向左滚动,支持多段文本循环展示。

  • 每段文本滚动结束后可以设置停留时间。

1.2 垂直滚动

  • 文本从下向上滚动,支持多段文本循环展示。

  • 如果文本长度超过视图宽度,会自动触发水平滚动,直到文本完全显示。

1.3 点击事件

  • 支持点击暂停/恢复滚动。

  • 提供点击事件回调,返回当前滚动文本的下标和内容。

1.4 自定义属性

  • 支持通过 XML 或代码设置滚动速度、文本颜色、字体大小等属性。

2. 关键技术点解析


2.1. 使用 SurfaceView 实现高效绘制

SurfaceView 是一个独立的绘图表面,适合需要频繁更新的 UI 场景(如滚动文本)。与普通 View 不同,SurfaceView 的绘制在非 UI 线程中进行,因此不会阻塞主线程。

public class ScrollTextView extends SurfaceView implements SurfaceHolder.Callback {private SurfaceHolder surfaceHolder;public ScrollTextView(Context context, AttributeSet attrs) {super(context, attrs);surfaceHolder = getHolder();surfaceHolder.addCallback(this);}@Overridepublic void surfaceCreated(SurfaceHolder holder) {// 启动滚动线程new Thread(new ScrollTextThread()).start();}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {// 停止滚动线程stopScroll = true;}
}

2.2. 水平滚动逻辑

水平滚动的核心是通过不断更新文本的 X 坐标,实现从右向左的滚动效果。
在这里插入图片描述
水平滚动即从初始状态右侧View之外滑入,到左侧完全滑出为止。

   if (isHorizontal) { // 水平滚动逻辑if (pauseScroll) {// 暂停sleep(1000);continue;}drawText(viewWidth - textX, textY);// 绘制文本textX += speed;// 文本滚动距离if (textX > viewWidthPlusTextLength) {// 文本从右至左滚动超出屏幕currentTextIndex++;textX = 0;--needScrollTimes;if (currentTextIndex >= textList.size()) {// 文字循环展示if (isScrollForever) {currentTextIndex = 0;} else {stopScroll = true;break;}}measureTextParams(false);}}

2.3. 垂直滚动逻辑

垂直滚动的核心是通过不断更新文本的 Y 坐标,实现从下向上的滚动效果。如果文本长度超过视图宽度,会自动触发水平滚动。

在这里插入图片描述

  1. 阶段1
  • 文本从视图底部之外滚动到基线位置。

  • 如果文本超长,准备触发水平滚动。

  1. 阶段2
  • (若文本过长)文本从右向左水平滚动,直到文本末尾完全显示。

  • 水平滚动完成后,暂停 stayTimes 毫秒。

  1. 阶段3
  • 文本从基线位置继续向上滚动,直到完全离开视图顶部。

  • 切换到下一段文本,重复上述过程。

核心代码:

    private void drawVerticalScroll() {if (currentTextIndex >= textList.size()) return;// 计算基线String text = textList.get(currentTextIndex);float fontHeight = paint.getFontMetrics().bottom - paint.getFontMetrics().top;Paint.FontMetrics fontMetrics = paint.getFontMetrics();float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;float baseLine = (float) viewHeight / 2 + distance;// 是否需要水平滚动boolean needHorizontalScroll = paint.measureText(text) > viewWidth;// 水平滚动偏移float horizontalOffset = 0;// --- 阶段1:垂直滚动到基线位置(精准停止)---float currentVerticalPos = viewHeight + fontHeight; // 初始位置while (currentVerticalPos > baseLine) {if (stopScroll || isSetNewText) return;if (pauseScroll) {try {Thread.sleep(1000);} catch (InterruptedException ignored) {}continue;}// 计算下一步位置,避免越过基线float nextPos = currentVerticalPos - 3;if (nextPos < baseLine) nextPos = baseLine; // 确保不会越过基线drawText(0, nextPos);currentVerticalPos = nextPos;}// --- 阶段1暂停:基线位置暂停 ---if (needHorizontalScroll) {sleep(1000);} else if (!stopScroll && !isSetNewText) {sleep(stayTimes); // 原基线位置暂停}// --- 阶段2:水平滚动(垂直位置固定为基线)---if (needHorizontalScroll) {// 计算最大水平滚动偏移float maxHorizontalOffset = -(paint.measureText(text) - viewWidth);// 水平滚动while (horizontalOffset > maxHorizontalOffset) {if (stopScroll || isSetNewText) return;horizontalOffset -= speed;drawText(horizontalOffset, baseLine);}// --- 阶段2暂停:水平滚动完成后暂停 ---if (!stopScroll && !isSetNewText) {sleep(stayTimes); // 新增水平滚动完成后的暂停}}// --- 阶段3:继续垂直滚动到视图外 ---for (float i = baseLine; i > -fontHeight; i -= 3) {if (stopScroll || isSetNewText) return;if (pauseScroll) {sleep(1000);continue;}drawText(needHorizontalScroll ? horizontalOffset : 0, i);}// 切换到下一个文本currentTextIndex++;if (currentTextIndex >= textList.size()) {if (isScrollForever) currentTextIndex = 0;else stopScroll = true;}}

2.4. 点击事件处理

@Override
public boolean onTouchEvent(MotionEvent event) {if (clickEnable && event.getAction() == MotionEvent.ACTION_DOWN) {pauseScroll = !pauseScroll; // 切换暂停状态if (onTextClickListener != null) {// 回调当前滚动文本的下标和内容onTextClickListener.onTextClick(currentTextIndex, textList.get(currentTextIndex));}}return true;
}

3. 自定义属性


    <declare-styleable name="ScrollTextView"><attr name="clickEnable" format="boolean" /><attr name="isHorizontal" format="boolean" /><attr name="speed" format="integer" /><attr name="stTextColor" format="color" /><attr name="stTextSize" format="dimension" /><attr name="times" format="integer" /><attr name="isScrollForever" format="boolean" /></declare-styleable>

4. 完整代码


public class ScrollTextView extends SurfaceView implements SurfaceHolder.Callback {private final String TAG = "ScrollTextView";private SurfaceHolder surfaceHolder;// SurfaceHolder管理SurfaceView的绘制表面private Paint paint = null;private boolean stopScroll = false;// 停止滚动private boolean pauseScroll = false;// 暂停滚动private boolean clickEnable = false;// 是否启用点击事件public boolean isHorizontal = true;// 是否水平滚动private int speed = 4;// 滚动速度private long stayTimes = 5_000;// 横向滚动每段文字结束后的停留时间private List<String> textList = new ArrayList<>(); // 存储多个文本内容private int currentTextIndex = 0; // 当前滚动的文本索引private float textSize = 20f;// 文本字体大小private int textColor;// 文本颜色private int textBackColor = 0x00000000;// 文本背景颜色private int needScrollTimes = Integer.MAX_VALUE;// 需要滚动的次数private int viewWidth = 0, viewHeight = 0;// 视图的宽度和高度private float textWidth = 0f, textX = 0f, textY = 0f;// 文本的宽度、X坐标、Y坐标private float viewWidthPlusTextLength = 0.0f;// 视图宽度加上文本长度boolean isSetNewText = false;// 是否设置了新的文本boolean isScrollForever = true;// 是否无限滚动private Canvas canvas;// 画布,用于绘制文本private OnTextClickListener onTextClickListener;// 点击事件监听器public interface OnTextClickListener {void onTextClick(int index, String text);}public void setOnTextClickListener(OnTextClickListener listener) {this.onTextClickListener = listener;}public ScrollTextView(Context context) {super(context);}public ScrollTextView(Context context, AttributeSet attrs) {super(context, attrs);surfaceHolder = this.getHolder();  //get The surface holdersurfaceHolder.addCallback(this);paint = new Paint();TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.ScrollTextView);clickEnable = arr.getBoolean(R.styleable.ScrollTextView_clickEnable, clickEnable);isHorizontal = arr.getBoolean(R.styleable.ScrollTextView_isHorizontal, isHorizontal);speed = arr.getInteger(R.styleable.ScrollTextView_speed, speed);textColor = arr.getColor(R.styleable.ScrollTextView_stTextColor, Color.BLACK);textSize = arr.getDimension(R.styleable.ScrollTextView_stTextSize, textSize);needScrollTimes = arr.getInteger(R.styleable.ScrollTextView_times, Integer.MAX_VALUE);isScrollForever = arr.getBoolean(R.styleable.ScrollTextView_isScrollForever, true);paint.setColor(textColor);paint.setTextSize(textSize);paint.setFlags(Paint.ANTI_ALIAS_FLAG);paint.setAntiAlias(true);paint.setFilterBitmap(true);setZOrderOnTop(true);  //控制表面视图的表面位于其窗口的顶部。getHolder().setFormat(PixelFormat.TRANSLUCENT);setFocusable(true);arr.recycle();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int mHeight = getFontHeight(textSize);      //实际的视图高viewWidth = MeasureSpec.getSize(widthMeasureSpec);viewHeight = MeasureSpec.getSize(heightMeasureSpec);// 当布局的宽度或高度是wrap_content,应该初始化ScrollTextView的宽度/高度if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, mHeight);viewHeight = mHeight;} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, viewHeight);} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, mHeight);viewHeight = mHeight;}}/*** surfaceChanged*/@Overridepublic void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {Log.d(TAG, "arg0:" + arg0.toString() + "  arg1:" + arg1 + "  arg2:" + arg2 + "  arg3:" + arg3);}/*** 界面出现,初始化一个新的滚动线程。** @param holder holder*/@Overridepublic void surfaceCreated(SurfaceHolder holder) {stopScroll = false;new Thread(new ScrollTextThread()).start();Log.d(TAG, "ScrollTextTextView is created");}/*** 界面退出,视图消失,自动调用** @param arg0 SurfaceHolder*/@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) {synchronized (this) {stopScroll = true;}Log.d(TAG, "ScrollTextTextView is destroyed");}private int getFontHeight(float fontSize) {Paint paint = new Paint();paint.setTextSize(fontSize);Paint.FontMetrics fm = paint.getFontMetrics();return (int) Math.ceil(fm.descent - fm.ascent);}public int getBackgroundColor() {return textBackColor;}public void setTextBackColor(int color) {this.setBackgroundColor(color);this.textBackColor = color;}public int getTextBackColor() {return textBackColor;}public int getSpeed() {return speed;}public int getCurrentTextIndex() {return currentTextIndex;}public void setTimes(int times) {if (times <= 0) {throw new IllegalArgumentException("times was invalid integer, it must between > 0");} else {needScrollTimes = times;isScrollForever = false;}}/*** 纵向滚动设置每段文字的停留时间* @param stayTimes*/public void setStayTimes(long stayTimes) {this.stayTimes = stayTimes;}public long getStayTimes() {return stayTimes;}public void setTextSize(float textSizeTem) {if (textSizeTem < 20) {throw new IllegalArgumentException("textSize must  > 20");} else if (textSizeTem > 900) {throw new IllegalArgumentException("textSize must  < 900");} else {this.textSize = sp2px(getContext(), textSizeTem);//重新设置Sizepaint.setTextSize(textSize);//视图区域也要改变measureTextParams(true);//实际的视图高int mHeight = getFontHeight(textSizeTem);android.view.ViewGroup.LayoutParams lp = this.getLayoutParams();lp.width = viewWidth;lp.height = dip2px(this.getContext(), mHeight);this.setLayoutParams(lp);isSetNewText = true;}}private int dip2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}private int sp2px(Context context, float spValue) {float fontScale = context.getResources().getDisplayMetrics().scaledDensity;return (int) (spValue * fontScale + 0.5f);}public int px2sp(Context context, float pxValue) {float fontScale = context.getResources().getDisplayMetrics().scaledDensity;return (int) (pxValue / fontScale + 0.5f);}public void setHorizontal(boolean horizontal) {isHorizontal = horizontal;}// 设置多个文本内容public void setTextList(List<String> textList) {this.textList = textList;this.currentTextIndex = 0;isSetNewText = true;stopScroll = false;measureTextParams(true);}public void setTextColor(@ColorInt int color) {textColor = color;paint.setColor(textColor);}public int getTextColor() {return textColor;}/*** 设置滚动速度** @param speed SCROLL SPEED [4,14] / 0?*/public void setSpeed(int speed) {if (speed > 14 || speed < 4) {throw new IllegalArgumentException("Speed was invalid integer, it must between 4 and 14");} else {this.speed = speed;}}/*** 设置是否永远滚动文本*/public void setScrollForever(boolean scrollForever) {isScrollForever = scrollForever;}public boolean isPauseScroll() {return pauseScroll;}public void setPauseScroll(boolean pauseScroll) {this.pauseScroll = pauseScroll;}public boolean isClickEnable() {return clickEnable;}public void setClickEnable(boolean clickEnable) {this.clickEnable = clickEnable;}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (!clickEnable) {return true;}switch (event.getAction()) {case MotionEvent.ACTION_DOWN:pauseScroll = !pauseScroll;if (onTextClickListener != null) {onTextClickListener.onTextClick(currentTextIndex, textList.get(currentTextIndex));}break;}return true;}private void drawVerticalScroll() {if (currentTextIndex >= textList.size()) return;// 计算基线String text = textList.get(currentTextIndex);float fontHeight = paint.getFontMetrics().bottom - paint.getFontMetrics().top;Paint.FontMetrics fontMetrics = paint.getFontMetrics();float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;float baseLine = (float) viewHeight / 2 + distance;// 是否需要水平滚动boolean needHorizontalScroll = paint.measureText(text) > viewWidth;// 水平滚动偏移float horizontalOffset = 0;// --- 阶段1:垂直滚动到基线位置(精准停止)---float currentVerticalPos = viewHeight + fontHeight; // 初始位置while (currentVerticalPos > baseLine) {if (stopScroll || isSetNewText) return;if (pauseScroll) {try {Thread.sleep(1000);} catch (InterruptedException ignored) {}continue;}// 计算下一步位置,避免越过基线float nextPos = currentVerticalPos - 3;if (nextPos < baseLine) nextPos = baseLine; // 确保不会越过基线drawText(0, nextPos);currentVerticalPos = nextPos;}// --- 阶段1暂停:基线位置暂停 ---if (needHorizontalScroll) {sleep(1000);} else if (!stopScroll && !isSetNewText) {sleep(stayTimes); // 原基线位置暂停}// --- 阶段2:水平滚动(垂直位置固定为基线)---if (needHorizontalScroll) {// 计算最大水平滚动偏移float maxHorizontalOffset = -(paint.measureText(text) - viewWidth);// 水平滚动while (horizontalOffset > maxHorizontalOffset) {if (stopScroll || isSetNewText) return;horizontalOffset -= speed;drawText(horizontalOffset, baseLine);}// --- 阶段2暂停:水平滚动完成后暂停 ---if (!stopScroll && !isSetNewText) {sleep(stayTimes); // 新增水平滚动完成后的暂停}}// --- 阶段3:继续垂直滚动到视图外 ---for (float i = baseLine; i > -fontHeight; i -= 3) {if (stopScroll || isSetNewText) return;if (pauseScroll) {sleep(1000);continue;}drawText(needHorizontalScroll ? horizontalOffset : 0, i);}// 切换到下一个文本currentTextIndex++;if (currentTextIndex >= textList.size()) {if (isScrollForever) currentTextIndex = 0;else stopScroll = true;}}/*** 绘制文本*/private synchronized void drawText(float x, float y) {try {canvas = surfaceHolder.lockCanvas();if (canvas == null) return;canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);canvas.drawText(textList.get(currentTextIndex), x, y, paint);} finally {if (canvas != null) {surfaceHolder.unlockCanvasAndPost(canvas);}}}@Overrideprotected void onVisibilityChanged(View changedView, int visibility) {super.onVisibilityChanged(changedView, visibility);this.setVisibility(visibility);}// 测量文本参数private void measureTextParams(boolean isInitialSetup) {if (currentTextIndex >= textList.size()) return;textWidth = paint.measureText(textList.get(currentTextIndex));viewWidthPlusTextLength = viewWidth + textWidth;if (isInitialSetup) {textX = viewWidth - viewWidth / 2; // 第一次创建 ,默认从居中的位置开始滚动Paint.FontMetrics fm = paint.getFontMetrics();float distance = (fm.bottom - fm.top) / 2 - fm.bottom;textY = viewHeight / 2 + distance;}}/*** Scroll thread*/class ScrollTextThread implements Runnable {@Overridepublic void run() {measureTextParams(true);while (!stopScroll && surfaceHolder != null && !Thread.currentThread().isInterrupted()) {if (isHorizontal) { // 水平滚动逻辑if (pauseScroll) {// 暂停sleep(1000);continue;}drawText(viewWidth - textX, textY);// 绘制文本textX += speed;// 文本滚动距离if (textX > viewWidthPlusTextLength) {// 文本从右至左滚动超出屏幕currentTextIndex++;textX = 0;--needScrollTimes;if (currentTextIndex >= textList.size()) {// 文字循环展示if (isScrollForever) {currentTextIndex = 0;} else {stopScroll = true;break;}}measureTextParams(false);}} else { // 垂直滚动逻辑drawVerticalScroll();isSetNewText = false;--needScrollTimes;}if (needScrollTimes <= 0 && isScrollForever) { // 滚动次数小于0,则滚动结束stopScroll = true;}}}}private void sleep(long millis) {try {Thread.sleep(millis);} catch (InterruptedException ignored) {}}}

5. 使用示例


        // 文字列表val textList = mutableListOf("关于开展2025年市直公益性岗位征集工作的通知","国家发通知要求规范OTA升级 广汽将构建全域智行安全守护体系","2月28日,工信部联合市场监管总局发布了《关于进一步加强智能网联汽车准入、召回及软件在线升级管理的通知》(以下简称《通知》),为汽车产业健康有序发展提供了顶层指引。对此,广汽集团表示,承诺所有安全投入绝不转嫁用户成本。")// 横向mBind.scrollTextH.apply {setTextList(textList)this.isHorizontal = truespeed = 4textColor = Color.WHITEtextBackColor = R.color.teal_200isClickEnable = truesetOnTextClickListener { index, text ->ToastUtils.show("点击了 $index $text")}}// 竖向mBind.scrollTextV.apply {setTextList(textList) // 文字列表this.isHorizontal = false // 是否横向滚动speed = 4 // 滚动速度stayTimes = 3000 // 停留时间 3stextColor = Color.WHITE // 文字颜色textBackColor = R.color.teal_700 // 文字背景颜色isClickEnable = false // 是否可点击}

6. 最后


以上功能可能只满足部分场景,如需要更多功能,比如:滚动方向,多行滚动等,可自行拓展。直接看代码,可能不容易理解,但是把功能的细节点全都剖析出来,在脑子里有个完整的轮廓,再去看代码,就事半功倍了。see you ~

关键字:工程建设管理条例_推广app收益排行榜_seo排名快速上升_实体店铺引流推广方法

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: