Android-TextView跑马灯探秘

前言

自定义View实现的跑马灯一直没有实现类似 Android TextView 的跑马灯首尾相接的效果,所以一直想看看Android TextView 的跑马灯是如何实现

本文主要探秘 Android TextView 的跑马灯实现原理及实现自下往上效果的跑马灯

探秘

TextView#onDraw

原生 Android TextView 如何设置开启跑马灯效果,此处不再描述,View 的绘制都在 onDraw 方法中,这里直接查看 TextView#onDraw() 方法,删减一些不关心的代码

 protected void onDraw(Canvas canvas) {
     // 是否需要重启启动跑马灯
     restartMarqueeIfNeeded();
 ​
     // Draw the background for this view
     super.onDraw(canvas);
         
     // 删减不关心的代码
 ​
     // 创建`mLayout`对象, 此处为`StaticLayout`
     if (mLayout == null) {
         assumeLayout();
     }
 ​
     Layout layout = mLayout;
 ​
     canvas.save();
 ​
     // 删减不关心的代码
 ​
     final int layoutDirection = getLayoutDirection();
     final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
 ​
     // 判断跑马灯设置项是否正确
     if (isMarqueeFadeEnabled()) {
         if (!mSingleLine && getLineCount() == 1 && canMarquee()
               && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
            final int width = mRight - mLeft;
            final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
            final float dx = mLayout.getLineRight(0) - (width - padding);
            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         }
 ​
         // 判断跑马灯是否启动
         if (mMarquee != null && mMarquee.isRunning()) {
             final float dx = -mMarquee.getScroll();
             // 移动画布
             canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         }
     }
 ​
     final int cursorOffsetVertical = voffsetCursor - voffsetText;
 ​
     Path highlight = getUpdatedHighlightPath();
     if (mEditor != null) {
         mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
     } else {
         // 绘制文本
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }
 ​
     // 判断是否可以绘制尾部文本
     if (mMarquee != null && mMarquee.shouldDrawGhost()) {
         final float dx = mMarquee.getGhostOffset();
         // 移动画布
         canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         // 绘制尾部文本
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }
 ​
     canvas.restore();
 } 

Marquee

