├── .gitignore ├── .idea ├── gradle.xml ├── kotlinc.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── xiaweizi │ │ └── marqueetextview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── xiaweizi │ │ │ └── marqueetextview │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── xiaweizi │ └── marqueetextview │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── marquee ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── xiaweizi │ │ └── marquee │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── xiaweizi │ │ │ └── marquee │ │ │ └── MarqueeTextView.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── xiaweizi │ └── marquee │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 85 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Abstraction issuesJava 105 | 106 | 107 | AccessibilityLintAndroid 108 | 109 | 110 | Android 111 | 112 | 113 | Annotations verifyingGroovy 114 | 115 | 116 | Assignment issuesGroovy 117 | 118 | 119 | Assignment issuesJava 120 | 121 | 122 | Bidirectional TextInternationalizationLintAndroid 123 | 124 | 125 | Bitwise operation issuesJava 126 | 127 | 128 | C/C++ 129 | 130 | 131 | Chrome OSCorrectnessLintAndroid 132 | 133 | 134 | Class metricsJava 135 | 136 | 137 | Class structureJava 138 | 139 | 140 | Cloning issuesJava 141 | 142 | 143 | Code style issuesJava 144 | 145 | 146 | Concurrency annotation issuesJava 147 | 148 | 149 | Control FlowGroovy 150 | 151 | 152 | Control flow issuesJava 153 | 154 | 155 | CorrectnessLintAndroid 156 | 157 | 158 | Data flow analysisC/C++ 159 | 160 | 161 | Data flow issuesJava 162 | 163 | 164 | Declaration redundancyJava 165 | 166 | 167 | Dependency issuesJava 168 | 169 | 170 | Encapsulation issuesJava 171 | 172 | 173 | Error handlingGroovy 174 | 175 | 176 | Error handlingJava 177 | 178 | 179 | Finalization issuesJava 180 | 181 | 182 | General 183 | 184 | 185 | GeneralC/C++ 186 | 187 | 188 | Google Cloud Endpoints 189 | 190 | 191 | Groovy 192 | 193 | 194 | HTML 195 | 196 | 197 | IconsUsabilityLintAndroid 198 | 199 | 200 | ImportsJava 201 | 202 | 203 | Inheritance issuesJava 204 | 205 | 206 | Initialization issuesJava 207 | 208 | 209 | Internationalization issues 210 | 211 | 212 | Internationalization issuesJava 213 | 214 | 215 | InternationalizationLintAndroid 216 | 217 | 218 | J2ME issuesJava 219 | 220 | 221 | JSON 222 | 223 | 224 | JUnit issuesJava 225 | 226 | 227 | Java 228 | 229 | 230 | Java 5Java language level migration aidsJava 231 | 232 | 233 | Java 7Java language level migration aidsJava 234 | 235 | 236 | Java 8Java language level migration aidsJava 237 | 238 | 239 | Java interop issuesKotlin 240 | 241 | 242 | Java language level migration aidsJava 243 | 244 | 245 | JavaBeans issuesJava 246 | 247 | 248 | Javadoc issuesJava 249 | 250 | 251 | Kotlin 252 | 253 | 254 | Kotlin Android 255 | 256 | 257 | Language Injection 258 | 259 | 260 | LintAndroid 261 | 262 | 263 | LintLintAndroid 264 | 265 | 266 | Logging issuesJava 267 | 268 | 269 | Memory issuesJava 270 | 271 | 272 | MessagesCorrectnessLintAndroid 273 | 274 | 275 | Method MetricsGroovy 276 | 277 | 278 | Method metricsJava 279 | 280 | 281 | Modularization issuesJava 282 | 283 | 284 | Naming ConventionsGroovy 285 | 286 | 287 | Naming conventionsJava 288 | 289 | 290 | Naming conventionsKotlin 291 | 292 | 293 | Numeric issuesJava 294 | 295 | 296 | Other problemsKotlin 297 | 298 | 299 | Packaging issuesJava 300 | 301 | 302 | Performance issuesJava 303 | 304 | 305 | PerformanceLintAndroid 306 | 307 | 308 | Portability issuesJava 309 | 310 | 311 | Potentially confusing code constructsGroovy 312 | 313 | 314 | Probable bugsGroovy 315 | 316 | 317 | Probable bugsJava 318 | 319 | 320 | Probable bugsKotlin 321 | 322 | 323 | Properties Files 324 | 325 | 326 | Redundant constructsKotlin 327 | 328 | 329 | RegExp 330 | 331 | 332 | Resource management issuesJava 333 | 334 | 335 | Security issuesJava 336 | 337 | 338 | SecurityLintAndroid 339 | 340 | 341 | Serialization issuesJava 342 | 343 | 344 | Style issuesKotlin 345 | 346 | 347 | TestNGJava 348 | 349 | 350 | Threading issuesGroovy 351 | 352 | 353 | Threading issuesJava 354 | 355 | 356 | Type checksC/C++ 357 | 358 | 359 | TypographyUsabilityLintAndroid 360 | 361 | 362 | UsabilityLintAndroid 363 | 364 | 365 | Verbose or redundant code constructsJava 366 | 367 | 368 | Visibility issuesJava 369 | 370 | 371 | XML 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 383 | 384 | $USER_HOME$/.subversion 385 | 386 | 387 | 388 | 389 | 390 | 1.8 391 | 392 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 408 | 409 | 410 | 411 | 412 | 413 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 最近公司接到小需求--「可以滚动的提示」,其实就是跑马灯。这让我想到了大学时专业**物联网**,当时学的单片机入门教程就是跑马灯,很是亲切。其实就是灯(或文字)按照某个方向循环滚动。 4 | 5 | ## Android 原生的跑马灯 6 | 7 | 其实,`Android`中的`TextView`自带跑马灯效果,只需要通过简单的配置,就可以完成滚动的效果。 8 | 9 | 在`XML`中进行配置 10 | 11 | 23 | 24 | 可以看到需要很多的属性配置,了解一下每个属性的含义: 25 | 26 | - `android:ellipsize="marquee"` 设置为跑马灯效果 27 | - `android:focusable="true"` 获取焦点 28 | - `android:focusableInTouchMode="true"` touch 时获取焦点 29 | - `android:marqueeRepeatLimit="marquee_forever"` 设置重复次数 30 | - `android:scrollHorizontally="true"` 设置为水平滚动 31 | - `android:singleLine="true"` 单行显示 32 | 33 | 按照上面的配置,正常情况下是可以运转的,但是用到项目中的时候,会发现很多`bug`和不足之处。 34 | 35 | 比如,偶尔突然不滚动了,具体的原因是没有获取到焦点。我觉得这是原生跑马灯最坑的一点,必须获取到焦点才能正常运行。 36 | 37 | 当然解决方式也有,第一种,通过主动获取焦点的方式,即调用`view.setFocusable(true)`。还有一种就是重写`TextView`的`isFocused()`方法,强制让他获取焦点。 38 | 39 | @Override 40 | public boolean isFocused() { 41 | return true; 42 | } 43 | 44 | 就算这样,在遇到复杂的界面还是会遇到问题,要么焦点会被断断续续的被抢夺,导致卡顿,要么不符合`UI`提出的滚动速度要求。 45 | 46 | 47 | ## 自定义跑马灯 48 | 49 | 鉴于这个背景,通过`Scroller`完成自定义的跑马灯,代码已上传至`GitHub`上:[MarqueeTextView](https://github.com/xiaweizi/MarqueeTextView) 50 | 51 | 先看一下整体的效果: 52 | 53 | ![MarqueeTextView](http://upload-images.jianshu.io/upload_images/4043475-693d71be1451c080.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 54 | 55 | 如果想直接使用,在根`build.gradle`配置: 56 | 57 | allprojects { 58 | repositories { 59 | ... 60 | maven { url 'https://jitpack.io' } 61 | } 62 | } 63 | 64 | 在`app`下的`build.gradle`添加依赖 65 | 66 | dependencies { 67 | compile 'com.github.xiaweizi:MarqueeTextView:1.0' 68 | } 69 | 70 | 最后在`XML`直接使用即可: 71 | 72 | 83 | 84 | 具有一下功能: 85 | 86 | - 控制滚动时间 87 | - 控制滚动延迟 88 | - 控制滚动模式 89 | - 生命周期可以自己控制 90 | - 暂停 91 | - 继续 92 | - 重新开始 93 | - 停止 94 | 95 | ## 实现原理 96 | 97 | 通过`Scroller`控制器来控制整个`View`的滚动,那什么是`Scroller`,做个简单的介绍。 98 | 99 | `Scroller`内部封装了滚动的操作,通过构造函数中传入插值器。可以控制起始位置和整个滚动的时间,并且通过`computeScrollOffset()`得到滚动动作是否结束。 100 | 101 | 最核心的方法有两个: 102 | 103 | 1. `startScroll` 104 | 105 | /** 106 | * @param startX 水平方向滚动的偏移值,以像素为单位。 107 | * @param startY 垂直方向滚动的偏移值,以像素为单位 108 | * @param dx 水平方向滚动的距离 109 | * @param dy 垂直方向滚动的距离 110 | * @param duration 滚动持续的时间,以毫秒为单位 111 | */ 112 | public void startScroll (int startX, int startY, int dx, int dy, int duration) { 113 | ... 114 | } 115 | 116 | 2. `computeScrollOffset` 117 | 118 | /** 119 | * @return 返回动画是否结束 120 | */ 121 | public boolean computeScrollOffset (){ 122 | ... 123 | } 124 | 125 | 注释已经很清楚了,那么接下来讲一下滚动的大概实现。 126 | 127 | **首先**,要算出从初始位置开始滚动,到结束的距离,其实就是文字的长度。 128 | 129 | /** 130 | * 计算滚动的距离 131 | * @return 滚动的距离 132 | */ 133 | private int calculateScrollingLen() { 134 | TextPaint tp = getPaint(); 135 | Rect rect = new Rect(); 136 | String strTxt = getText().toString(); 137 | tp.getTextBounds(strTxt, 0, strTxt.length(), rect); 138 | return rect.width(); 139 | } 140 | 141 | **其次**,调用`startScroll`方法进行滚动,注意的是需要调用`invalidate`方法,才会有效果。 142 | 143 | **最后**一个问题就是,滚动结束后继续滚动。`Scroller`在滚动的时候,会不断回调`View`的`computeScroll`方法,于是就可以在这个方法里进行判断,如果结束了,就重新开始。 144 | 145 | 到此一个简单的跑马灯效果就实现了,当然如果还想添加别的需要,只要搞懂其原理,这些都不是问题。 146 | 147 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 26 9 | defaultConfig { 10 | applicationId "com.xiaweizi.marqueetextview" 11 | minSdkVersion 19 12 | targetSdkVersion 26 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(include: ['*.jar'], dir: 'libs') 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 28 | implementation 'com.android.support:appcompat-v7:26.1.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 30 | testImplementation 'junit:junit:4.12' 31 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 32 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 33 | implementation project(':marquee') 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/xiaweizi/marqueetextview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.xiaweizi.marqueetextview 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.xiaweizi.marqueetextview", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaweizi/marqueetextview/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.xiaweizi.marqueetextview 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import kotlinx.android.synthetic.main.activity_main.* 6 | 7 | class MainActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | resume.setOnClickListener { 13 | marquee1.resumeScroll() 14 | marquee2.resumeScroll() 15 | marquee3.resumeScroll() 16 | marquee4.resumeScroll() 17 | } 18 | 19 | pause.setOnClickListener { 20 | marquee1.pauseScroll() 21 | marquee2.pauseScroll() 22 | marquee3.pauseScroll() 23 | marquee4.pauseScroll() 24 | } 25 | 26 | restart.setOnClickListener { 27 | marquee1.startScroll() 28 | marquee2.startScroll() 29 | marquee3.startScroll() 30 | marquee4.startScroll() 31 | } 32 | 33 | stop.setOnClickListener { 34 | marquee1.stopScroll() 35 | marquee2.stopScroll() 36 | marquee3.stopScroll() 37 | marquee4.stopScroll() 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 35 | 36 | 49 | 50 | 63 | 64 |