根据 onDraw() 方法分析,跑马灯效果的实现主要依赖 mMarquee 这个对象来实现,好的,看下 Marquee 吧,Marquee 代码较少,就贴上全部源码吧

 private static final class Marquee {
     // TODO: Add an option to configure this
     // 缩放相关,不关心此字段
     private static final float MARQUEE_DELTA_MAX = 0.07f;
     
     // 跑马灯跑完一次后多久开始下一次
     private static final int MARQUEE_DELAY = 1200;
     
     // 绘制一次跑多长距离因子,此字段与速度相关
     private static final int MARQUEE_DP_PER_SECOND = 30;
 ​
     // 跑马灯状态常量
     private static final byte MARQUEE_STOPPED = 0x0;
     private static final byte MARQUEE_STARTING = 0x1;
     private static final byte MARQUEE_RUNNING = 0x2;
 ​
     // 对TextView进行弱引用
     private final WeakReference<TextView> mView;
     
     // 帧率相关
     private final Choreographer mChoreographer;
 ​
     // 状态
     private byte mStatus = MARQUEE_STOPPED;
     
     // 绘制一次跑多长距离
     private final float mPixelsPerMs;
     
     // 最大滚动距离
     private float mMaxScroll;
     
     // 是否可以绘制右阴影, 右侧淡入淡出效果
     private float mMaxFadeScroll;
     
     // 尾部文本什么时候开始绘制
     private float mGhostStart;
     
     // 尾部文本绘制位置偏移量
     private float mGhostOffset;
     
     // 是否可以绘制左阴影,左侧淡入淡出效果
     private float mFadeStop;
     
     // 重复限制
     private int mRepeatLimit;
 ​
     // 跑动距离
     private float mScroll;
     
     // 最后一次跑动时间,单位毫秒
     private long mLastAnimationMs;
 ​
     Marquee(TextView v) {
         final float density = v.getContext().getResources().getDisplayMetrics().density;
         // 计算每次跑多长距离
         mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
         mView = new WeakReference<TextView>(v);
         mChoreographer = Choreographer.getInstance();
     }
 ​
     // 帧率回调,用于跑马灯跑动
     private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
             tick();
         }
     };
 ​
     // 帧率回调,用于跑马灯开始跑动
     private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
             mStatus = MARQUEE_RUNNING;
             mLastAnimationMs = mChoreographer.getFrameTime();
             tick();
         }
     };
 ​
     // 帧率回调,用于跑马灯重新跑动
     private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
             if (mStatus == MARQUEE_RUNNING) {
                 if (mRepeatLimit >= 0) {
                     mRepeatLimit--;
                 }
                 start(mRepeatLimit);
             }
         }
     };
 ​
     // 跑马灯跑动实现
     void tick() {
         if (mStatus != MARQUEE_RUNNING) {
             return;
         }
 ​
         mChoreographer.removeFrameCallback(mTickCallback);
 ​
         final TextView textView = mView.get();
         // 判断TextView是否处于获取焦点或选中状态
         if (textView != null && (textView.isFocused() || textView.isSelected())) {
             // 获取当前时间
             long currentMs = mChoreographer.getFrameTime();
             // 计算当前时间与上次时间的差值
             long deltaMs = currentMs - mLastAnimationMs;
             mLastAnimationMs = currentMs;
             // 根据时间差计算本次跑动的距离,减轻视觉上跳动/卡顿
             float deltaPx = deltaMs * mPixelsPerMs;
             // 计算跑动距离
             mScroll += deltaPx;
             // 判断是否已经跑完
             if (mScroll > mMaxScroll) {
                 mScroll = mMaxScroll;
                 // 发送重新开始跑动事件
                 mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
             } else {
                 // 发送下一次跑动事件
                 mChoreographer.postFrameCallback(mTickCallback);
             }
             // 调用此方法会触发执行`onDraw`方法
             textView.invalidate();
         }
     }
 ​
     // 停止跑马灯
     void stop() {
         mStatus = MARQUEE_STOPPED;
         mChoreographer.removeFrameCallback(mStartCallback);
         mChoreographer.removeFrameCallback(mRestartCallback);
         mChoreographer.removeFrameCallback(mTickCallback);
         resetScroll();
     }
 ​
     private void resetScroll() {
         mScroll = 0.0f;
         final TextView textView = mView.get();
         if (textView != null) textView.invalidate();
     }
 ​
     // 启动跑马灯
     void start(int repeatLimit) {
         if (repeatLimit == 0) {
             stop();
             return;
         }
         mRepeatLimit = repeatLimit;
         final TextView textView = mView.get();
         if (textView != null && textView.mLayout != null) {
             // 设置状态为在跑
             mStatus = MARQUEE_STARTING;
             // 重置跑动距离
             mScroll = 0.0f;
             // 计算TextView宽度
             final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
                 - textView.getCompoundPaddingRight();
             // 获取文本第0行的宽度
             final float lineWidth = textView.mLayout.getLineWidth(0);
             // 取TextView宽度的三分之一
             final float gap = textWidth / 3.0f;
             // 计算什么时候可以开始绘制尾部文本:首部文本跑动到哪里可以绘制尾部文本
             mGhostStart = lineWidth - textWidth + gap;
             // 计算最大滚动距离:什么时候认为跑完一次
             mMaxScroll = mGhostStart + textWidth;
             // 尾部文本绘制偏移量
             mGhostOffset = lineWidth + gap;
             // 跑动到哪里时不绘制左侧阴影
             mFadeStop = lineWidth + textWidth / 6.0f;
             // 跑动到哪里时不绘制右侧阴影
             mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
 ​
             textView.invalidate();
             // 开始跑动
             mChoreographer.postFrameCallback(mStartCallback);
         }
     }
 ​
     // 获取尾部文本绘制位置偏移量
     float getGhostOffset() {
         return mGhostOffset;
     }
 ​
     // 获取当前滚动距离
     float getScroll() {
         return mScroll;
     }
 ​
     // 获取可以右侧阴影绘制的最大距离
     float getMaxFadeScroll() {
         return mMaxFadeScroll;
     }
 ​
     // 判断是否可以绘制左侧阴影
     boolean shouldDrawLeftFade() {
         return mScroll <= mFadeStop;
     }
 ​
     // 判断是否可以绘制尾部文本
     boolean shouldDrawGhost() {
         return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
     }
 ​
     // 跑马灯是否在跑
     boolean isRunning() {
         return mStatus == MARQUEE_RUNNING;
     }
 ​
     // 跑马灯是否不跑
     boolean isStopped() {
         return mStatus == MARQUEE_STOPPED;
     }
 } 

好的,分析完 Marquee,跑马灯实现原理豁然明亮

  1. TextView 开启跑马灯效果时调用 Marquee#start() 方法
  2. Marquee#start() 方法中触发 TextView 重绘,开始计算跑动距离
  3. TextView#onDraw() 方法中根据跑动距离移动画布并绘制首部文本,再根据跑动距离判断是否可以移动画布绘制尾部文本

小结

TextView 通过移动画布绘制两次文本实现跑马灯效果,根据两帧绘制的时间差计算跑动距离,怎一个"妙"字了得

应用

上面分析完原生 Android TextView 跑马灯的实现原理,但是原生 Android TextView 跑马灯有几点不足:

  1. 无法设置跑动速度
  2. 无法设置重跑间隔时长
  3. 无法实现上下跑动

以上第1、2点在上面 Marquee 分析中已经有解决方案,接下来根据原生实现原理实现第3点上下跑动

MarqueeTextView

这里给出实现方案,列出主要实现逻辑,继承 AppCompatTextView,复写 onDraw() 方法,上下跑动主要是计算上下跑动的距离,然后再次重绘 TextView 上下移动画布绘制文本

 /**
  * 继承AppCompatTextView,复写onDraw方法
  */
 public class MarqueeTextView extends AppCompatTextView {
 ​
     private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFEFEFEF");
 ​
     @IntDef({HORIZONTAL, VERTICAL})
     @Retention(RetentionPolicy.SOURCE)
     public @interface OrientationMode {
     }
 ​
     public static final int HORIZONTAL = 0;
     public static final int VERTICAL = 1;
 ​
     private Marquee mMarquee;
     private boolean mRestartMarquee;
     private boolean isMarquee;
 ​
     private int mOrientation;
 ​
     public MarqueeTextView(@NonNull Context context) {
         this(context, null);
     }
 ​
     public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
         this(context, attrs, 0);
     }
 ​
     public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 ​
         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MarqueeTextView, defStyleAttr, 0);
 ​
         mOrientation = ta.getInt(R.styleable.MarqueeTextView_orientation, HORIZONTAL);
 ​
         ta.recycle();
     }
 ​
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
 ​
         if (mOrientation == HORIZONTAL) {
             if (getWidth() > 0) {
                 mRestartMarquee = true;
             }
         } else {
             if (getHeight() > 0) {
                 mRestartMarquee = true;
             }
         }
     }
 ​
     private void restartMarqueeIfNeeded() {
         if (mRestartMarquee) {
             mRestartMarquee = false;
             startMarquee();
         }
     }
 ​
     public void setMarquee(boolean marquee) {
         boolean wasStart = isMarquee();
 ​
         isMarquee = marquee;
 ​
         if (wasStart != marquee) {
             if (marquee) {
                 startMarquee();
             } else {
                 stopMarquee();
             }
         }
     }
 ​
     public void setOrientation(@OrientationMode int orientation) {
         mOrientation = orientation;
     }
 ​
     public int getOrientation() {
         return mOrientation;
     }
 ​
     public boolean isMarquee() {
         return isMarquee;
     }
 ​
     private void stopMarquee() {
         if (mOrientation == HORIZONTAL) {
             setHorizontalFadingEdgeEnabled(false);
         } else {
             setVerticalFadingEdgeEnabled(false);
         }
 ​
         requestLayout();
         invalidate();
 ​
         if (mMarquee != null && !mMarquee.isStopped()) {
             mMarquee.stop();
         }
     }
 ​
     private void startMarquee() {
         if (canMarquee()) {
 ​
             if (mOrientation == HORIZONTAL) {
                 setHorizontalFadingEdgeEnabled(true);
             } else {
                 setVerticalFadingEdgeEnabled(true);
             }
 ​
             if (mMarquee == null) mMarquee = new Marquee(this);
             mMarquee.start(-1);
         }
     }
 ​
     private boolean canMarquee() {
         if (mOrientation == HORIZONTAL) {
             int viewWidth = getWidth() - getCompoundPaddingLeft() -
                 getCompoundPaddingRight();
             float lineWidth = getLayout().getLineWidth(0);
             return (mMarquee == null || mMarquee.isStopped())
                 && (isFocused() || isSelected() || isMarquee())
                 && viewWidth > 0
                 && lineWidth > viewWidth;
         } else {
             int viewHeight = getHeight() - getCompoundPaddingTop() -
                 getCompoundPaddingBottom();
             float textHeight = getLayout().getHeight();
             return (mMarquee == null || mMarquee.isStopped())
                 && (isFocused() || isSelected() || isMarquee())
                 && viewHeight > 0
                 && textHeight > viewHeight;
         }
     }
 ​
     /**
      * 仿照TextView#onDraw()方法
      */
     @Override
     protected void onDraw(Canvas canvas) {
         restartMarqueeIfNeeded();
 ​
         super.onDraw(canvas);
 ​
         // 再次绘制背景色,覆盖下面由TextView绘制的文本,视情况可以不调用`super.onDraw(canvas);`
         // 如果没有背景色则使用默认颜色
         Drawable background = getBackground();
         if (background != null) {
             background.draw(canvas);
         } else {
             canvas.drawColor(DEFAULT_BG_COLOR);
         }
 ​
         canvas.save();
 ​
         canvas.translate(0, 0);
 ​
         // 实现左右跑马灯
         if (mOrientation == HORIZONTAL) {
             if (mMarquee != null && mMarquee.isRunning()) {
                 final float dx = -mMarquee.getScroll();
                 canvas.translate(dx, 0.0F);
             }
 ​
             getLayout().draw(canvas, null, null, 0);
 ​
             if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                 final float dx = mMarquee.getGhostOffset();
                 canvas.translate(dx, 0.0F);
                 getLayout().draw(canvas, null, null, 0);
             }
         } else {
             // 实现上下跑马灯
             if (mMarquee != null && mMarquee.isRunning()) {
                 final float dy = -mMarquee.getScroll();
                 canvas.translate(0.0F, dy);
             }
 ​
             getLayout().draw(canvas, null, null, 0);
 ​
             if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                 final float dy = mMarquee.getGhostOffset();
                 canvas.translate(0.0F, dy);
                 getLayout().draw(canvas, null, null, 0);
             }
         }
 ​
         canvas.restore();
     }
 } 

Marquee

 private static final class Marquee {
     // 修改此字段设置重跑时间间隔 - 对应不足点2
     private static final int MARQUEE_DELAY = 1200;
 ​
     // 修改此字段设置跑动速度 - 对应不足点1
     private static final int MARQUEE_DP_PER_SECOND = 30;
 ​
     private static final byte MARQUEE_STOPPED = 0x0;
     private static final byte MARQUEE_STARTING = 0x1;
     private static final byte MARQUEE_RUNNING = 0x2;
 ​
     private static final String METHOD_GET_FRAME_TIME = "getFrameTime";
 ​
     private final WeakReference<MarqueeTextView> mView;
     private final Choreographer mChoreographer;
 ​
     private byte mStatus = MARQUEE_STOPPED;
     private final float mPixelsPerSecond;
     private float mMaxScroll;
     private float mMaxFadeScroll;
     private float mGhostStart;
     private float mGhostOffset;
     private float mFadeStop;
     private int mRepeatLimit;
 ​
     private float mScroll;
     private long mLastAnimationMs;
 ​
     Marquee(MarqueeTextView v) {
         final float density = v.getContext().getResources().getDisplayMetrics().density;
         mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
         mView = new WeakReference<>(v);
         mChoreographer = Choreographer.getInstance();
     }
 ​
     private final Choreographer.FrameCallback mTickCallback = frameTimeNanos -> tick();
 ​
     private final Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
             mStatus = MARQUEE_RUNNING;
             mLastAnimationMs = getFrameTime();
             tick();
         }
     };
 ​
     /**
      * `getFrameTime`是隐藏api,此处使用反射调用,高系统版本可能失效,可使用某些方案绕过此限制
      */
     @SuppressLint("PrivateApi")
     private long getFrameTime() {
         try {
             Class<? extends Choreographer> clz = mChoreographer.getClass();
             Method getFrameTime = clz.getDeclaredMethod(METHOD_GET_FRAME_TIME);
             getFrameTime.setAccessible(true);
             return (long) getFrameTime.invoke(mChoreographer);
         } catch (Exception e) {
             e.printStackTrace();
             return 0;
         }
     }
 ​
     private final Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
             if (mStatus == MARQUEE_RUNNING) {
                 if (mRepeatLimit >= 0) {
                     mRepeatLimit--;
                 }
                 start(mRepeatLimit);
             }
         }
     };
 ​
     void tick() {
         if (mStatus != MARQUEE_RUNNING) {
             return;
         }
 ​
         mChoreographer.removeFrameCallback(mTickCallback);
 ​
         final MarqueeTextView textView = mView.get();
         if (textView != null && (textView.isFocused() || textView.isSelected() || textView.isMarquee())) {
             long currentMs = getFrameTime();
             long deltaMs = currentMs - mLastAnimationMs;
             mLastAnimationMs = currentMs;
             float deltaPx = deltaMs / 1000F * mPixelsPerSecond;
             mScroll += deltaPx;
             if (mScroll > mMaxScroll) {
                 mScroll = mMaxScroll;
                 mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
             } else {
                 mChoreographer.postFrameCallback(mTickCallback);
             }
             textView.invalidate();
         }
     }
 ​
     void stop() {
         mStatus = MARQUEE_STOPPED;
         mChoreographer.removeFrameCallback(mStartCallback);
         mChoreographer.removeFrameCallback(mRestartCallback);
         mChoreographer.removeFrameCallback(mTickCallback);
         resetScroll();
     }
 ​
     private void resetScroll() {
         mScroll = 0.0F;
         final MarqueeTextView textView = mView.get();
         if (textView != null) textView.invalidate();
     }
 ​
     void start(int repeatLimit) {
         if (repeatLimit == 0) {
             stop();
             return;
         }
         mRepeatLimit = repeatLimit;
         final MarqueeTextView textView = mView.get();
         if (textView != null && textView.getLayout() != null) {
             mStatus = MARQUEE_STARTING;
             mScroll = 0.0F;
 ​
             // 分别计算左右和上下跑动所需的数据
             if (textView.getOrientation() == HORIZONTAL) {
                 int viewWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
                     textView.getCompoundPaddingRight();
                 float lineWidth = textView.getLayout().getLineWidth(0);
                 float gap = viewWidth / 3.0F;
                 mGhostStart = lineWidth - viewWidth + gap;
                 mMaxScroll = mGhostStart + viewWidth;
                 mGhostOffset = lineWidth + gap;
                 mFadeStop = lineWidth + viewWidth / 6.0F;
                 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
             } else {
                 int viewHeight = textView.getHeight() - textView.getCompoundPaddingTop() -
                     textView.getCompoundPaddingBottom();
                 float textHeight = textView.getLayout().getHeight();
                 float gap = viewHeight / 3.0F;
                 mGhostStart = textHeight - viewHeight + gap;
                 mMaxScroll = mGhostStart + viewHeight;
                 mGhostOffset = textHeight + gap;
                 mFadeStop = textHeight + viewHeight / 6.0F;
                 mMaxFadeScroll = mGhostStart + textHeight + textHeight;
             }
 ​
             textView.invalidate();
             mChoreographer.postFrameCallback(mStartCallback);
         }
     }
 ​
     float getGhostOffset() {
         return mGhostOffset;
     }
 ​
     float getScroll() {
         return mScroll;
     }
 ​
     float getMaxFadeScroll() {
         return mMaxFadeScroll;
     }
 ​
     boolean shouldDrawLeftFade() {
         return mScroll <= mFadeStop;
     }
 ​
     boolean shouldDrawTopFade() {
         return mScroll <= mFadeStop;
     }
 ​
     boolean shouldDrawGhost() {
         return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
     }
 ​
     boolean isRunning() {
         return mStatus == MARQUEE_RUNNING;
     }
 ​
     boolean isStopped() {
         return mStatus == MARQUEE_STOPPED;
     }
 } 

效果

跑马灯

happy~

文末

如果要想成为Android架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Android中,可以通过设置TextView的一些属性来实现马灯效果。其中包括设置ellipsize属性为marquee,marqueeRepeatLimit属性为marquee_forever,focusable属性为true,singleLine属性为true等。通过这些属性的设置,可以让TextView的文字在有限的宽度下实现马灯效果,让用户完整地看到所有的文字。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Android——TextView实现真正的马灯效果](https://blog.csdn.net/u013836857/article/details/51423393)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [Android TextView设置马灯效果](https://blog.csdn.net/qq_43278826/article/details/122668992)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Android三种方式实现TextView马灯效果](https://blog.csdn.net/qq_26440221/article/details/52621302)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值