Full Code of lsjwzh/FastTextView for AI

master c6234059f97c cached
91 files
290.4 KB
83.3k tokens
407 symbols
1 requests
Download .txt
Showing preview only (318K chars total). Download the full file or copy to clipboard to get everything.
Repository: lsjwzh/FastTextView
Branch: master
Commit: c6234059f97c
Files: 91
Total size: 290.4 KB

Directory structure:
gitextract_dtlfnmzy/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── com/
│           │       ├── lsjwzh/
│           │       │   └── test/
│           │       │       ├── AutoScrollHandler.java
│           │       │       ├── Const.java
│           │       │       ├── FastTextLayoutView.java
│           │       │       ├── FastTextView.java
│           │       │       ├── FpsCalculator.java
│           │       │       ├── GhostThread.java
│           │       │       ├── StaticLayoutManager.java
│           │       │       ├── TestSingleLineTextView.java
│           │       │       ├── TestSpan.java
│           │       │       ├── TestStats.java
│           │       │       ├── TestTextView.java
│           │       │       ├── TextLineView.java
│           │       │       └── Util.java
│           │       └── wechat/
│           │           └── testdemo/
│           │               ├── EllipseFragment.java
│           │               ├── FastTextViewListTestFragment.java
│           │               ├── MainActivity.java
│           │               ├── MainActivityFragment.java
│           │               ├── NormalLayoutTestFragment.java
│           │               ├── ReadMoreFragment.java
│           │               ├── ReadMoreListTestFragment.java
│           │               ├── StaticLayoutCacheTestFragment.java
│           │               └── TestListAdapter.java
│           └── res/
│               ├── anim/
│               │   ├── popup_enter.xml
│               │   └── popup_exit.xml
│               ├── drawable/
│               │   └── popup_background.xml
│               ├── layout/
│               │   ├── activity_main.xml
│               │   ├── ellipse_demo.xml
│               │   ├── fast_layout_ui.xml
│               │   ├── fast_list_item.xml
│               │   ├── fragment_main.xml
│               │   ├── layout_cache_demo.xml
│               │   ├── normal_layout_ui.xml
│               │   ├── normal_list_item.xml
│               │   ├── read_more_demo.xml
│               │   ├── readmore_list_item.xml
│               │   ├── spinner_item.xml
│               │   ├── static_layout_ui.xml
│               │   └── static_list_item.xml
│               ├── menu/
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-v23/
│                   └── styles.xml
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── text.Textline/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── android/
│                   └── text/
│                       ├── Directions.java
│                       ├── ITextLine.java
│                       ├── TextLineCompat.java
│                       ├── TextLineImpl15.java
│                       └── TextLineImpl23.java
└── widget.FastTextView/
    ├── .gitignore
    ├── build.gradle
    ├── proguard-rules.pro
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   ├── android/
            │   │   └── text/
            │   │       ├── EllipsisSpannedContainer.java
            │   │       ├── LayoutUtils.java
            │   │       ├── SpanSetCompat.java
            │   │       ├── StaticLayoutBuilderCompat.java
            │   │       ├── TextLayoutCache.java
            │   │       └── TextLayoutWarmer.java
            │   └── com/
            │       └── lsjwzh/
            │           └── widget/
            │               └── text/
            │                   ├── ClickableSpanLayoutView.java
            │                   ├── ClickableSpanUtil.java
            │                   ├── FastTextLayoutView.java
            │                   ├── FastTextView.java
            │                   ├── ItalicReplacementSpan.java
            │                   ├── LineUtil.java
            │                   ├── NestReplacementSpan.java
            │                   ├── ReadMoreTextView.java
            │                   ├── SingleLineTextView.java
            │                   ├── StrokableTextView.java
            │                   ├── StrokeReplacementSpan.java
            │                   ├── StrokeSpan.java
            │                   ├── StrokeSpanUtil.java
            │                   ├── TextMeasureUtil.java
            │                   └── TextViewAttrsHelper.java
            └── res/
                └── values/
                    └── attrs.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# FastTextView

FastTextView is faster than Android TextView.
FastTextView use StaticLayout to render Spanned String,
so it support most features of Android TextView.

Inspired by :

https://engineering.instagram.com/improving-comment-rendering-on-android-a77d5db3d82e

http://ragnraok.github.io/textview-pre-render-research.html

## Features
1.Faster than Android TextView

2.More flexibility

3.Support Stroke Text
(More accurate method to measure stroke text and italic text)

4.Correct Ellipsis handle with ImageSpan

![](ellipsis.png)

5.Custom Read More Support

![](readmore.gif)


## FastTextView vs Android TextView
Rendering a SpannableString of 389 chars with ClickableSpan and ImageSpan.
Call 'onMeasure'、'onDraw' 1000 times.
Here are the test results on MI MAX (Android 6.0.1):

```
D/FastTextLayoutView: FastTextLayoutView onMeasure cost:0
D/FastTextView: FastTextView onMeasure cost:1
D/TestTextView: TestTextView measure cost:104
D/FastTextLayoutView: FastTextLayoutView onDraw cost:271
D/FastTextView: FastTextView onDraw cost:250
D/TestTextView: TestTextView onDraw cost:249
```
You can see FastTextView's 'onMeasure' almost no time consuming.

## Basic Usage
```
repositories {
    ...
    jcenter()
    ...
}

dependencies {
    ...
    compile 'com.lsjwzh.widget:FastTextView:1.2.15'
    ...
}
```
java code
```
    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);
    fastTextView.setText(spannableString);
```

## Advance Usage
### Use Layout directly
java code
```
    TextPaint textPaint = new TextPaint();
    textPaint.setAntiAlias(true);
    textPaint.setColor(Color.WHITE);
    float textSize = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics());
    textPaint.setTextSize(textSize);
    FastTextLayoutView fastTextLayoutView = (FastTextLayoutView) mRootView.findViewById(R.id.fast_tv);
    int width = Layout.getDesiredWidth(spannableStringBuilder, textPaint);

    StaticLayout layout = new StaticLayout(spannableStringBuilder, textPaint,
            Math.min(width, getResources().getDisplayMetrics().widthPixels), Layout.Alignment.ALIGN_NORMAL,
            1.0f, 0.0f, true);
    fastTextLayoutView.setTextLayout(layout);
```

### Single Line Stroke Text
java code
```
    ... ...
    StrokeSpan strokeSpan = new StrokeSpan(Color.BLUE, Color.YELLOW, 20);
    spannableStringBuilder.setSpan(strokeSpan, 0, spannableStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);
    fastTextView.setText(spannableStringBuilder);
```

### Read More
```

    ReadMoreTextView readMoreTextView = (ReadMoreTextView) mRootView.findViewById(R.id.readmore_tv);
    readMoreTextView.setText(spannableStringBuilder);
    readMoreTextView.setCustomEllipsisSpan(new ReadMoreTextView.EllipsisSpan("  Read More"));
    readMoreTextView.setCustomCollapseSpan(new ReadMoreTextView.EllipsisSpan("  Collapse"));
```

# Build Project
https://github.com/anggrayudi/android-hidden-api

You should use 'android-hidden-api' to build project.

1.Go to <SDK location>/platforms/.

2.Copy, paste and replace the downloaded hidden API file into this directory, e.g. android-25/android.jar.

3.Change compileSdkVersion and targetSdkVersion to 25 (for example).

4.Finally, rebuild your project.

# TODO List
<del>1.Layout Cache (For List Scene)</del>

<del>2.ColorState background</del>

<del>3.ReadMore</del>

<del>4.Ellipsis</del>

5.AutoSize

<del>6.ColorState textColor</del>


# License
```
Copyright 2017 lsjwzh

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

================================================
FILE: app/.gitignore
================================================
/build


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"
    defaultConfig {
        applicationId "com.wechat.testdemo"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:28.0.0'
    implementation project(':widget.FastTextView')
//    compile project(':text.Textline')
//    compile 'com.lsjwzh.widget:FastTextView:0.9.3'

}

task cleanAndroidMock(type: Delete) {
    description = 'Deletes the mockable Android jar'

    delete fileTree("${project.buildDir}/generated") {
        include 'mockable-android*.jar'
    }
}

project.afterEvaluate {
    tasks['createMockableJar'].dependsOn cleanAndroidMock
}

================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/wenye/android_sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.wechat.testdemo">

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:hardwareAccelerated="false"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>

================================================
FILE: app/src/main/java/com/lsjwzh/test/AutoScrollHandler.java
================================================
package com.lsjwzh.test;

import android.os.Handler;
import android.os.Looper;
import android.widget.AbsListView;
import android.widget.ListView;

/**
 * Created by ragnarok on 15/7/22.
 */
public class AutoScrollHandler {

  private ListView listView;

  private int itemCount;

  private Handler uiHandler = new Handler(Looper.getMainLooper());

  public AutoScrollHandler(ListView listView, int itemCount) {
    this.listView = listView;
    this.itemCount = itemCount;
  }

  public void startAutoScrollDown(final Callback callback) {
    FpsCalculator.instance().startCalculate();
    final int position = listView.getAdapter().getCount() - 1;
    startPositionAndTrack(position, callback);
  }

  public void startAutoScrollUp(final Callback callback) {
    FpsCalculator.instance().startCalculate();
    startPositionAndTrack(0, callback);
  }

  private void startPositionAndTrack(final int position, final Callback callback) {
    FpsCalculator.instance().startCalculate();
    listView.smoothScrollToPosition(position);
    listView.setOnScrollListener(new AbsListView.OnScrollListener() {
      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int
          totalItemCount) {

      }

      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == SCROLL_STATE_IDLE
            && (position == 0 ? listView.getFirstVisiblePosition() == 0 :
            listView.getLastVisiblePosition() == position)) {
          listView.setOnScrollListener(null);
          final int fps = FpsCalculator.instance().stopGetAvgFPS();
          callback.callback(fps);
        }
      }
    });
  }

  public interface Callback {
    void callback(int fps);
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/Const.java
================================================
package com.lsjwzh.test;

/**
 * Created by wenye on 2017/11/6.
 */

public class Const {
  public final static int LOOP_COUNT = 1;
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/FastTextLayoutView.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;

/**
 * A custom view for rendering layout directly.
 */
public class FastTextLayoutView extends com.lsjwzh.widget.text.ClickableSpanLayoutView {
  private static final String TAG = FastTextLayoutView.class.getSimpleName();
  private boolean mIsDebug = false;
  public static final TestStats TEST_STATS = new TestStats();

  public FastTextLayoutView(Context context) {
    super(context);
  }

  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
  }

  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }

  @Override
  protected void onDraw(Canvas canvas) {
    TEST_STATS.drawStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      // TODO for test
      super.onDraw(canvas);
    }
    TEST_STATS.drawEnd();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onDraw cost:" + TEST_STATS.getDrawCost());
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    TEST_STATS.measuretart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      // TODO for test
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    TEST_STATS.measureEnd();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onMeasure cost:" + TEST_STATS.getMeasureCost());
    }
  }

  @Override
  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {
    TEST_STATS.layoutStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      // TODO for test
      super.layout(l, t, r, b);
    }
    TEST_STATS.layoutEnd();
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/FastTextView.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;

/**
 * Simple and Fast TextView.
 */
public class FastTextView extends com.lsjwzh.widget.text.FastTextView {
  private static final String TAG = FastTextView.class.getSimpleName();
  private boolean mIsDebug = false;
  public static final TestStats TEST_STATS = new TestStats();

  public FastTextView(Context context) {
    super(context);
  }

  public FastTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
  }

  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }

  @Override
  protected void onDraw(Canvas canvas) {
    TEST_STATS.drawStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onDraw(canvas);
    }
    TEST_STATS.drawEnd();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onDraw cost:" + TEST_STATS.getDrawCost());
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    TEST_STATS.measuretart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    TEST_STATS.measureEnd();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onMeasure cost:" + TEST_STATS.getMeasureCost());
    }
  }

  @Override
  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {
    TEST_STATS.layoutStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      // TODO for test
      super.layout(l, t, r, b);
    }
    TEST_STATS.layoutEnd();
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/FpsCalculator.java
================================================
package com.lsjwzh.test;

import android.annotation.TargetApi;
import android.os.Build;
import android.util.Log;
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;

public class FpsCalculator {

	private final static String TAG = "FpsCalculator";
	
    private long mFrameIntervalNanos;
    
    private boolean mRunning = false;
    
    private static FpsCalculator instance;

	private AtomicInteger atom = new AtomicInteger(0);
	private Thread syncCheckThread = null;
    
    static {
    	instance = new FpsCalculator();
    }
    
    public static FpsCalculator instance() {
    	return instance;
    }
	
	private int totalFps;
	private int fpsCalculateCount;
	private boolean isCalculatingFPS;
	
	// calculate the average fps
	
	public void startCalculate() {
		totalFps = 0;
		fpsCalculateCount = 0;
		isCalculatingFPS = true;
	}
	
	public int stopGetAvgFPS() {
		isCalculatingFPS = false;
		int avgFPS = totalFps / fpsCalculateCount;
		totalFps = 0;
		fpsCalculateCount = 0;
		return avgFPS;
	}

	private void syncCheckThread(){
		if(!mRunning){
			return;
		}
		int val = atom.getAndSet(0);
		if (isCalculatingFPS) {
			totalFps += val;
			fpsCalculateCount++;
		}
		android.util.Log.i(TAG, "FPS: " + val);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	    
	private FrameCallback frameCallback = new FrameCallback() {

		@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
		@Override
		public void doFrame(long frameTimeNanos) {
			
			if (!mRunning) {
				return;
			}
            
            Choreographer.getInstance().postFrameCallback(frameCallback);

			atom.incrementAndGet();
		}};
	
	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
	public void start() {
		Log.d(TAG, "start vsync detect");
		if (mRunning) {
			return;
		}
		
		mRunning = true;

		syncCheckThread = new Thread(new Runnable() {
			@Override
			public void run() {
				for (;;) {
					if (!mRunning) {
						break;
					}
					syncCheckThread();
				}
			}
		});
		syncCheckThread.start();
		
		Choreographer chor = Choreographer.getInstance();
		Field field;
		try {
			field = chor.getClass().getDeclaredField("mFrameIntervalNanos");
			field.setAccessible(true);
			mFrameIntervalNanos = field.getLong(chor);
			Log.d(TAG, "mFrameIntervalNanos " + mFrameIntervalNanos);
		} catch (Exception e) {
			Log.e(TAG, "error: " + e.getMessage());
		}
		chor.postFrameCallback(frameCallback);

	}
	
	public void stop() {
		mRunning = false;
		if (syncCheckThread != null) {
			try {
				syncCheckThread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/GhostThread.java
================================================
package com.lsjwzh.test;


import android.util.Log;

import java.util.Random;

/**
 * Created by ragnarok on 15/7/22.
 * The GhostThread is used to simulate the heavily jobs behind the UI thread in the 
 * real world apps
 * Currently, the implementation is work by launch several threads and calculate floating point
 * multiplication infinity
 */
public class GhostThread  {
    private static final String TAG = "GhostThread";
    
    private static boolean isStart = false;
    
    private static Thread[] threads = new Thread[0];

    private static Random random = new Random();
    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            for (; ;) {
                if (!isStart) {
                    break;
                }
                double c = Math.PI * Math.PI * Math.PI * random.nextFloat();
//                Log.e("test", "v:" + c);
            }
        }
    };

    public static void start() {
        if (isStart) {
            return;
        }
        isStart = true;
        for (int i = 0; i < threads.length; i++) {
            Thread thread = new Thread(runnable);
            thread.setPriority(Thread.NORM_PRIORITY);
            thread.start();
            threads[i] = thread;
        }
    }
    
    public static void stop() {
        isStart = false;
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/StaticLayoutManager.java
================================================
package com.lsjwzh.test;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.TextLayoutWarmer;
import android.text.TextPaint;

/**
 * Created by ragnarok on 15/7/21.
 */
public class StaticLayoutManager {
  public final static TextLayoutWarmer<StaticLayout> sLayoutWarmer = new TextLayoutWarmer<>();

  private StaticLayout[] layout = new StaticLayout[Util.TEST_LIST_ITEM_COUNT];

  private StaticLayout longStringLayout;

  private TextPaint textPaint;
  private TextDirectionHeuristic textDir;
  private Layout.Alignment alignment;

  private Canvas dummyCanvas;

  private int hardCodeWidth;

  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
  public void initLayout(Context context, CharSequence source, CharSequence longString) {

    textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    textPaint.density = context.getResources().getDisplayMetrics().density;
    textPaint.setTextSize(Util.fromDPtoPix(context, Util.TEXT_SIZE_DP));

    textDir = TextDirectionHeuristics.LTR;

    alignment = Layout.Alignment.ALIGN_NORMAL;

    hardCodeWidth = Util.getScreenWidth(context);

    longStringLayout = new StaticLayout(longString, textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);

    dummyCanvas = new Canvas();

    longStringLayout.draw(dummyCanvas);

    for (int i = 0; i < layout.length; i++) {
      layout[i] = new StaticLayout(TestSpan.getSpanString(i), textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);
      layout[i].draw(dummyCanvas);
    }
  }

  public StaticLayout getLayout(int index) {
    return layout[index] != null ? layout[index]
        : sLayoutWarmer.getLayout(TestSpan.getSpanString(index));
  }

  public StaticLayout getLongStringLayout() {
    return longStringLayout;
  }

  private static StaticLayoutManager INSTANCE = null;

  public static StaticLayoutManager getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new StaticLayoutManager();
    }
    return INSTANCE;
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/TestSingleLineTextView.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;

import com.lsjwzh.widget.text.SingleLineTextView;

/**
 * Created by wenye on 2017/11/5.
 */

public class TestSingleLineTextView extends SingleLineTextView {
  private static final String TAG = "TestSingleLineTextView";
  private boolean mIsDebug = true;

  public TestSingleLineTextView(Context context) {
    super(context);
  }

  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
  }

  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }


  @Override
  protected void onDraw(Canvas canvas) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onDraw(canvas);
    }
    long end = System.currentTimeMillis();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onDraw cost:" + (end - start));
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    long end = System.currentTimeMillis();
    if (mIsDebug) {
      Log.d(TAG, TAG + " onMeasure cost:" + (end - start));
    }
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/TestSpan.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.BitmapFactory;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ImageSpan;

import com.wechat.testdemo.R;

import java.util.Random;

/**
 * Created by ragnarok on 15/7/21.
 */
public class TestSpan {

  private static String testString = "(\uD83E\uDDC0|\uD83E\uDD84|\uD83E\uDD83|\uD83E\uDD82|\uD83E" +
      "\uDD81" +
          "|\uD83E\uDD80|\uD83E\uDD18\uD83C\uDFFF|\uD83E\uDD18\uD83C\uDFFE|\uD83E\uDD18\uD83C" +
          "\uDFFD|\uD83E\uDD18\uD83C\uDFFC|\uD83E\uDD18\uD83C\uDFFB|\uD83E\uDD18|\uD83E\uDD17" +
          "|\uD83E\uDD16|\uD83E\uDD15|\uD83E\uDD14|\uD83E\uDD13|\uD83E\uDD12|\uD83E\uDD11|\uD83E" +
          "\uDD10|\uD83DuDEA4|\uD83D\uDEA3\uD83C\uDFFF|\uD83D\uDEA3\uD83C\uDFFE|\uD83D\uDEA3" +
          "\uD83C\uDFFD|\uD83D\uDEA3\uD83C\uDFFC|\uD83D\uDEA3\uD83C\uDFFB|\uD83D\uDEA3\u200D" +
          "\u2640\uFE0F|\uD83D" +
          "\uD83D\uDC87\uD83C\uDFFC|\uD83D\uDC87\uD83C\uDFFB|\uD83D\uDC87\u200D\u2642" +
          "\uFE0F|\uD83D\uDC87|\uD83D\uDC86\uD83C\uDFFF|\uD83D\uDC86\uD83C\uDFFE" +
          "|\uD83D\uDC86\uD83C\uDFFD|\uD83D\uDC86\uD83C\uDFFC|\uD83D\uDC86\uD83C" +
          "\uDFFB|\uD83D\uDC86\u200D\u2642\uFE0F|\uD83D\uDC86|\uD83D\uDC85\uD83C" +
          "\uDFFF|\uD83D\uDC85\uD83C\uDFFE|\uD83D\uDC85\uD83C\uDFFD|\uD83D\uDC85" +
          "\uD83C\uDFFC|\uD83D\uDC85\uD83C\uDFFB|\uD83D\uDC85|\uD83D\uDC84|\uD83D" +
          "\uDC83\uD83C\uDFFF|\uD83D\uDC83\uD83C\uDFFE|\uD83D\uDC83\uD83C\uDFFD" +
          "|\uD83D\uDC83\uD83C\uDFFC|\uD83D\uDC83\uD83C\uDFFB|\uD83D\uDC83|\uD83D" +
          "\uDC82\uD83C\uDFFF|\uD83D\uDC82\uD83C\uDFFE|\uD83D\uDC82\uD83C\uDFFD" +
          "|\uD83D\uDC82\uD83C\uDFFC|\uD83D\uDC82\uD83C\uDFFB|\uD83D\uDC82\u200D" +
          "\u2640\uFE0F|\uD83D\uDC82|\uD83D\uDC81\uD83C\uDFFF|\uD83D\uDC81\uD83C" +
          "\uDFFE|\uD83D\uDC81\uD83C\uDFFD|\uD83D\uDC81\uD83C\uDFFC|\uD83D\uDC81" +
          "\uD83C\uDFFB|\uD83D\uDC81\u200D\u2642\uFE0F|\uD83D\uDC81|\uD83D\uDC80" +
          "|\uD83D\uDC7F|\uD83D\uDC7E|\uD83D\uDC7D|\uD83D\uDC7C\uD83C\uDFFF|\uD83D" +
          "\uDC7C\uD83C\uDFFE|\uD83D\uDC7C\uD83C\uDFFD|\uD83D\uDC7C\uD83C\uDFFC" +
          "|\uD83D\uDC7C\uD83C\uDFFB|\uD83D\uDC7C|\uD83D\uDC7B|\uD83D\uDC7A|\uD83D" +
          "\uDC79|\uD83D\uDC78\uD83C\uDFFF|\uD83D\uDC78\uD83C\uDFFE|\uD83D\uDC78" +
          "\uD83C\uDFFD|\uD83D\uDC78\uD83C\uDFFC|\uD83D\uDC78\uD83C\uDFFB|\uD83D" +
          "\uDC78|\uD83D\uDC77\uD83C\uDFFF|\uD83D\uDC77\uD83C\uDFFE|\uD83D\uDC77" +
          "\uD83C\uDFFD|\uD83D\uDC77\uD83C\uDFFC|\uD83D\uDC77\uD83C\uDFFB|\uD83D" +
          "\uDC77\u200D\u2640\uFE0F|\uD83D\uDC77|\uD83D\uDC76\uD83C\uDFFF|\uD83D" +
          "\uDC76\uD83C\uDFFE|\uD83D\uDC76\uD83C\uDFFD|\uD83D\uDC76\uD83C\uDFFC" +
          "|\uD83D\uDC76\uD83C\uDFFB|\uD83D\uDC76|\uD83D\uDC75\uD83C\uDFFF|\uD83D" +
          "\uDC75\uD83C\uDFFE|\uD83D\uDC75\uD83C\uDFFD|\uD83D\uDC75\uD83C\uDFFC" +
          "|\uD83D\uDC75\uD83C\uDFFB|\uD83D\uDC75|\uD83D\uDC74\uD83C\uDFFF|\uD83D" +
          "\uDC74\uD83C\uDFFE|\uD83D\uDC74\uD83C\uDFFD|\uD83D\uDC74\uD83C\uDFFC" +
          "|\uD83D\uDC74\uD83C\uDFFB|\uD83D\uDC74|\uD83D\uDC73\uD83C\uDFFF|\uD83D" +
          "\uDC73\uD83C\uDFFE|\uD83D\uDC73\uD83C\uDFFD|\uD83D\uDC73\uD83C\uDFFC" +
          "|\uD83D\uDC73\uD83C\uDFFB|\uD83D\uDC73\u200D\u2640\uFE0F|\uD83D\uDC73" +
          "|\uD83D\uDC72\uD83C\uDFFF|\uD83D\uDC72\uD83C\uDFFE|\uD83D\uDC72\uD83C" +
          "\uDFFD|\uD83D\uDC72\uD83C\uDFFC|\uD83D\uDC72\uD83C\uDFFB|\uD83D\uDC72" +
          "|\uD83D\uDC71\uD83C\uDFFF|\uD83D\uDC71\uD83C\uDFFE|\uD83D\uDC71\uD83C" +
          "\uDFFD|\uD83D\uDC71\uD83C\uDFFC|\uD83D\uDC71\uD83C\uDFFB|\uD83D\uDC71" +
          "\u200D\u2640\uFE0F|\uD83D\uDC71|\uD83D\uDC70\uD83C\uDFFF|\uD83D\uDC70" +
          "\uD83C\uDFFE|\uD83D\uDC70\uD83C\uDFFD|\uD83D\uDC70\uD83C\uDFFC|\uD83D" +
          "\uDC70\uD83C\uDFFB|\uD83D\uDC70|\uD83D\uDC6F\u200D\u2642\uFE0F|\uD83D" +
          "\uDC6F|\uD83D\uDC6E\uD83C\uDFFF|\uD83D\uDC6E\uD83C\uDFFE|\uD83D\uDC6E" +
          "\uD83C\uDFFD|\uD83D\uDC6E\uD83C\uDFFC|\uD83D\uDC6E\uD83C\uDFFB|\uD83D" +
          "\uDC6E\u200D\u2640\uFE0F|\uD83D\uDC6E|\uD83D\uDC6D|\uD83D\uDC6C|\uD83D" +
          "\uDC6B|\uD83D\uDC6A|\uD83D\uDC69\uD83C\uDFFF|\uD83D\uDC69\uD83C\uDFFE" +
          "|\uD83D\uDC69\uD83C\uDFFD|\uD83D\uDC69\uD83C\uDFFC|\uD83D\uDC69\uD83C" +
          "\uDFFB|\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67" +
          "|\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66|\uD83D" +
          "\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC69" +
          "\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D" +
          "\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67|\uD83D\uDC69" +
          "\u200D\uD83D\uDC67\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D" +
          "\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC66" +
          "|\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC69|\uD83D" +
          "\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68|\uD83D\uDC69" +
          "\u200D\u2764\uFE0F\u200D\uD83D\uDC69|\uD83D\uDC69\u200D\u2764\uFE0F\u200D" +
          "\uD83D\uDC68|\uD83D\uDC69|\uD83D\uDC68\uD83C\uDFFF|\uD83D\uDC68\uD83C" +
          "\uDFFE|\uD83D\uDC68\uD83C\uDFFD|\uD83D\uDC68\uD83C\uDFFC|\uD83D\uDC68" +
          "\uD83C\uDFFB|\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D" +
          "\uDC67|\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66" +
          "|\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC68\u200D\uD83D" +
          "\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC68\u200D\uD83D\uDC69" +
          "\u200D\uD83D\uDC66|\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D" +
          "\uD83D\uDC67|\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D" +
          "\uDC66|\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67|\uD83D\uDC68\u200D" +
          "\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC68\u200D\uD83D" +
          "\uDC68\u200D\uD83D\uDC66|\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67" +
          "|\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66|\uD83D\uDC68\u200D\uD83D" +
          "\uDC67|\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC68\u200D" +
          "\uD83D\uDC66|\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D" +
          "\uDC68|\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC68|\uD83D\uDC68" +
          "|\uD83D\uDC67\uD83C\uDFFF|\uD83D\uDC67\uD83C\uDFFE|\uD83D\uDC67\uD83C" +
          "\uDFFD|\uD83D\uDC67\uD83C\uDFFC|\uD83D\uDC67\uD83C\uDFFB|\uD83D\uDC67" +
          "|\uD83D\uDC66\uD83C\uDFFF|\uD83D\uDC66\uD83C\uDFFE|\uD83D\uDC66\uD83C" +
          "\uDFFD|\uD83D\uDC66\uD83C\uDFFC|\uD83D\uDC66\uD83C\uDFFB|\uD83D\uDC66" +
          "|\uD83D\uDC65|\uD83D\uDC64|\uD83D\uDC63|\uD83D\uDC62|\uD83D\uDC61|\uD83D" +
          "\uDC60|\uD83D\uDC5F|\uD83D\uDC5E|\uD83D\uDC5D|\uD83D\uDC5C|\uD83D\uDC5B" +
          "|\uD83D\uDC5A|\uD83D\uDC59|\uD83D\uDC58|\uD83D\uDC57|\uD83D\uDC56|\uD83D" +
          "\uDC55|\uD83D\uDC54|\uD83D\uDC53|\uD83D\uDC52|\uD83D\uDC51|\uD83D\uDC50" +
          "\uD83C\uDFFF|\uD83D\uDC50\uD83C\uDFFE|\uD83D\uDC50\uD83C\uDFFD|\uD83D" +
          "\uDC50\uD83C\uDFFC|\uD83D\uDC50\uD83C\uDFFB|\uD83D\uDC50|\uD83D\uDC4F" +
          "\uD83C\uDFFF|\uD83D\uDC4F\uD83C\uDFFE|\uD83D\uDC4F\uD83C\uDFFD|\uD83D" +
          "\uDC4F\uD83C\uDFFC|\uD83D\uDC4F\uD83C\uDFFB|\uD83D\uDC4F|\uD83D\uDC4E" +
          "\uD83C\uDFFF|\uD83D\uDC4E\uD83C\uDFFE|\uD83D\uDC4E\uD83C\uDFFD|\uD83D" +
          "\uDC4E\uD83C\uDFFC|\uD83D\uDC4E\uD83C\uDFFB|\uD83D\uDC4E|\uD83D\uDC4D" +
          "\uD83C\uDFFF|\uD83D\uDC4D\uD83C\uDFFE|\uD83D\uDC4D\uD83C\uDFFD|\uD83D" +
          "\uDC4D\uD83C\uDFFC|\uD83D\uDC4D\uD83C\uDFFB|\uD83D\uDC4D|\uD83D\uDC4C" +
          "\uD83C\uDFFF|\uD83D\uDC4C\uD83C\uDFFE|\uD83D\uDC4C\uD83C\uDFFD|\uD83D" +
          "\uDC4C\uD83C\uDFFC|\uD83D\uDC4C\uD83C\uDFFB|\uD83D\uDC4C|\uD83D\uDC4B" +
          "\uD83C\uDFFF|\uD83D\uDC4B\uD83C\uDFFE|\uD83D\uDC4B\uD83C\uDFFD|\uD83D" +
          "\uDC4B\uD83C\uDFFC|\uD83D\uDC4B\uD83C\uDFFB|\uD83D\uDC4B|\uD83D\uDC4A" +
          "\uD83C\uDFFF|\uD83D\uDC4A\uD83C\uDFFE|\uD83D\uDC4A\uD83C\uDFFD|\uD83D" +
          "\uDC4A\uD83C\uDFFC|\uD83D\uDC4A\uD83C\uDFFB|\uD83D\uDC4A|\uD83D\uDC49" +
          "\uD83C\uDFFF|\uD83D\uDC49\uD83C\uDFFE|\uD83D\uDC49\uD83C\uDFFD|\uD83D" +
          "\uDC49\uD83C\uDFFC|\uD83D\uDC49\uD83C\uDFFB|\uD83D\uDC49|\uD83D\uDC48" +
          "\uD83C\uDFFF|\uD83D\uDC48\uD83C\uDFFE|\uD83D\uDC48\uD83C\uDFFD|\uD83D" +
          "\uDC48\uD83C\uDFFC|\uD83D\uDC48\uD83C\uDFFB|\uD83D\uDC48|\uD83D\uDC47" +
          "\uD83C\uDFFF|\uD83D\uDC47\uD83C\uDFFE|\uD83D\uDC47\uD83C\uDFFD|\uD83D" +
          "\uDC47\uD83C\uDFFC|\uD83D\uDC47\uD83C\uDFFB|\uD83D\uDC47|\uD83D\uDC46" +
          "\uD83C\uDFFF|\uD83D\uDC46\uD83C\uDFFE|\uD83D\uDC46\uD83C\uDFFD|\uD83D" +
          "\uDC46\uD83C\uDFFC|\uD83D\uDC46\uD83C\uDFFB|\uD83D\uDC46|\uD83D\uDC45" +
          "|\uD83D\uDC44|\uD83D\uDC43\uD83C\uDFFF|\uD83D\uDC43\uD83C\uDFFE|\uD83D" +
          "\uDC43\uD83C\uDFFD|\uD83D\uDC43\uD83C\uDFFC|\uD83D\uDC43\uD83C\uDFFB" +
          "|\uD83D\uDC43|\uD83D\uDC42\uD83C\uDFFF|\uD83D\uDC42\uD83C\uDFFE|\uD83D" +
          "\uDC42\uD83C\uDFFD|\uD83D\uDC42\uD83C\uDFFC|\uD83D\uDC42\uD83C\uDFFB" +
          "|\uD83D\uDC42|\uD83D\uDC41\u200D\uD83D\uDDE8|\uD83D\uDC41|\uD83D\uDC40" +
          "|\uD83D\uDC3F|\uD83D\uDC3E|\uD83D\uDC3D|\uD83D\uDC3C|\uD83D\uDC3B|\uD83D" +
          "\uDC3A|\uD83D\uDC39|\uD83D\uDC38|\uD83D\uDC37|\uD83D\uDC36|\uD83D\uDC35" +
          "|\uD83D\uDC34|\uD83D\uDC33|\uD83D\uDC32|\uD83D\uDC31|\uD83D\uDC30|\uD83D" +
          "\uDC2F|\uD83D\uDC2E|\uD83D\uDC2D|\uD83D\uDC2C|\uD83D\uDC2B|\uD83D\uDC2A" +
          "|\uD83D\uDC29|\uD83D\uDC28|\uD83D\uDC27|\uD83D\uDC26|\uD83D\uDC25|\uD83D" +
          "\uDC24|\uD83D\uDC23|\uD83D\uDC22|\uD83D\uDC21|\uD83D\uDC20|\uD83D\uDC1F" +
          "|\uD83D\uDC1E|\uD83D\uDC1D|\uD83D\uDC1C|\uD83D\uDC1B|\uD83D\uDC1A|\uD83D" +
          "\uDC19|\uD83D\uDC18|\uD83D\uDC17|\uD83D\uDC16|\uD83D\uDC15|\uD83D\uDC14" +
          "|\uD83D\uDC13|\uD83D\uDC12|\uD83D\uDC11|\uD83D\uDC10|\uD83D\uDC0F|\uD83D" +
          "\uDC0E|\uD83D\uDC0D|\uD83D\uDC0C|\uD83D\uDC0B|\uD83D\uDC0A|\uD83D\uDC09" +
          "|\uD83D\uDC08|\uD83D\uDC07|\uD83D\uDC06|\uD83D\uDC05|\uD83D\uDC04|\uD83D" +
          "\uDC03|\uD83D\uDC02|\uD83D\uDC01|\uD83D\uDC00|\uD83C\uDFFF|\uD83C\uDFFE" +
          "|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB|\uD83C\uDFFA|\uD83C\uDFF9|\uD83C" +
          "\uDFF8|\uD83C\uDFF7|\uD83C\uDFF5|\uD83C\uDFF4|\uD83C\uDFF3\uFE0F\u200D" +
          "\uD83C\uDF08|\uD83C\uDFF3|\uD83C\uDFF0|\uD83C\uDFEF|\uD83C\uDFEE|\uD83C" +
          "\uDFED|\uD83C\uDFEC|\uD83C\uDFEB|\uD83C\uDFEA|\uD83C\uDFE9|\uD83C\uDFE8" +
          "|\uD83C\uDFE7|\uD83C\uDFE6|\uD83C\uDFE5|\uD83C\uDFE4|\uD83C\uDFE3|\uD83C" +
          "\uDFE2|\uD83C\uDFE1|\uD83C\uDFE0|\uD83C\uDFDF|\uD83C\uDFDE|\uD83C\uDFDD" +
          "|\uD83C\uDFDC|\uD83C\uDFDB|\uD83C\uDFDA|\uD83C\uDFD9|\uD83C\uDFD8|\uD83C" +
          "\uDFD7|\uD83C\uDFD6|\uD83C\uDFD5|\uD83C\uDFD4|\uD83C\uDFD3|\uD83C\uDFD2" +
          "|\uD83C\uDFD1|\uD83C\uDFD0|\uD83C\uDFCF|\uD83C\uDFCE|\uD83C\uDFCD|\uD83C" +
          "\uDFCC\uFE0F\u200D\u2640\uFE0F|\uD83C\uDFCC|\uD83C\uDFCB\uFE0F\u200D\u2640" +
          "\uFE0F|\uD83C\uDFCB\uD83C\uDFFF|\uD83C\uDFCB\uD83C\uDFFE|\uD83C\uDFCB" +
          "\uD83C\uDFFD|\uD83C\uDFCB\uD83C\uDFFC|\uD83C\uDFCB\uD83C\uDFFB|\uD83C" +
          "\uDFCB|\uD83C\uDFCA\uD83C\uDFFF|\uD83C\uDFCA\uD83C\uDFFE|\uD83C\uDFCA" +
          "\uD83C\uDFFD|\uD83C\uDFCA\uD83C\uDFFC|\uD83C\uDFCA\uD83C\uDFFB|\uD83C" +
          "\uDFCA\u200D\u2640\uFE0F|\uD83C\uDFCA|\uD83C\uDFC9|\uD83C\uDFC8|\uD83C" +
          "\uDFC7|\uD83C\uDFC6|\uD83C\uDFC5|\uD83C\uDFC4\uD83C\uDFFF|\uD83C\uDFC4" +
          "\uD83C\uDFFE|\uD83C\uDFC4\uD83C\uDFFD|\uD83C\uDFC4\uD83C\uDFFC|\uD83C" +
          "\uDFC4\uD83C\uDFFB|\uD83C\uDFC4\u200D\u2640\uFE0F|\uD83C\uDFC4|\uD83C" +
          "\uDFC3\uD83C\uDFFF|\uD83C\uDFC3\uD83C\uDFFE|\uD83C\uDFC3\uD83C\uDFFD" +
          "|\uD83C\uDFC3\uD83C\uDFFC|\uD83C\uDFC3\uD83C\uDFFB|\uD83C\uDFC3\u200D" +
          "\u2640\uFE0F|\uD83C\uDFC3|\uD83C\uDFC2|\uD83C\uDFC1|\uD83C\uDFC0|\uD83C" +
          "\uDFBF|\uD83C\uDFBE|\uD83C\uDFBD|\uD83C\uDFBC|\uD83C\uDFBB|\uD83C\uDFBA" +
          "|\uD83C\uDFB9|\uD83C\uDFB8|\uD83C\uDFB7|\uD83C\uDFB6|\uD83C\uDFB5|\uD83C" +
          "\uDFB4|\uD83C\uDFB3|\uD83C\uDFB2|\uD83C\uDFB1|\uD83C\uDFB0|\uD83C\uDFAF" +
          "|\uD83C\uDFAE|\uD83C\uDFAD|\uD83C\uDFAC|\uD83C\uDFAB|\uD83C\uDFAA|\uD83C" +
          "\uDFA9|\uD83C\uDFA8|\uD83C\uDFA7|\uD83C\uDFA6|\uD83C\uDFA5|\uD83C\uDFA4" +
          "|\uD83C\uDFA3|\uD83C\uDFA2|\uD83C\uDFA1|\uD83C\uDFA0|\uD83C\uDF9F|\uD83C" +
          "\uDF9E|\uD83C\uDF9B|\uD83C\uDF9A|\uD83C\uDF99|\uD83C\uDF97|\uD83C\uDF96" +
          "|\uD83C\uDF93|\uD83C\uDF92|\uD83C\uDF91|\uD83C\uDF90|\uD83C\uDF8F|\uD83C" +
          "\uDF8E|\uD83C\uDF8D|\uD83C\uDF8C|\uD83C\uDF8B|\uD83C\uDF8A|\uD83C\uDF89" +
          "|\uD83C\uDF88|\uD83C\uDF87|\uD83C\uDF86|\uD83C\uDF85\uD83C\uDFFF|\uD83C" +
          "\uDF85\uD83C\uDFFE|\uD83C\uDF85\uD83C\uDFFD|\uD83C\uDF85\uD83C\uDFFC" +
          "|\uD83C\uDF85\uD83C\uDFFB|\uD83C\uDF85|\uD83C\uDF84|\uD83C\uDF83|\uD83C" +
          "\uDF82|\uD83C\uDF81|\uD83C\uDF80|\uD83C\uDF7F|\uD83C\uDF7E|\uD83C\uDF7D" +
          "|\uD83C\uDF7C|\uD83C\uDF7B|\uD83C\uDF7A|\uD83C\uDF79|\uD83C\uDF78|\uD83C" +
          "\uDF77|\uD83C\uDF76|\uD83C\uDF75|\uD83C\uDF74|\uD83C\uDF73|\uD83C\uDF72" +
          "|\uD83C\uDF71|\uD83C\uDF70|\uD83C\uDF6F|\uD83C\uDF6E|\uD83C\uDF6D|\uD83C" +
          "\uDF6C|\uD83C\uDF6B|\uD83C\uDF6A|\uD83C\uDF69|\uD83C\uDF68|\uD83C\uDF67" +
          "|\uD83C\uDF66|\uD83C\uDF65|\uD83C\uDF64|\uD83C\uDF63|\uD83C\uDF62|\uD83C" +
          "\uDF61|\uD83C\uDF60|\uD83C\uDF5F|\uD83C\uDF5E|\uD83C\uDF5D|\uD83C\uDF5C" +
          "|\uD83C\uDF5B|\uD83C\uDF5A|\uD83C\uDF59|\uD83C\uDF58|\uD83C\uDF57|\uD83C" +
          "\uDF56|\uD83C\uDF55|\uD83C\uDF54|\uD83C\uDF53|\uD83C\uDF52|\uD83C\uDF51" +
          "|\uD83C\uDF50|\uD83C\uDF4F|\uD83C\uDF4E|\uD83C\uDF4D|\uD83C\uDF4C|\uD83C" +
          "\uDF4B|\uD83C\uDF4A|\uD83C\uDF49|\uD83C\uDF48|\uD83C\uDF47|\uD83C\uDF46" +
          "|\uD83C\uDF45|\uD83C\uDF44|\uD83C\uDF43|\uD83C\uDF42|\uD83C\uDF41|\uD83C" +
          "\uDF40|\uD83C\uDF3F|\uD83C\uDF3E|\uD83C\uDF3D|\uD83C\uDF3C|\uD83C\uDF3B" +
          "|\uD83C\uDF3A|\uD83C\uDF39|\uD83C\uDF38|\uD83C\uDF37|\uD83C\uDF36|\uD83C" +
          "\uDF35|\uD83C\uDF34|\uD83C\uDF33|\uD83C\uDF32|\uD83C\uDF31|\uD83C\uDF30" +
          "|\uD83C\uDF2F|\uD83C\uDF2E|\uD83C\uDF2D|\uD83C\uDF2C|\uD83C\uDF2B|\uD83C" +
          "\uDF2A|\uD83C\uDF29|\uD83C\uDF28|\uD83C\uDF27|\uD83C\uDF26|\uD83C\uDF25" +
          "|\uD83C\uDF24|\uD83C\uDF21|\uD83C\uDF20|\uD83C\uDF1F|\uD83C\uDF1E|\uD83C" +
          "\uDF1D|\uD83C\uDF1C|\uD83C\uDF1B|\uD83C\uDF1A|\uD83C\uDF19|\uD83C\uDF18" +
          "|\uD83C\uDF17|\uD83C\uDF16|\uD83C\uDF15|\uD83C\uDF14|\uD83C\uDF13|\uD83C" +
          "\uDF12|\uD83C\uDF11|\uD83C\uDF10|\uD83C\uDF0F|\uD83C\uDF0E|\uD83C\uDF0D" +
          "|\uD83C\uDF0C|\uD83C\uDF0B|\uD83C\uDF0A|\uD83C\uDF09|\uD83C\uDF08|\uD83C" +
          "\uDF07|\uD83C\uDF06|\uD83C\uDF05|\uD83C\uDF04|\uD83C\uDF03|\uD83C\uDF02" +
          "|\uD83C\uDF01|\uD83C\uDF00|\uD83C\uDE51|\uD83C\uDE50|\uD83C\uDE3A|\uD83C" +
          "\uDE39|\uD83C\uDE38|\uD83C\uDE37|\uD83C\uDE36|\uD83C\uDE35|\uD83C\uDE34" +
          "|\uD83C\uDE33|\uD83C\uDE32|\uD83C\uDE2F|\uD83C\uDE1A|\uD83C\uDE02|\uD83C" +
          "\uDE01|\uD83C\uDDFF\uD83C\uDDFC|\uD83C\uDDFF\uD83C\uDDF2|\uD83C\uDDFF" +
          "\uD83C\uDDE6|\uD83C\uDDFE\uD83C\uDDF9|\uD83C\uDDFE\uD83C\uDDEA|\uD83C" +
          "\uDDFD\uD83C\uDDF0|\uD83C\uDDFC\uD83C\uDDF8|\uD83C\uDDFC\uD83C\uDDEB" +
          "|\uD83C\uDDFB\uD83C\uDDFA|\uD83C\uDDFB\uD83C\uDDF3|\uD83C\uDDFB\uD83C" +
          "\uDDEE|\uD83C\uDDFB\uD83C\uDDEC|\uD83C\uDDFB\uD83C\uDDEA|\uD83C\uDDFB" +
          "\uD83C\uDDE8|\uD83C\uDDFB\uD83C\uDDE6|\uD83C\uDDFA\uD83C\uDDFF|\uD83C" +
          "\uDDFA\uD83C\uDDFE|\uD83C\uDDFA\uD83C\uDDF8|\uD83C\uDDFA\uD83C\uDDF2" +
          "|\uD83C\uDDFA\uD83C\uDDEC|\uD83C\uDDFA\uD83C\uDDE6|\uD83C\uDDF9\uD83C" +
          "\uDDFF|\uD83C\uDDF9\uD83C\uDDFC|\uD83C\uDDF9\uD83C\uDDFB|\uD83C\uDDF9" +
          "\uD83C\uDDF9|\uD83C\uDDF9\uD83C\uDDF7|\uD83C\uDDF9\uD83C\uDDF4|\uD83C" +
          "\uDDF9\uD83C\uDDF3|\uD83C\uDDF9\uD83C\uDDF2|\uD83C\uDDF9\uD83C\uDDF1" +
          "|\uD83C\uDDF9\uD83C\uDDF0|\uD83C\uDDF9\uD83C\uDDEF|\uD83C\uDDF9\uD83C" +
          "\uDDED|\uD83C\uDDF9\uD83C\uDDEC|\uD83C\uDDF9\uD83C\uDDEB|\uD83C\uDDF9" +
          "\uD83C\uDDE9|\uD83C\uDDF9\uD83C\uDDE8|\uD83C\uDDF9\uD83C\uDDE6|\uD83C" +
          "\uDDF8\uD83C\uDDFF|\uD83C\uDDF8\uD83C\uDDFE|\uD83C\uDDF8\uD83C\uDDFD" +
          "|\uD83C\uDDF8\uD83C\uDDFB|\uD83C\uDDF8\uD83C\uDDF9|\uD83C\uDDF8\uD83C" +
          "\uDDF8|\uD83C\uDDF8\uD83C\uDDF7|\uD83C\uDDF8\uD83C\uDDF4|\uD83C\uDDF8" +
          "\uD83C\uDDF3|\uD83C\uDDF8\uD83C\uDDF2|\uD83C\uDDF8\uD83C\uDDF1|\uD83C" +
          "\uDDF8\uD83C\uDDF0|\uD83C\uDDF8\uD83C\uDDEF|\uD83C\uDDF8\uD83C\uDDEE" +
          "|\uD83C\uDDF8\uD83C\uDDED|\uD83C\uDDF8\uD83C\uDDEC|\uD83C\uDDF8\uD83C" +
          "\uDDEA|\uD83C\uDDF8\uD83C\uDDE9|\uD83C\uDDF8\uD83C\uDDE8|\uD83C\uDDF8" +
          "\uD83C\uDDE7|\uD83C\uDDF8\uD83C\uDDE6|\uD83C\uDDF7\uD83C\uDDFC|\uD83C" +
          "\uDDF7\uD83C\uDDFA|\uD83C\uDDF7\uD83C\uDDF8|\uD83C\uDDF7\uD83C\uDDF4" +
          "|\uD83C\uDDF7\uD83C\uDDEA|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF5\uD83C" +
          "\uDDFE|\uD83C\uDDF5\uD83C\uDDFC|\uD83C\uDDF5\uD83C\uDDF9|\uD83C\uDDF5" +
          "\uD83C\uDDF8|\uD83C\uDDF5\uD83C\uDDF7|\uD83C\uDDF5\uD83C\uDDF3|\uD83C" +
          "\uDDF5\uD83C\uDDF2|\uD83C\uDDF5\uD83C\uDDF1|\uD83C\uDDF5\uD83C\uDDF0" +
          "|\uD83C\uDDF5\uD83C\uDDED|\uD83C\uDDF5\uD83C\uDDEC|\uD83C\uDDF5\uD83C" +
          "\uDDEB|\uD83C\uDDF5\uD83C\uDDEA|\uD83C\uDDF5\uD83C\uDDE6|\uD83C\uDDF4" +
          "\uD83C\uDDF2|\uD83C\uDDF3\uD83C\uDDFF|\uD83C\uDDF3\uD83C\uDDFA|\uD83C" +
          "\uDDF3\uD83C\uDDF7|\uD83C\uDDF3\uD83C\uDDF5|\uD83C\uDDF3\uD83C\uDDF4" +
          "|\uD83C\uDDF3\uD83C\uDDF1|\uD83C\uDDF3\uD83C\uDDEE|\uD83C\uDDF3\uD83C" +
          "\uDDEC|\uD83C\uDDF3\uD83C\uDDEB|\uD83C\uDDF3\uD83C\uDDEA|\uD83C\uDDF3" +
          "\uD83C\uDDE8|\uD83C\uDDF3\uD83C\uDDE6|\uD83C\uDDF2\uD83C\uDDFF|\uD83C" +
          "\uDDF2\uD83C\uDDFE|\uD83C\uDDF2\uD83C\uDDFD|\uD83C\uDDF2\uD83C\uDDFC" +
          "|\uD83C\uDDF2\uD83C\uDDFB|\uD83C\uDDF2\uD83C\uDDFA|\uD83C\uDDF2\uD83C" +
          "\uDDF9|\uD83C\uDDF2\uD83C\uDDF8|\uD83C\uDDF2\uD83C\uDDF7|\uD83C\uDDF2" +
          "\uD83C\uDDF6|\uD83C\uDDF2\uD83C\uDDF5|\uD83C\uDDF2\uD83C\uDDF4|\uD83C" +
          "\uDDF2\uD83C\uDDF3|\uD83C\uDDF2\uD83C\uDDF2|\uD83C\uDDF2\uD83C\uDDF1" +
          "|\uD83C\uDDF2\uD83C\uDDF0|\uD83C\uDDF2\uD83C\uDDED|\uD83C\uDDF2\uD83C" +
          "\uDDEC|\uD83C\uDDF2\uD83C\uDDEB|\uD83C\uDDF2\uD83C\uDDEA|\uD83C\uDDF2" +
          "\uD83C\uDDE9|\uD83C\uDDF2\uD83C\uDDE8|\uD83C\uDDF2\uD83C\uDDE6|\uD83C" +
          "\uDDF1\uD83C\uDDFE|\uD83C\uDDF1\uD83C\uDDFB|\uD83C\uDDF1\uD83C\uDDFA" +
          "|\uD83C\uDDF1\uD83C\uDDF9|\uD83C\uDDF1\uD83C\uDDF8|\uD83C\uDDF1\uD83C" +
          "\uDDF7|\uD83C\uDDF1\uD83C\uDDF0|\uD83C\uDDF1\uD83C\uDDEE|\uD83C\uDDF1" +
          "\uD83C\uDDE8|\uD83C\uDDF1\uD83C\uDDE7|\uD83C\uDDF1\uD83C\uDDE6|\uD83C" +
          "\uDDF0\uD83C\uDDFF|\uD83C\uDDF0\uD83C\uDDFE|\uD83C\uDDF0\uD83C\uDDFC" +
          "|\uD83C\uDDF0\uD83C\uDDF7|\uD83C\uDDF0\uD83C\uDDF5|\uD83C\uDDF0\uD83C" +
          "\uDDF3|\uD83C\uDDF0\uD83C\uDDF2|\uD83C\uDDF0\uD83C\uDDEE|\uD83C\uDDF0" +
          "\uD83C\uDDED|\uD83C\uDDF0\uD83C\uDDEC|\uD83C\uDDF0\uD83C\uDDEA|\uD83C" +
          "\uDDEF\uD83C\uDDF5|\uD83C\uDDEF\uD83C\uDDF4|\uD83C\uDDEF\uD83C\uDDF2" +
          "|\uD83C\uDDEF\uD83C\uDDEA|\uD83C\uDDEE\uD83C\uDDF9|\uD83C\uDDEE\uD83C" +
          "\uDDF8|\uD83C\uDDEE\uD83C\uDDF7|\uD83C\uDDEE\uD83C\uDDF6|\uD83C\uDDEE" +
          "\uD83C\uDDF4|\uD83C\uDDEE\uD83C\uDDF3|\uD83C\uDDEE\uD83C\uDDF2|\uD83C" +
          "\uDDEE\uD83C\uDDF1|\uD83C\uDDEE\uD83C\uDDEA|\uD83C\uDDEE\uD83C\uDDE9" +
          "|\uD83C\uDDEE\uD83C\uDDE8|\uD83C\uDDED\uD83C\uDDFA|\uD83C\uDDED\uD83C" +
          "\uDDF9|\uD83C\uDDED\uD83C\uDDF7|\uD83C\uDDED\uD83C\uDDF3|\uD83C\uDDED" +
          "\uD83C\uDDF2|\uD83C\uDDED\uD83C\uDDF0|\uD83C\uDDEC\uD83C\uDDFE|\uD83C" +
          "\uDDEC\uD83C\uDDFC|\uD83C\uDDEC\uD83C\uDDFA|\uD83C\uDDEC\uD83C\uDDF9" +
          "|\uD83C\uDDEC\uD83C\uDDF8|\uD83C\uDDEC\uD83C\uDDF7|\uD83C\uDDEC\uD83C" +
          "\uDDF6|\uD83C\uDDEC\uD83C\uDDF5|\uD83C\uDDEC\uD83C\uDDF3|\uD83C\uDDEC" +
          "\uD83C\uDDF2|\uD83C\uDDEC\uD83C\uDDF1|\uD83C\uDDEC\uD83C\uDDEE|\uD83C" +
          "\uDDEC\uD83C\uDDED|\uD83C\uDDEC\uD83C\uDDEC|\uD83C\uDDEC\uD83C\uDDEB" +
          "|\uD83C\uDDEC\uD83C\uDDEA|\uD83C\uDDEC\uD83C\uDDE9|\uD83C\uDDEC\uD83C" +
          "\uDDE7|\uD83C\uDDEC\uD83C\uDDE6|\uD83C\uDDEB\uD83C\uDDF7|\uD83C\uDDEB" +
          "\uD83C\uDDF4|\uD83C\uDDEB\uD83C\uDDF2|\uD83C\uDDEB\uD83C\uDDF0|\uD83C" +
          "\uDDEB\uD83C\uDDEF|\uD83C\uDDEB\uD83C\uDDEE|\uD83C\uDDEA\uD83C\uDDFA" +
          "|\uD83C\uDDEA\uD83C\uDDF9|\uD83C\uDDEA\uD83C\uDDF8|\uD83C\uDDEA\uD83C" +
          "\uDDF7|\uD83C\uDDEA\uD83C\uDDED|\uD83C\uDDEA\uD83C\uDDEC|\uD83C\uDDEA" +
          "\uD83C\uDDEA|\uD83C\uDDEA\uD83C\uDDE8|\uD83C\uDDEA\uD83C\uDDE6|\uD83C" +
          "\uDDE9\uD83C\uDDFF|\uD83C\uDDE9\uD83C\uDDF4|\uD83C\uDDE9\uD83C\uDDF2" +
          "|\uD83C\uDDE9\uD83C\uDDF0|\uD83C\uDDE9\uD83C\uDDEF|\uD83C\uDDE9\uD83C" +
          "\uDDEC|\uD83C\uDDE9\uD83C\uDDEA|\uD83C\uDDE8\uD83C\uDDFF|\uD83C\uDDE8" +
          "\uD83C\uDDFE|\uD83C\uDDE8\uD83C\uDDFD|\uD83C\uDDE8\uD83C\uDDFC|\uD83C" +
          "\uDDE8\uD83C\uDDFB|\uD83C\uDDE8\uD83C\uDDFA|\uD83C\uDDE8\uD83C\uDDF7" +
          "|\uD83C\uDDE8\uD83C\uDDF5|\uD83C\uDDE8\uD83C\uDDF4|\uD83C\uDDE8\uD83C" +
          "\uDDF3|\uD83C\uDDE8\uD83C\uDDF2|\uD83C\uDDE8\uD83C\uDDF1|\uD83C\uDDE8" +
          "\uD83C\uDDF0|\uD83C\uDDE8\uD83C\uDDEE|\uD83C\uDDE8\uD83C\uDDED|\uD83C" +
          "\uDDE8\uD83C\uDDEC|\uD83C\uDDE8\uD83C\uDDEB|\uD83C\uDDE8\uD83C\uDDE9" +
          "|\uD83C\uDDE8\uD83C\uDDE8|\uD83C\uDDE8\uD83C\uDDE6|\uD83C\uDDE7\uD83C" +
          "\uDDFF|\uD83C\uDDE7\uD83C\uDDFE|\uD83C\uDDE7\uD83C\uDDFC|\uD83C\uDDE7" +
          "\uD83C\uDDFB|\uD83C\uDDE7\uD83C\uDDF9|\uD83C\uDDE7\uD83C\uDDF8|\uD83C" +
          "\uDDE7\uD83C\uDDF7|\uD83C\uDDE7\uD83C\uDDF6|\uD83C\uDDE7\uD83C\uDDF4" +
          "|\uD83C\uDDE7\uD83C\uDDF3|\uD83C\uDDE7\uD83C\uDDF2|\uD83C\uDDE7\uD83C" +
          "\uDDF1|\uD83C\uDDE7\uD83C\uDDEF|\uD83C\uDDE7\uD83C\uDDEE|\uD83C\uDDE7" +
          "\uD83C\uDDED|\uD83C\uDDE7\uD83C\uDDEC|\uD83C\uDDE7\uD83C\uDDEB|\uD83C" +
          "\uDDE7\uD83C\uDDEA|\uD83C\uDDE7\uD83C\uDDE9|\uD83C\uDDE7\uD83C\uDDE7" +
          "|\uD83C\uDDE7\uD83C\uDDE6|\uD83C\uDDE6\uD83C\uDDFF|\uD83C\uDDE6\uD83C" +
          "\uDDFD|\uD83C\uDDE6\uD83C\uDDFC|\uD83C\uDDE6\uD83C\uDDFA|\uD83C\uDDE6" +
          "\uD83C\uDDF9|\uD83C\uDDE6\uD83C\uDDF8|\uD83C\uDDE6\uD83C\uDDF7|\uD83C" +
          "\uDDE6\uD83C\uDDF6|\uD83C\uDDE6\uD83C\uDDF4|\uD83C\uDDE6\uD83C\uDDF2" +
          "|\uD83C\uDDE6\uD83C\uDDF1|\uD83C\uDDE6\uD83C\uDDEE|\uD83C\uDDE6\uD83C" +
          "\uDDEC|\uD83C\uDDE6\uD83C\uDDEB|\uD83C\uDDE6\uD83C\uDDEA|\uD83C\uDDE6" +
          "\uD83C\uDDE9|\uD83C\uDDE6\uD83C\uDDE8|\uD83C\uDD9A|\uD83C\uDD99|\uD83C" +
          "\uDD98|\uD83C\uDD97|\uD83C\uDD96|\uD83C\uDD95|\uD83C\uDD94|\uD83C\uDD93" +
          "|\uD83C\uDD92|\uD83C\uDD91|\uD83C\uDD8E|\uD83C\uDD7F|\uD83C\uDD7E|\uD83C" +
          "\uDD71|\uD83C\uDD70|\uD83C\uDCCF|\uD83C\uDC04|\u3299|\u3297|\u303D|\u3030" +
          "|\u2B55|\u2B50|\u2B1C|\u2B1B|\u2B07|\u2B06|\u2B05|\u2935|\u2934|\u27BF" +
          "|\u27B0|\u27A1|\u2797|\u2796|\u2795|\u2764|\u2763|\u2757|\u2755|\u2754" +
          "|\u2753|\u274E|\u274C|\u2747|\u2744|\u2734|\u2733|\u2728|\u2721|\u271D" +
          "|\u2716|\u2714|\u2712|\u270F|\u270D\uD83C\uDFFF|\u270D\uD83C\uDFFE|\u270D" +
          "\uD83C\uDFFD|\u270D\uD83C\uDFFC|\u270D\uD83C\uDFFB|\u270D|\u270C\uD83C" +
          "\uDFFF|\u270C\uD83C\uDFFE|\u270C\uD83C\uDFFD|\u270C\uD83C\uDFFC|\u270C" +
          "\uD83C\uDFFB|\u270C|\u270B\uD83C\uDFFF|\u270B\uD83C\uDFFE|\u270B\uD83C" +
          "\uDFFD|\u270B\uD83C\uDFFC|\u270B\uD83C\uDFFB|\u270B|\u270A\uD83C\uDFFF" +
          "|\u270A\uD83C\uDFFE|\u270A\uD83C\uDFFD|\u270A\uD83C\uDFFC|\u270A\uD83C" +
          "\uDFFB|\u270A|\u2709|\u2708|\u2705|\u2702|\u26FD|\u26FA|\u26F9\uFE0F\u200D" +
          "\u2640\uFE0F|\u26F9\uD83C\uDFFF|\u26F9\uD83C\uDFFE|\u26F9\uD83C\uDFFD" +
          "|\u26F9\uD83C\uDFFC|\u26F9\uD83C\uDFFB|\u26F9|\u26F8|\u26F7|\u26F5|\u26F4" +
          "|\u26F3|\u26F2|\u26F1|\u26F0|\u26EA|\u26E9|\u26D4|\u26D3|\u26D1|\u26CF" +
          "|\u26CE|\u26C8|\u26C5|\u26C4|\u26BE|\u26BD|\u26B1|\u26B0|\u26AB|\u26AA" +
          "|\u26A1|\u26A0|\u269C|\u269B|\u2699|\u2697|\u2696|\u2694|\u2693|\u2692" +
          "|\u267F|\u267B|\u2668|\u2666|\u2665|\u2663|\u2660|\u2653|\u2652|\u2651" +
          "|\u2650|\u264F|\u264E|\u264D|\u264C|\u264B|\u264A|\u2649|\u2648|\u263A" +
          "|\u2639|\u2638|\u262F|\u262E|\u262A|\u2626|\u2623|\u2622|\u2620|\u261D" +
          "\uD83C\uDFFF|\u261D\uD83C\uDFFE|\u261D\uD83C\uDFFD|\u261D\uD83C\uDFFC" +
          "|\u261D\uD83C\uDFFB|\u261D|\u2618|\u2615|\u2614|\u2611|\u260E|\u2604|\u2603" +
          "|\u2602|\u2601|\u2600|\u25FE|\u25FD|\u25FC|\u25FB|\u25C0|\u25B6|\u25AB" +
          "|\u25AA|\u24C2|\u23FA|\u23F9|\u23F8|\u23F3|\u23F2|\u23F1|\u23F0|\u23EF" +
          "|\u23EE|\u23ED|\u23EC|\u23EB|\u23EA|\u23E9|\u23CF|\u2328|\u231B|\u231A" +
          "|\u21AA|\u21A9|\u2199|\u2198|\u2197|\u2196|\u2195|\u2194|\u2139|\u2122" +
          "|\u2049|\u203C|\u00AE|\u00A9|\u0039\uFE0F\u20E3|\u0038\uFE0F\u20E3|\u0037" +
          "\uFE0F\u20E3|\u0036\uFE0F\u20E3|\u0035\uFE0F\u20E3|\u0034\uFE0F\u20E3" +
          "|\u0033\uFE0F\u20E3|\u0032\uFE0F\u20E3|\u0031\uFE0F\u20E3|\u0030\uFE0F" +
          "\u20E3|\u002A\uFE0F\u20E3|\u0023\uFE0F\u20E3)";

  private static SpannableString testLongString =
      new SpannableString
          ("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" +
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAA");

  private static SpannableStringBuilder[] allTestStrings = new SpannableStringBuilder[Util.TEST_LIST_ITEM_COUNT];

  public static void init(Context context) {

    Random random = new Random();

    for (int i = 0; i < allTestStrings.length; i++) {
      int start = i;
      int len = 50 + random.nextInt(100);
      len = len > 1 ? len : 1;
      int end = start + len > testString.length() ? testString.length() : start + len;
      allTestStrings[i] =  new SpannableStringBuilder(i + ":" + testString.subSequence(start, end));

      ImageSpan imgSpan = new ImageSpan(context,
          BitmapFactory.decodeResource(context.getResources(), R.drawable.test));
      int spanStart = random.nextInt(len - 1);
      allTestStrings[i].setSpan(imgSpan, spanStart, spanStart + 1,
          Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    }

    for (int i = 0; i < testLongString.length(); i++) {
      if (i + 1 < testLongString.length()) {
        ImageSpan imgSpan = new ImageSpan(context, BitmapFactory.decodeResource(context
            .getResources(), R.drawable.test));
        testLongString.setSpan(imgSpan, i, i + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
      }
    }
  }

  public static SpannableStringBuilder getSpanString(int index) {
    return allTestStrings[index];
  }

  public static SpannableString getLongSpanString() {
    return testLongString;
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/TestStats.java
================================================
package com.lsjwzh.test;

import android.os.SystemClock;

/**
 * Created by wenye on 2017/12/15.
 */
public class TestStats {
  private volatile long drawCost = 0;
  private volatile long drawCount = 0;
  private volatile long measureCost = 0;
  private volatile long measureCount = 0;
  private volatile long layoutCost = 0;
  private volatile long layoutCount = 0;

  private long drawStart = 0;
  private long measureStart = 0;
  private long layoutStart = 0;

  @Override
  public String toString() {
    return "TestStats{" +
        "drawCost=" + drawCost +
        ", drawCount=" + drawCount +
        ", measureCost=" + measureCost +
        ", measureCount=" + measureCount +
        ", layoutCost=" + layoutCost +
        ", layoutCount=" + layoutCount +
        '}';
  }

  public void reset() {
    drawCost = 0;
    drawCount = 0;
    measureCost = 0;
    measureCount = 0;
    layoutCost = 0;
    layoutCount = 0;
  }

  public long getDrawCost() {
    return drawCost;
  }

  public long getDrawCount() {
    return drawCount;
  }

  public long getMeasureCost() {
    return measureCost;
  }

  public long getMeasureCount() {
    return measureCount;
  }

  public long getLayoutCost() {
    return layoutCost;
  }

  public long getLayoutCount() {
    return layoutCount;
  }

  public void drawStart() {
    drawStart = SystemClock.elapsedRealtime();
  }

  public void drawEnd() {
    drawCost += SystemClock.elapsedRealtime() - drawStart;
    drawCount++;
  }

  public void measuretart() {
    measureStart = SystemClock.elapsedRealtime();
  }

  public void measureEnd() {
    measureCost += SystemClock.elapsedRealtime() - measureStart;
    measureCount++;
  }

  public void layoutStart() {
    layoutStart = SystemClock.elapsedRealtime();
  }

  public void layoutEnd() {
    layoutCost += SystemClock.elapsedRealtime() - layoutStart;
    layoutCount++;
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/TestTextView.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.Canvas;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * Created by wenye on 2017/10/12.
 */

public class TestTextView extends TextView {
  private static final String TAG = TestTextView.class.getSimpleName();
  public static final TestStats TEST_STATS = new TestStats();

  public TestTextView(Context context) {
    super(context);
  }

  public TestTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
  }

  public TestTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    TEST_STATS.measuretart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    TEST_STATS.measureEnd();
  }

  @Override
  protected void onDraw(Canvas canvas) {
    TEST_STATS.drawStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      super.onDraw(canvas);
    }
    TEST_STATS.drawEnd();
//    Log.d(TAG, TAG + " onDraw cost:" + (end - start));
  }

  @Override
  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {
    TEST_STATS.layoutStart();
    for (int i = 0; i < Const.LOOP_COUNT; i++) {
      // TODO for test
      super.layout(l, t, r, b);
    }
    TEST_STATS.layoutEnd();
  }
}


================================================
FILE: app/src/main/java/com/lsjwzh/test/TextLineView.java
================================================
//package com.lsjwzh.test;
//
//import android.content.Context;
//import android.graphics.Canvas;
//import android.graphics.Color;
//import android.graphics.Paint;
//import android.os.Build;
//import android.os.SystemClock;
//import android.support.annotation.Nullable;
//import android.support.annotation.RequiresApi;
//import android.text.Directions;
//import android.text.Layout;
//import android.text.TextLineCompat;
//import android.text.TextPaint;
//import android.util.AttributeSet;
//import android.util.Log;
//import android.view.View;
//
///**
// * Just for test.
// * Created by wenye on 2017/11/5.
// */
//public class TextLineView extends View {
//  private static final String TAG = "TextLineView";
//  private CharSequence mText;
//  private TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
//  private final Paint.FontMetricsInt mFontMetric = new Paint.FontMetricsInt();
//  TextLineCompat mTextLineCompat;
//
//  public TextLineView(Context context) {
//    super(context);
//  }
//
//  public TextLineView(Context context, @Nullable AttributeSet attrs) {
//    super(context, attrs);
//  }
//
//  public TextLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
//    super(context, attrs, defStyleAttr);
//  }
//
//  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
//  public TextLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
//    super(context, attrs, defStyleAttr, defStyleRes);
//  }
//
//  public void setText(CharSequence text) {
//    mText = text;
//    mTextPaint.setTextSize(20);
//    mTextPaint.setColor(Color.RED);
//    mTextLineCompat = TextLineCompat.obtain();
//    mTextLineCompat.set(mTextPaint, mText, 0, mText.length(), Layout.DIR_LEFT_TO_RIGHT, Directions.DIRS_ALL_LEFT_TO_RIGHT, false, null);
//    mTextPaint.getFontMetricsInt(mFontMetric);
//  }
//
//  @Override
//  protected void onDraw(Canvas canvas) {
//    long start = SystemClock.elapsedRealtime();
//    for (int i = 0; i < Const.LOOP_COUNT; i++) {
//      super.onDraw(canvas);
//      int offset = getHeight() - (mFontMetric.bottom - mFontMetric.top);
//      int baseLine = offset / 2 - mFontMetric.top;
//      mTextLineCompat.draw(canvas, 0, 0, baseLine, baseLine + mFontMetric.bottom);
//    }
//    long end = SystemClock.elapsedRealtime();
//    Log.d(TAG, TAG + " onDraw cost:" + (end - start));
//  }
//
//  @Override
//  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//    long start = SystemClock.elapsedRealtime();
//    for (int i = 0; i < Const.LOOP_COUNT; i++) {
//      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//    }
//    long end = SystemClock.elapsedRealtime();
//    Log.d(TAG, TAG + " measure cost:" + (end - start));
//
//  }
//
//
//}


================================================
FILE: app/src/main/java/com/lsjwzh/test/Util.java
================================================
package com.lsjwzh.test;

import android.content.Context;
import android.graphics.Point;
import android.view.WindowManager;

/**
 * Created by ragnarok on 15/7/21.
 */
public class Util {
    
    public static final int TEST_LIST_ITEM_COUNT = 500;
    
    public static final int TEXT_SIZE_DP = 25;
    
    public static final int AUTO_SCROLL_INTERVAL = 1;
    
    public static final int AUTO_SCROLL_STEP = 10;
    
    public static float fromDPtoPix(Context context, int dp) {
        return context.getResources().getDisplayMetrics().density * dp;
    }
    
    public static int getScreenWidth(Context context) {
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        
        Point size = new Point();
        windowManager.getDefaultDisplay().getSize(size);
        
        return size.x;
    }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/EllipseFragment.java
================================================
package com.wechat.testdemo;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.StaticLayoutBuilderCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.lsjwzh.test.TestSingleLineTextView;
import com.lsjwzh.widget.text.FastTextLayoutView;
import com.lsjwzh.widget.text.FastTextView;
import com.lsjwzh.widget.text.ReadMoreTextView;
import com.lsjwzh.widget.text.StrokeSpan;

/**
 * A placeholder fragment containing a simple view.
 */
public class EllipseFragment extends Fragment {

  private View mRootView;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    if (mRootView != null) {
      return mRootView;
    }
    mRootView = inflater.inflate(R.layout.ellipse_demo, container, false);
    SpannableStringBuilder spannableStringBuilder = getSpannable();
    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);
    fastTextView.setText(spannableStringBuilder);
//    fastTextView.setCustomEllipsisSpan(new ImageSpan(drawable));

    TextView tv = (TextView) mRootView.findViewById(R.id.system_tv);
    tv.setText(spannableStringBuilder);
//    tv.setMovementMethod(LinkMovementMethod.getInstance());


    return mRootView;
  }

  private StaticLayout getStaticLayout(SpannableStringBuilder spannableStringBuilder, TextPaint textPaint, float textSize, Rect bounds, String text) {
    long start = SystemClock.elapsedRealtime();
    for (int i = 0; i < 1000; i++) {
      textPaint.getTextBounds(text, 0, spannableStringBuilder.length(), bounds);
    }
    long end = SystemClock.elapsedRealtime();
    float withWithTextBounds = bounds.width();
    Log.d("test", "withWithTextBounds:" + withWithTextBounds + " offset:" + 0.5f * textSize + " cost:" + (end - start));

    start = SystemClock.elapsedRealtime();
    float withWithMeasureText = 0;
    for (int i = 0; i < 1000; i++) {
      withWithMeasureText = textPaint.measureText(spannableStringBuilder, 0, spannableStringBuilder.length());
    }
    end = SystemClock.elapsedRealtime();
    Log.d("test", "withWithMeasureText:" + withWithMeasureText + " cost:" + (end - start));

    start = SystemClock.elapsedRealtime();
    float withWithDesiredWidth = 0;
    for (int i = 0; i < 1000; i++) {
      withWithDesiredWidth = Layout.getDesiredWidth(spannableStringBuilder, textPaint);
    }
    end = SystemClock.elapsedRealtime();
    Log.d("test", "withWithDesiredWidth:" + withWithDesiredWidth + " cost:" + (end - start));


    start = SystemClock.elapsedRealtime();
    for (int i = 0; i < 1000; i++) {
      spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), StrokeSpan.class);
    }
    end = SystemClock.elapsedRealtime();
    Log.d("test", "getSpans cost:" + (end - start));

    int width = (int) Math.ceil(Math.max(Math.max(withWithTextBounds, withWithMeasureText), withWithDesiredWidth));

    return StaticLayoutBuilderCompat.obtain(spannableStringBuilder, 0, spannableStringBuilder.length(),
        textPaint, Math.min(width, getResources().getDisplayMetrics().widthPixels))
        .setAlignment(Layout.Alignment.ALIGN_NORMAL)
        .setLineSpacing(0f, 1f)
        .setEllipsize(TextUtils.TruncateAt.END)
        .setMaxLines(2).setIncludePad(true).build();
  }

  @NonNull
  private SpannableStringBuilder getSpannable() {
    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.content_cn));
    Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
    drawable.setBounds(0, 0, 35, 35);

    spannableStringBuilder.setSpan(new ImageSpan(drawable)
        , 36, 37, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

    spannableStringBuilder.setSpan(new ImageSpan(drawable)
        , 37, 38, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

    spannableStringBuilder.setSpan(new ImageSpan(drawable)
        , 38, 39, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

    spannableStringBuilder.setSpan(new ImageSpan(drawable)
        , 39, 40, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
//    ItalicReplacementSpan italicSpan = new ItalicReplacementSpan(-0.25f);
//    StrokeSpan strokeSpan = new StrokeSpan(Color.BLUE, Color.YELLOW, 20);
//    spannableStringBuilder.setSpan(strokeSpan, 0, spannableStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    return spannableStringBuilder;
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/FastTextViewListTestFragment.java
================================================
package com.wechat.testdemo;

import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.lsjwzh.test.AutoScrollHandler;
import com.lsjwzh.test.FastTextView;
import com.lsjwzh.test.TestSpan;
import com.lsjwzh.test.TestTextView;
import com.lsjwzh.test.Util;

public class FastTextViewListTestFragment extends Fragment {

  private ListView listView;

  private ListAdapter adapter;

  private AutoScrollHandler autoScrollHandler;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
      Bundle savedInstanceState) {
    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);

    listView = (ListView) viewRoot.findViewById(R.id.test_list);

    adapter = new ListAdapter(getActivity());

    listView.setAdapter(adapter);

    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);

    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        FastTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestFastTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestFastTextViewStats", FastTextView.TEST_STATS.toString());
          }
        });
      }
    });

    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        FastTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestFastTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestFastTextViewStats", FastTextView.TEST_STATS.toString());
          }
        });
      }
    });
    return viewRoot;
  }

  private static class ListAdapter extends TestListAdapter {

    private ListAdapter(Context context) {
      super(context);
    }

    @Override
    public View bindView(int position, View convertView, ViewGroup parent) {

      if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.fast_list_item, parent,
            false);

        ViewHolder viewHolder = new ViewHolder();
        viewHolder.textView = (FastTextView) convertView.findViewById(R.id.fast_text_view);
        convertView.setTag(viewHolder);
      }

      ViewHolder holder = (ViewHolder) convertView.getTag();
      holder.textView.setText(TestSpan.getSpanString(position));
      return convertView;
    }

    private class ViewHolder {
      FastTextView textView;
    }
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/MainActivity.java
================================================
package com.wechat.testdemo;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

import com.lsjwzh.test.FpsCalculator;
import com.lsjwzh.test.GhostThread;

public class MainActivity extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
//    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
//    setSupportActionBar(toolbar);

    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        getSupportFragmentManager().popBackStackImmediate();
      }
    });
    getSupportFragmentManager().beginTransaction()
        .replace(R.id.fragment, new MainActivityFragment())
        .commit();

//    GhostThread.start();
    FpsCalculator.instance().start();
  }

  @Override
  public void onBackPressed() {
    super.onBackPressed();
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      return true;
    }

    return super.onOptionsItemSelected(item);
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    FpsCalculator.instance().stop();
    GhostThread.stop();
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/MainActivityFragment.java
================================================
package com.wechat.testdemo;

import android.graphics.Paint;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextLayoutCache;
import android.text.TextLayoutWarmer;
import android.text.TextPaint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import com.lsjwzh.test.StaticLayoutManager;
import com.lsjwzh.test.TestSpan;
import com.lsjwzh.test.Util;

/**
 * A placeholder fragment containing a simple view.
 */
public class MainActivityFragment extends Fragment {

  private View mRootView;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    if (mRootView != null) {
      return mRootView;
    }
    mRootView = inflater.inflate(R.layout.fragment_main, container, false);
    mRootView.findViewById(R.id.demo_measure_test).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new EllipseFragment())
            .addToBackStack(null)
            .commit();
      }
    });
    mRootView.findViewById(R.id.demo_ellipsis).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new EllipseFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });
    mRootView.findViewById(R.id.demo_read_more).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new ReadMoreFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });
    mRootView.findViewById(R.id.demo_read_more_list).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new ReadMoreListTestFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });

    mRootView.findViewById(R.id.prepare_layout_cache).setOnClickListener(new View.OnClickListener
        () {
      @Override
      public void onClick(View v) {
        StaticLayoutManager.sLayoutWarmer.setLayoutFactory(new TextLayoutWarmer
            .LayoutFactory<StaticLayout>() {

          @Override
          public StaticLayout makeLayout(CharSequence text) {
            // 公用TextPaint会导致fps下降? why
            final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
            textPaint.setTextSize(Util.fromDPtoPix(getActivity(), Util.TEXT_SIZE_DP));
            int width = (int) Layout.getDesiredWidth(text, textPaint);
            return new StaticLayout(text, textPaint,
                Math.min(width, Util.getScreenWidth(getActivity())),
                Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, true);
          }
        });
        StaticLayoutManager.sLayoutWarmer.addListener(new TextLayoutWarmer
            .WarmListener<StaticLayout>() {
          int count = 0;

          @Override
          public void onWarmComplete(CharSequence text, TextLayoutWarmer.WarmerTask<StaticLayout>
              warmerTask) {
            TextLayoutCache.STATIC_LAYOUT_CACHE.put(text, warmerTask.mLayout);
            count++;
            if (count == Util.TEST_LIST_ITEM_COUNT) {
              mRootView.post(new Runnable() {
                @Override
                public void run() {
                  Toast.makeText(getActivity(), "init layout and span finish", Toast.LENGTH_LONG)
                      .show();
                }
              });
            }
          }
        });

        for (int i = 0; i < Util.TEST_LIST_ITEM_COUNT; i++) {
          StaticLayoutManager.sLayoutWarmer.addText(TestSpan.getSpanString(i));
        }
//        new Thread(new Runnable() {
//          @Override
//          public void run() {
//            StaticLayoutManager.getInstance().initLayout(getActivity(), TestSpan.getSpanString
// (), TestSpan.getLongSpanString());
//            getActivity().runOnUiThread(new Runnable() {
//              @Override
//              public void run() {
//                Toast.makeText(getActivity(), "init layout and span finish", Toast.LENGTH_LONG)
// .show();
//              }
//            });
//          }
//        }).start();


      }
    });
    mRootView.findViewById(R.id.demo_layout_cache).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new StaticLayoutCacheTestFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });
    mRootView.findViewById(R.id.demo_fast_tv).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new FastTextViewListTestFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });
    mRootView.findViewById(R.id.demo_normal_list).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        getFragmentManager().beginTransaction()
            .replace(R.id.fragment, new NormalLayoutTestFragment())
            .addToBackStack(null)
            .commitAllowingStateLoss();
      }
    });
    TestSpan.init(getActivity());
    return mRootView;
  }


}


================================================
FILE: app/src/main/java/com/wechat/testdemo/NormalLayoutTestFragment.java
================================================
package com.wechat.testdemo;

import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.lsjwzh.test.AutoScrollHandler;
import com.lsjwzh.test.FastTextView;
import com.lsjwzh.test.TestSpan;
import com.lsjwzh.test.TestTextView;
import com.lsjwzh.test.Util;

public class NormalLayoutTestFragment extends Fragment {

  private ListView listView;

  private NormalListAdapter adapter;

  private AutoScrollHandler autoScrollHandler;


  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
      Bundle savedInstanceState) {
    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);

    listView = (ListView) viewRoot.findViewById(R.id.test_list);

    adapter = new NormalListAdapter(getActivity());

    listView.setAdapter(adapter);

    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);

    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        TestTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestTextViewStats", TestTextView.TEST_STATS.toString());
          }
        });
      }
    });

    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        TestTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestTextViewStats", TestTextView.TEST_STATS.toString());

          }
        });
      }
    });
    return viewRoot;
  }

  private static class NormalListAdapter extends TestListAdapter {

    private NormalListAdapter(Context context) {
      super(context);
    }

    @Override
    public View bindView(int position, View convertView, ViewGroup parent) {

      if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.normal_list_item, parent,
            false);

        ViewHolder viewHolder = new ViewHolder();
        viewHolder.textView = (TextView) convertView.findViewById(R.id.normal_text);

        convertView.setTag(viewHolder);
      }

      ViewHolder holder = (ViewHolder) convertView.getTag();
      holder.textView.setMovementMethod(LinkMovementMethod.getInstance());
      holder.textView.setText(TestSpan.getSpanString(position));
      return convertView;
    }

    private class ViewHolder {
      TextView textView;
    }
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/ReadMoreFragment.java
================================================
package com.wechat.testdemo;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import com.lsjwzh.widget.text.ReadMoreTextView;

/**
 * A placeholder fragment containing a simple view.
 */
public class ReadMoreFragment extends Fragment {

  private View mRootView;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    if (mRootView != null) {
      return mRootView;
    }
    mRootView = inflater.inflate(R.layout.read_more_demo, container, false);
    SpannableStringBuilder spannableStringBuilder = getSpannable();
    final ReadMoreTextView readMoreTextView = mRootView.findViewById(R.id.readmore_tv);
    readMoreTextView.setText(spannableStringBuilder);
    readMoreTextView.setCustomEllipsisSpan(new ReadMoreTextView.EllipsisSpan("  Read More") {
      @Override
      public void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
        int oldColor = paint.getColor();
        paint.setColor(Color.RED);
        super.draw(canvas, text, start, end, x, top, y, bottom, paint);
        paint.setColor(oldColor);
      }
    });
    readMoreTextView.setCustomCollapseSpan(new ReadMoreTextView.EllipsisSpan("  Collapse") {
      @Override
      public void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
        int oldColor = paint.getColor();
        paint.setColor(Color.RED);
        super.draw(canvas, text, start, end, x, top, y, bottom, paint);
        paint.setColor(oldColor);
      }
    });
    readMoreTextView.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View v) {
        readMoreTextView.setTextColor(Color.RED);
        return false;
      }
    });
    return mRootView;
  }

  @NonNull
  private SpannableStringBuilder getSpannable() {
    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.content_cn));
    return spannableStringBuilder;
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/ReadMoreListTestFragment.java
================================================
package com.wechat.testdemo;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;

import com.lsjwzh.test.AutoScrollHandler;
import com.lsjwzh.test.FastTextView;
import com.lsjwzh.test.TestSpan;
import com.lsjwzh.test.Util;
import com.lsjwzh.widget.text.ReadMoreTextView;

public class ReadMoreListTestFragment extends Fragment {

  private ListView listView;

  private ListAdapter adapter;

  private AutoScrollHandler autoScrollHandler;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
      Bundle savedInstanceState) {
    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);

    listView = (ListView) viewRoot.findViewById(R.id.test_list);

    adapter = new ListAdapter(getActivity());

    listView.setAdapter(adapter);

    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);

    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        FastTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestFastTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestFastTextViewStats", FastTextView.TEST_STATS.toString());
          }
        });
      }
    });

    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        FastTextView.TEST_STATS.reset();
        adapter.bindCost = 0;
        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int fps) {
            Toast.makeText(listView.getContext(), "Average FPS: " + fps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "TestFastTextView.avgFps" + fps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("TestFastTextViewStats", FastTextView.TEST_STATS.toString());
          }
        });
      }
    });
    return viewRoot;
  }

  private static class ListAdapter extends TestListAdapter {

    private ListAdapter(Context context) {
      super(context);
    }

    @Override
    public View bindView(int position, View convertView, ViewGroup parent) {

      if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.readmore_list_item, parent,
            false);

        ViewHolder viewHolder = new ViewHolder();
        viewHolder.textView = (ReadMoreTextView) convertView.findViewById(R.id.fast_text_view);

        convertView.setTag(viewHolder);
      }

      ViewHolder holder = (ViewHolder) convertView.getTag();
      holder.textView.setText(TestSpan.getSpanString(position));
      return convertView;
    }

    private class ViewHolder {
      ReadMoreTextView textView;
    }
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/StaticLayoutCacheTestFragment.java
================================================
package com.wechat.testdemo;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;

import com.lsjwzh.test.AutoScrollHandler;
import com.lsjwzh.test.StaticLayoutManager;
import com.lsjwzh.test.Util;
import com.lsjwzh.widget.text.FastTextLayoutView;

public class StaticLayoutCacheTestFragment extends Fragment {

  private static final String TAG = "StaticLayoutUI";

  private ListView listview;

  private StaticListAdapter adapter;

  private AutoScrollHandler autoScrollHandler;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable
      Bundle savedInstanceState) {
    View viewRoot = inflater.inflate(R.layout.static_layout_ui, container, false);
    listview = (ListView) viewRoot.findViewById(R.id.test_list);

    adapter = new StaticListAdapter(getActivity());

    listview.setAdapter(adapter);

    autoScrollHandler = new AutoScrollHandler(listview, Util.TEST_LIST_ITEM_COUNT);

    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        com.lsjwzh.test.FastTextLayoutView.TEST_STATS.reset();
        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int avgFps) {
            Toast.makeText(getActivity(), "Average FPS: " + avgFps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "FastTextLayoutView.avgFps" + avgFps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("FastTextLayoutViewStats", com.lsjwzh.test.FastTextLayoutView.TEST_STATS
                .toString());
          }
        });
      }
    });

    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        com.lsjwzh.test.FastTextLayoutView.TEST_STATS.reset();
        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {
          @Override
          public void callback(int avgFps) {
            Toast.makeText(getActivity(), "Average FPS: " + avgFps, Toast.LENGTH_LONG).show();
            Log.e("drawFps", "FastTextLayoutView.avgFps" + avgFps);
            Log.e("bindCost", "bindCost" + adapter.bindCost);
            Log.e("FastTextLayoutViewStats", com.lsjwzh.test.FastTextLayoutView.TEST_STATS
                .toString());
          }
        });
      }
    });

    return viewRoot;
  }

  private static class StaticListAdapter extends TestListAdapter {

    private StaticListAdapter(Context context) {
      super(context);
    }

    @Override
    public View bindView(int position, View convertView, ViewGroup parent) {
      if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.static_list_item, parent,
            false);

        ViewHolder viewHolder = new ViewHolder();
        viewHolder.staticLayoutView = (FastTextLayoutView) convertView.findViewById(R.id
            .static_layout_view);

        convertView.setTag(viewHolder);
      }

      ViewHolder holder = (ViewHolder) convertView.getTag();
      holder.staticLayoutView.setTextLayout(StaticLayoutManager.getInstance().getLayout(position));
      holder.staticLayoutView.requestLayout();
      return convertView;
    }

    private class ViewHolder {
      FastTextLayoutView staticLayoutView;
    }
  }
}


================================================
FILE: app/src/main/java/com/wechat/testdemo/TestListAdapter.java
================================================
package com.wechat.testdemo;

import android.content.Context;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.lsjwzh.test.Util;

public abstract class TestListAdapter extends BaseAdapter {

  protected Context context;
  public long bindCost = 0;

  public TestListAdapter(Context context) {
    this.context = context;
  }

  @Override
  public int getCount() {
    return Util.TEST_LIST_ITEM_COUNT;
  }

  @Override
  public Object getItem(int position) {
    return position;
  }

  @Override
  public long getItemId(int position) {
    return position;
  }

  @Override
  public final View getView(int position, View convertView, ViewGroup parent) {
    long start = SystemClock.elapsedRealtime();
    convertView = bindView(position, convertView, parent);
    long end = SystemClock.elapsedRealtime();
    bindCost += (end - start);
    return convertView;
  }


  public abstract View bindView(int position, View convertView, ViewGroup parent);

}

================================================
FILE: app/src/main/res/anim/popup_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:shareInterpolator="false" >
    <alpha android:fromAlpha="1.0" android:toAlpha="1.0"
           android:interpolator="@android:anim/decelerate_interpolator"
           android:duration="100" />
</set>

================================================
FILE: app/src/main/res/anim/popup_exit.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:shareInterpolator="false" >
    <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
           android:interpolator="@android:anim/decelerate_interpolator"
           android:duration="@integer/abc_config_activityShortDur" />
</set>

================================================
FILE: app/src/main/res/drawable/popup_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" android:innerRadius="10dp">
  <solid android:color="#33ffffff"/>
  <corners android:radius="10dp"/>
</shape>

================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context="com.wechat.testdemo.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"/>

  </android.support.design.widget.AppBarLayout>

  <FrameLayout
    android:id="@+id/fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

  </FrameLayout>

  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    app:srcCompat="@drawable/ic_ab_back_holo_dark_am"/>

</android.support.design.widget.CoordinatorLayout>


================================================
FILE: app/src/main/res/layout/ellipse_demo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fillViewport="true"
  tools:context="com.wechat.testdemo.MainActivityFragment"
  tools:showIn="@layout/activity_main">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Android TextView:"/>

    <com.lsjwzh.test.TestTextView
      android:id="@+id/system_tv"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#6600ffff"
      android:ellipsize="end"
      android:maxLines="2"
      android:textSize="20sp"/>


    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="FastTextView:"/>

    <com.lsjwzh.test.FastTextView
      android:id="@+id/fast_tv2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#6600ffff"
      android:clickable="true"
      android:ellipsize="end"
      android:maxLines="2"
      android:text="FastTextView"
      android:textSize="20sp"/>

    <View
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>

</ScrollView>


================================================
FILE: app/src/main/res/layout/fast_layout_ui.xml
================================================
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

  <LinearLayout
    android:id="@+id/op_layout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:gravity="center">

    <Button
      android:id="@+id/scroll_down_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollDown"
      />

    <Button
      android:id="@+id/scroll_up_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollUp"/>
  </LinearLayout>

  <ListView
    android:id="@+id/test_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/op_layout"/>

</RelativeLayout>


================================================
FILE: app/src/main/res/layout/fast_list_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.test.FastTextView
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	android:id="@+id/fast_text_view"
	app:enableLayoutCache="true"
	android:textSize="25sp"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"/>

================================================
FILE: app/src/main/res/layout/fragment_main.xml
================================================
<?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:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context="com.wechat.testdemo.MainActivityFragment"
  tools:showIn="@layout/activity_main">

  <TextView
    android:id="@+id/demo_measure_test"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Measure Test Demo"/>

  <TextView
    android:id="@+id/demo_ellipsis"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Ellipsis Demo"/>

  <TextView
    android:id="@+id/demo_read_more"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Read More Demo"/>

  <TextView
    android:id="@+id/demo_read_more_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Read More List Demo"/>


  <TextView
    android:id="@+id/prepare_layout_cache"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Prepare Cache"/>

  <TextView
    android:id="@+id/demo_layout_cache"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Layout Cache Demo"/>

  <TextView
    android:id="@+id/demo_fast_tv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="FastTextView With Cache Layout"/>

  <TextView
    android:id="@+id/demo_normal_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="30sp"
    android:text="Normal List Demo"/>

</LinearLayout>


================================================
FILE: app/src/main/res/layout/layout_cache_demo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fillViewport="true"
  tools:context="com.wechat.testdemo.MainActivityFragment"
  tools:showIn="@layout/activity_main">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Android TextView:"/>

    <com.lsjwzh.test.TestTextView
      android:id="@+id/system_tv"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#6600ffff"
      android:ellipsize="end"
      android:maxLines="2"
      android:textSize="20sp"/>

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="FastTextLayoutView:"/>

    <com.lsjwzh.test.FastTextLayoutView
      android:id="@+id/fast_tv"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#6600ffff"
      android:clickable="true"/>

    <View
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>

</ScrollView>


================================================
FILE: app/src/main/res/layout/normal_layout_ui.xml
================================================
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

  <LinearLayout
    android:id="@+id/op_layout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:gravity="center">

    <Button
      android:id="@+id/scroll_down_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollDown"
      />

    <Button
      android:id="@+id/scroll_up_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollUp"/>
  </LinearLayout>

  <ListView
    android:id="@+id/test_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/op_layout"/>

</RelativeLayout>


================================================
FILE: app/src/main/res/layout/normal_list_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.test.TestTextView xmlns:android="http://schemas.android.com/apk/res/android"
                              android:id="@+id/normal_text"
                              android:layout_width="wrap_content"
                              android:layout_height="wrap_content"
                              android:textSize="25sp">

</com.lsjwzh.test.TestTextView>

================================================
FILE: app/src/main/res/layout/read_more_demo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fillViewport="true"
  tools:context="com.wechat.testdemo.MainActivityFragment"
  tools:showIn="@layout/activity_main">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ReadMoreTextView:"/>

    <com.lsjwzh.widget.text.ReadMoreTextView
      android:id="@+id/readmore_tv"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#6600ffff"
      android:clickable="true"
      android:ellipsize="end"
      android:maxLines="2"
      android:textSize="20sp"/>


    <View
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>

</ScrollView>


================================================
FILE: app/src/main/res/layout/readmore_list_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.widget.text.ReadMoreTextView
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/fast_text_view"
	android:textSize="25sp"
	android:ellipsize="end"
	android:maxLines="2"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"/>

================================================
FILE: app/src/main/res/layout/spinner_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

  <TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="20sp"
    android:textColor="#ffffff"/>
</LinearLayout>

================================================
FILE: app/src/main/res/layout/static_layout_ui.xml
================================================
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
  >

  <LinearLayout
    android:id="@+id/op_layout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    android:gravity="center">

    <Button
      android:id="@+id/scroll_down_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollDown"
      />

    <Button
      android:id="@+id/scroll_up_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="ScrollUp"/>
  </LinearLayout>

  <ListView
    android:id="@+id/test_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/op_layout"/>

</RelativeLayout>


================================================
FILE: app/src/main/res/layout/static_list_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.test.FastTextLayoutView
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/static_layout_view"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"/>

================================================
FILE: app/src/main/res/menu/menu_main.xml
================================================
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="com.wechat.testdemo.MainActivity">
  <item
    android:id="@+id/action_settings"
    android:orderInCategory="100"
    android:title="@string/action_settings"
    app:showAsAction="never"/>
</menu>


================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
</resources>


================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
  <dimen name="fab_margin">16dp</dimen>
</resources>


================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
  <string name="app_name">testdemo</string>
  <string name="action_settings">Settings</string>
  <string name="content_eng">Jarlsberg lancashire edam  .Dolcelatte hard cheese brie st. agur blue
    cheese caerphilly bavarian bergkase cheese and biscuits mascarpone. Cheeseburger swiss bavarian
    bergkase cream cheese fromage frais cheesy feet port-salut airedale. St. agur blue cheese rubber
    cheese caerphilly cheddar cheesecake cream cheese manchego lancashire. Roquefort squirty cheese
    the big cheese.</string>
  <string name="content_cn">东临碣石,以观沧海。水何澹澹,山岛竦峙。树木丛生,百草丰茂。秋风萧瑟,洪波涌起。日月之行,若出其中。星汉灿烂,若出其里。幸甚至哉,歌以咏志。</string>
</resources>


================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>

  <!-- Base application theme. -->
  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>

  <style name="AppTheme.NoActionBar">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
  </style>

  <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>

  <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light">
    
  </style>

  <style name="DropDownPopup" />

  <style name="DropDownPopup.DropDownDown">
    <item name="android:windowEnterAnimation">@null</item>
    <item name="android:windowExitAnimation">@null</item>
  </style>

  <style name="DropDownPopup.DropDownUp">
    <item name="android:windowEnterAnimation">@null</item>
    <item name="android:windowExitAnimation">@null</item>
  </style>
</resources>


================================================
FILE: app/src/main/res/values-v23/styles.xml
================================================
<resources>

  <!-- Base application theme. -->
  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>

  <style name="AppTheme.NoActionBar">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
  </style>

  <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>

  <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light">
    <item name="android:popupEnterTransition">@null</item>
    <item name="android:popupExitTransition">@null</item>
  </style>

  <style name="DropDownPopup" />

  <style name="DropDownPopup.DropDownDown">
    <item name="android:windowEnterAnimation">@anim/popup_enter</item>
    <item name="android:windowExitAnimation">@anim/popup_exit</item>
  </style>

  <style name="DropDownPopup.DropDownUp">
    <item name="android:windowEnterAnimation">@anim/popup_enter</item>
    <item name="android:windowExitAnimation">@anim/popup_exit</item>
  </style>
</resources>


================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        mavenCentral()
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.1'
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
//        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
    }
}

Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
allprojects {
    gradle.taskGraph.whenReady {
        tasks.each { task ->
            if (task.name.equals('createMockableJar')) {
                task.enabled = false
            }
        }
    }
    repositories {
        google()
        maven {
            url "https://dl.bintray.com/lsjwzh/maven"
        }

        maven {
            url properties.getProperty("sdk.dir") + "/extras/android/m2repository"
        }
        mavenCentral()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true


================================================
FILE: gradlew
================================================
#!/usr/bin/env bash

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
    echo "$*"
}

die ( ) {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
esac

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
    JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"


================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windowz variants

if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*
goto execute

:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: settings.gradle
================================================
include ':app', ':widget.FastTextView'


================================================
FILE: text.Textline/.gitignore
================================================
/build


================================================
FILE: text.Textline/build.gradle
================================================
apply plugin: 'com.android.library'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 4
        versionName '1.0'

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

}


================================================
FILE: text.Textline/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: text.Textline/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.lsjwzh.text"/>


================================================
FILE: text.Textline/src/main/java/android/text/Directions.java
================================================
package android.text;

/**
 * Stores information about bidirectional (left-to-right or right-to-left)
 * text within the layout of a line.
 */
public class Directions {
  // Directions represents directional runs within a line of text.
  // Runs are pairs of ints listed in visual order, starting from the
  // leading margin.  The first int of each pair is the offset from
  // the first character of the line to the start of the run.  The
  // second int represents both the length and level of the run.
  // The length is in the lower bits, accessed by masking with
  // DIR_LENGTH_MASK.  The level is in the higher bits, accessed
  // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
  // To simply test for an RTL direction, test the bit using
  // DIR_RTL_FLAG, if set then the direction is rtl.

  /* package */ int[] mDirections;

  /* package */ Directions(int[] dirs) {
    mDirections = dirs;
  }


  /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
  /* package */ static final int RUN_LEVEL_SHIFT = 26;
  /* package */ static final int RUN_LEVEL_MASK = 0x3f;
  /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
  public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
      new Directions(new int[]{0, RUN_LENGTH_MASK});
  public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
      new Directions(new int[]{0, RUN_LENGTH_MASK | RUN_RTL_FLAG});
}

================================================
FILE: text.Textline/src/main/java/android/text/ITextLine.java
================================================
package android.text;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout.TabStops;

public interface ITextLine {

  /**
   * Initializes a TextLine and prepares it for use.
   *
   * @param paint      the base paint for the line
   * @param text       the text, can be Styled
   * @param start      the start of the line relative to the text
   * @param limit      the limit of the line relative to the text
   * @param dir        the paragraph direction of this line
   * @param directions the directions information of this line
   */
  void set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops);

  /**
   * Returns metrics information for the entire line.
   *
   * @param fmi receives font metrics information, can be null
   * @return the signed width of the line
   */
  float metrics(Paint.FontMetricsInt fmi);

  /**
   * Renders the TextLine.
   *
   * @param c      the canvas to render on
   * @param x      the leading margin position
   * @param top    the top of the line
   * @param y      the baseline
   * @param bottom the bottom of the line
   */
  void draw(Canvas c, float x, int top, int y, int bottom);
}


================================================
FILE: text.Textline/src/main/java/android/text/TextLineCompat.java
================================================
package android.text;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.text.Layout.TabStops;

/**
 * Make TextLine can be accessed by other package.
 */
public class TextLineCompat implements ITextLine {
  private ITextLine mTextLine;

  private TextLineCompat(ITextLine textLine) {
    mTextLine = textLine;
  }

  /**
   * Returns a new TextLine from the shared pool.
   *
   * @return an uninitialized TextLine
   */
  public static TextLineCompat obtain() {

    if (Build.VERSION.SDK_INT >= 23) {
      return new TextLineCompat(TextLineImpl23.obtain());
    } else {
      return new TextLineCompat(TextLineImpl15.obtain());
    }
  }

  /**
   * Puts a TextLine back into the shared pool. Do not use this TextLine once
   * it has been returned.
   *
   * @param tl the textLine
   *           TextLine
   */
  public static void recycle(TextLineCompat tl) {
    if (Build.VERSION.SDK_INT >= 23) {
      TextLineImpl23.recycle((TextLineImpl23) tl.mTextLine);
    } else {
      TextLineImpl15.recycle((TextLineImpl15) tl.mTextLine);
    }
  }

  /**
   * Initializes a TextLine and prepares it for use.
   *
   * @param paint      the base paint for the line
   * @param text       the text, can be Styled
   * @param start      the start of the line relative to the text
   * @param limit      the limit of the line relative to the text
   * @param dir        the paragraph direction of this line
   * @param directions the directions information of this line
   */
  @Override
  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops) {
    mTextLine.set(paint, text, start, limit, dir, directions, hasTabs, tabStops);
  }

  /**
   * Returns metrics information for the entire line.
   *
   * @param fmi receives font metrics information, can be null
   * @return the signed width of the line
   */
  public float metrics(Paint.FontMetricsInt fmi) {
    return mTextLine.metrics(fmi);
  }

  @Override
  public void draw(Canvas c, float x, int top, int y, int bottom) {
    mTextLine.draw(c, x, top, y, bottom);
  }
}


================================================
FILE: text.Textline/src/main/java/android/text/TextLineImpl15.java
================================================
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.RectF;
import android.support.annotation.RequiresApi;
import android.text.Layout.TabStops;
import android.text.style.CharacterStyle;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Log;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Represents a line of styled text, for measuring in visual order and
 * for rendering.
 * <p>
 * <p>Get a new instance using obtain(), and when finished with it, return it
 * to the pool using recycle().
 * <p>
 * <p>Call set to prepare the instance for use, then either draw, measure,
 * metrics, or caretToLeftRightOf.
 */
@RequiresApi(15)
public class TextLineImpl15 implements ITextLine {
  private static final boolean DEBUG = false;
  private static Method sDrawTextRun1;
  private static Method sDrawTextRun2;
  private static Method sGetTextRunAdvances1;
  private static Method sGetTextRunAdvances2;

  private TextPaint mPaint;
  private CharSequence mText;
  private int mStart;
  private int mLen;
  private int mDir;
  private Directions mDirections;
  private boolean mHasTabs;
  private TabStops mTabs;
  private char[] mChars;
  private boolean mCharsValid;
  private Spanned mSpanned;
  private final TextPaint mWorkPaint = new TextPaint();
  private final SpanSetCompat<MetricAffectingSpan> mMetricAffectingSpanSpanSetCompat =
      new SpanSetCompat<MetricAffectingSpan>(MetricAffectingSpan.class);
  private final SpanSetCompat<CharacterStyle> mCharacterStyleSpanSetCompat =
      new SpanSetCompat<CharacterStyle>(CharacterStyle.class);
  private final SpanSetCompat<ReplacementSpan> mReplacementSpanSpanSetCompat =
      new SpanSetCompat<ReplacementSpan>(ReplacementSpan.class);

  private static final TextLineImpl15[] sCached = new TextLineImpl15[3];

  /**
   * Returns a new TextLine from the shared pool.
   *
   * @return an uninitialized TextLine
   */
  static TextLineImpl15 obtain() {
    TextLineImpl15 tl;
    synchronized (sCached) {
      for (int i = sCached.length; --i >= 0; ) {
        if (sCached[i] != null) {
          tl = sCached[i];
          sCached[i] = null;
          return tl;
        }
      }
    }
    tl = new TextLineImpl15();
    if (DEBUG) {
      Log.v("TLINE", "new: " + tl);
    }
    return tl;
  }

  /**
   * Puts a TextLine back into the shared pool. Do not use this TextLine once
   * it has been returned.
   *
   * @param tl the textLine
   * @return null, as a convenience from clearing references to the provided
   * TextLine
   */
  static TextLineImpl15 recycle(TextLineImpl15 tl) {
    tl.mText = null;
    tl.mPaint = null;
    tl.mDirections = null;

    tl.mMetricAffectingSpanSpanSetCompat.recycle();
    tl.mCharacterStyleSpanSetCompat.recycle();
    tl.mReplacementSpanSpanSetCompat.recycle();

    synchronized (sCached) {
      for (int i = 0; i < sCached.length; ++i) {
        if (sCached[i] == null) {
          sCached[i] = tl;
          break;
        }
      }
    }
    return null;
  }

  /**
   * Initializes a TextLine and prepares it for use.
   *
   * @param paint      the base paint for the line
   * @param text       the text, can be Styled
   * @param start      the start of the line relative to the text
   * @param limit      the limit of the line relative to the text
   * @param dir        the paragraph direction of this line
   * @param directions the directions information of this line
   * @param hasTabs    true if the line might contain tabs or emoji
   * @param tabStops   the tabStops. Can be null.
   */
  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
                  Directions directions, boolean hasTabs, TabStops tabStops) {
    mPaint = paint;
    mText = text;
    mStart = start;
    mLen = limit - start;
    mDir = dir;
    mDirections = directions;
    if (mDirections == null) {
      throw new IllegalArgumentException("Directions cannot be null");
    }
    mHasTabs = hasTabs;
    mSpanned = null;

    boolean hasReplacement = false;
    if (text instanceof Spanned) {
      mSpanned = (Spanned) text;
      mReplacementSpanSpanSetCompat.init(mSpanned, start, limit);
      hasReplacement = mReplacementSpanSpanSetCompat.numberOfSpans > 0;
    }

    mCharsValid = hasReplacement || hasTabs || directions != Directions.DIRS_ALL_LEFT_TO_RIGHT;

    if (mCharsValid) {
      if (mChars == null || mChars.length < mLen) {
        mChars = new char[idealCharArraySize(mLen)];
      }
      TextUtils.getChars(text, start, limit, mChars, 0);
      if (hasReplacement) {
        // Handle these all at once so we don't have to do it as we go.
        // Replace the first character of each replacement run with the
        // object-replacement character and the remainder with zero width
        // non-break space aka BOM.  Cursor movement code skips these
        // zero-width characters.
        char[] chars = mChars;
        for (int i = start, inext; i < limit; i = inext) {
          inext = mReplacementSpanSpanSetCompat.getNextTransition(i, limit);
          if (mReplacementSpanSpanSetCompat.hasSpansIntersecting(i, inext)) {
            // transition into a span
            chars[i - start] = '\ufffc';
            for (int j = i - start + 1, e = inext - start; j < e; ++j) {
              chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
            }
          }
        }
      }
    }
    mTabs = tabStops;
  }

  /**
   * Renders the TextLine.
   *
   * @param c      the canvas to render on
   * @param x      the leading margin position
   * @param top    the top of the line
   * @param y      the baseline
   * @param bottom the bottom of the line
   */
  public void draw(Canvas c, float x, int top, int y, int bottom) {
    if (!mHasTabs) {
      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {
        drawRun(c, 0, mLen, false, x, top, y, bottom, false);
        return;
      }
      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {
        drawRun(c, 0, mLen, true, x, top, y, bottom, false);
        return;
      }
    }

    float h = 0;
    int[] runs = mDirections.mDirections;
    RectF emojiRect = null;

    int lastRunIndex = runs.length - 2;
    for (int i = 0; i < runs.length; i += 2) {
      int runStart = runs[i];
      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
      if (runLimit > mLen) {
        runLimit = mLen;
      }
      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;

      int segstart = runStart;
      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
        int codept = 0;
        Bitmap bm = null;

        if (mHasTabs && j < runLimit) {
          codept = mChars[j];
          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
            codept = Character.codePointAt(mChars, j);
            if (codept > 0xFFFF) {
              ++j;
              continue;
            }
          }
        }

        if (j == runLimit || codept == '\t' || bm != null) {
          h += drawRun(c, segstart, j, runIsRtl, x + h, top, y, bottom,
              i != lastRunIndex || j != mLen);

          if (codept == '\t') {
            h = mDir * nextTab(h * mDir);
          } else if (bm != null) {
            float bmAscent = ascent(j);
            float bitmapHeight = bm.getHeight();
            float scale = -bmAscent / bitmapHeight;
            float width = bm.getWidth() * scale;

            if (emojiRect == null) {
              emojiRect = new RectF();
            }
            emojiRect.set(x + h, y + bmAscent,
                x + h + width, y);
            c.drawBitmap(bm, null, emojiRect, mPaint);
            h += width;
            j++;
          }
          segstart = j + 1;
        }
      }
    }
  }

  /**
   * Returns metrics information for the entire line.
   *
   * @param fmi receives font metrics information, can be null
   * @return the signed width of the line
   */
  public float metrics(FontMetricsInt fmi) {
    return measure(mLen, false, fmi);
  }

  /**
   * Returns information about a position on the line.
   *
   * @param offset   the line-relative character offset, between 0 and the
   *                 line length, inclusive
   * @param trailing true to measure the trailing edge of the character
   *                 before offset, false to measure the leading edge of the character
   *                 at offset.
   * @param fmi      receives metrics information about the requested
   *                 character, can be null.
   * @return the signed offset from the leading margin to the requested
   * character edge.
   */
  float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    int target = trailing ? offset - 1 : offset;
    if (target < 0) {
      return 0;
    }

    float h = 0;

    if (!mHasTabs) {
      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {
        return measureRun(0, offset, mLen, false, fmi);
      }
      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {
        return measureRun(0, offset, mLen, true, fmi);
      }
    }

    char[] chars = mChars;
    int[] runs = mDirections.mDirections;
    for (int i = 0; i < runs.length; i += 2) {
      int runStart = runs[i];
      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
      if (runLimit > mLen) {
        runLimit = mLen;
      }
      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;

      int segstart = runStart;
      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
        int codept = 0;
        Bitmap bm = null;

        if (mHasTabs && j < runLimit) {
          codept = chars[j];
          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
            codept = Character.codePointAt(mChars, j);
            if (codept > 0xFFFF) {
              ++j;
              continue;
            }
          }
        }

        if (j == runLimit || codept == '\t' || bm != null) {
          boolean inSegment = target >= segstart && target < j;

          boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
          if (inSegment && advance) {
            return h += measureRun(segstart, offset, j, runIsRtl, fmi);
          }

          float w = measureRun(segstart, j, j, runIsRtl, fmi);
          h += advance ? w : -w;

          if (inSegment) {
            return h += measureRun(segstart, offset, j, runIsRtl, null);
          }

          if (codept == '\t') {
            if (offset == j) {
              return h;
            }
            h = mDir * nextTab(h * mDir);
            if (target == j) {
              return h;
            }
          }

          if (bm != null) {
            float bmAscent = ascent(j);
            float wid = bm.getWidth() * -bmAscent / bm.getHeight();
            h += mDir * wid;
            j++;
          }

          segstart = j + 1;
        }
      }
    }

    return h;
  }

  /**
   * Draws a unidirectional (but possibly multi-styled) run of text.
   *
   * @param c         the canvas to draw on
   * @param start     the line-relative start
   * @param limit     the line-relative limit
   * @param runIsRtl  true if the run is right-to-left
   * @param x         the position of the run that is closest to the leading margin
   * @param top       the top of the line
   * @param y         the baseline
   * @param bottom    the bottom of the line
   * @param needWidth true if the width value is required.
   * @return the signed width of the run, based on the paragraph direction.
   * Only valid if needWidth is true.
   */
  private float drawRun(Canvas c, int start,
                        int limit, boolean runIsRtl, float x, int top, int y, int bottom,
                        boolean needWidth) {

    if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
      float w = -measureRun(start, limit, limit, runIsRtl, null);
      handleRun(start, limit, limit, runIsRtl, c, x + w, top,
          y, bottom, null, false);
      return w;
    }

    return handleRun(start, limit, limit, runIsRtl, c, x, top,
        y, bottom, null, needWidth);
  }

  /**
   * Measures a unidirectional (but possibly multi-styled) run of text.
   *
   * @param start    the line-relative start of the run
   * @param offset   the offset to measure to, between start and limit inclusive
   * @param limit    the line-relative limit of the run
   * @param runIsRtl true if the run is right-to-left
   * @param fmi      receives metrics information about the requested
   *                 run, can be null.
   * @return the signed width from the start of the run to the leading edge
   * of the character at offset, based on the run (not paragraph) direction
   */
  private float measureRun(int start, int offset, int limit, boolean runIsRtl,
                           FontMetricsInt fmi) {
    return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
  }

  /**
   * Walk the cursor through this line, skipping conjuncts and
   * zero-width characters.
   * <p>
   * <p>This function cannot properly walk the cursor off the ends of the line
   * since it does not know about any shaping on the previous/following line
   * that might affect the cursor position. Callers must either avoid these
   * situations or handle the result specially.
   *
   * @param cursor the starting position of the cursor, between 0 and the
   *               length of the line, inclusive
   * @param toLeft true if the caret is moving to the left.
   * @return the new offset.  If it is less than 0 or greater than the length
   * of the line, the previous/following line should be examined to get the
   * actual offset.
   */
  int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    // 1) The caret marks the leading edge of a character. The character
    // logically before it might be on a different level, and the active caret
    // position is on the character at the lower level. If that character
    // was the previous character, the caret is on its trailing edge.
    // 2) Take this character/edge and move it in the indicated direction.
    // This gives you a new character and a new edge.
    // 3) This position is between two visually adjacent characters.  One of
    // these might be at a lower level.  The active position is on the
    // character at the lower level.
    // 4) If the active position is on the trailing edge of the character,
    // the new caret position is the following logical character, else it
    // is the character.

    int lineStart = 0;
    int lineEnd = mLen;
    boolean paraIsRtl = mDir == -1;
    int[] runs = mDirections.mDirections;

    int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
    boolean trailing = false;

    if (cursor == lineStart) {
      runIndex = -2;
    } else if (cursor == lineEnd) {
      runIndex = runs.length;
    } else {
      // First, get information about the run containing the character with
      // the active caret.
      for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
        runStart = lineStart + runs[runIndex];
        if (cursor >= runStart) {
          runLimit = runStart + (runs[runIndex + 1] & Layout.RUN_LENGTH_MASK);
          if (runLimit > lineEnd) {
            runLimit = lineEnd;
          }
          if (cursor < runLimit) {
            runLevel = (runs[runIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &
                Layout.RUN_LEVEL_MASK;
            if (cursor == runStart) {
              // The caret is on a run boundary, see if we should
              // use the position on the trailing edge of the previous
              // logical character instead.
              int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
              int pos = cursor - 1;
              for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
                prevRunStart = lineStart + runs[prevRunIndex];
                if (pos >= prevRunStart) {
                  prevRunLimit = prevRunStart +
                      (runs[prevRunIndex + 1] & Layout.RUN_LENGTH_MASK);
                  if (prevRunLimit > lineEnd) {
                    prevRunLimit = lineEnd;
                  }
                  if (pos < prevRunLimit) {
                    prevRunLevel = (runs[prevRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT)
                        & Layout.RUN_LEVEL_MASK;
                    if (prevRunLevel < runLevel) {
                      // Start from logically previous character.
                      runIndex = prevRunIndex;
                      runLevel = prevRunLevel;
                      runStart = prevRunStart;
                      runLimit = prevRunLimit;
                      trailing = true;
                      break;
                    }
                  }
                }
              }
            }
            break;
          }
        }
      }

      // caret might be == lineEnd.  This is generally a space or paragraph
      // separator and has an associated run, but might be the end of
      // text, in which case it doesn't.  If that happens, we ran off the
      // end of the run list, and runIndex == runs.length.  In this case,
      // we are at a run boundary so we skip the below test.
      if (runIndex != runs.length) {
        boolean runIsRtl = (runLevel & 0x1) != 0;
        boolean advance = toLeft == runIsRtl;
        if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
          // Moving within or into the run, so we can move logically.
          newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
              runIsRtl, cursor, advance);
          // If the new position is internal to the run, we're at the strong
          // position already so we're finished.
          if (newCaret != (advance ? runLimit : runStart)) {
            return newCaret;
          }
        }
      }
    }

    // If newCaret is -1, we're starting at a run boundary and crossing
    // into another run. Otherwise we've arrived at a run boundary, and
    // need to figure out which character to attach to.  Note we might
    // need to run this twice, if we cross a run boundary and end up at
    // another run boundary.
    while (true) {
      boolean advance = toLeft == paraIsRtl;
      int otherRunIndex = runIndex + (advance ? 2 : -2);
      if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
        int otherRunStart = lineStart + runs[otherRunIndex];
        int otherRunLimit = otherRunStart +
            (runs[otherRunIndex + 1] & Layout.RUN_LENGTH_MASK);
        if (otherRunLimit > lineEnd) {
          otherRunLimit = lineEnd;
        }
        int otherRunLevel = (runs[otherRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &
            Layout.RUN_LEVEL_MASK;
        boolean otherRunIsRtl = (otherRunLevel & 1) != 0;

        advance = toLeft == otherRunIsRtl;
        if (newCaret == -1) {
          newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
              otherRunLimit, otherRunIsRtl,
              advance ? otherRunStart : otherRunLimit, advance);
          if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
            // Crossed and ended up at a new boundary,
            // repeat a second and final time.
            runIndex = otherRunIndex;
            runLevel = otherRunLevel;
            continue;
          }
          break;
        }

        // The new caret is at a boundary.
        if (otherRunLevel < runLevel) {
          // The strong character is in the other run.
          newCaret = advance ? otherRunStart : otherRunLimit;
        }
        break;
      }

      if (newCaret == -1) {
        // We're walking off the end of the line.  The paragraph
        // level is always equal to or lower than any internal level, so
        // the boundaries get the strong caret.
        newCaret = advance ? mLen + 1 : -1;
        break;
      }

      // Else we've arrived at the end of the line.  That's a strong position.
      // We might have arrived here by crossing over a run with no internal
      // breaks and dropping out of the above loop before advancing one final
      // time, so reset the caret.
      // Note, we use '<=' below to handle a situation where the only run
      // on the line is a counter-directional run.  If we're not advancing,
      // we can end up at the 'lineEnd' position but the caret we want is at
      // the lineStart.
      if (newCaret <= lineEnd) {
        newCaret = advance ? lineEnd : lineStart;
      }
      break;
    }

    return newCaret;
  }

  /**
   * Returns the next valid offset within this directional run, skipping
   * conjuncts and zero-width characters.  This should not be called to walk
   * off the end of the line, since the returned values might not be valid
   * on neighboring lines.  If the returned offset is less than zero or
   * greater than the line length, the offset should be recomputed on the
   * preceding or following line, respectively.
   *
   * @param runIndex the run index
   * @param runStart the start of the run
   * @param runLimit the limit of the run
   * @param runIsRtl true if the run is right-to-left
   * @param offset   the offset
   * @param after    true if the new offset should logically follow the provided
   *                 offset
   * @return the new offset
   */
  private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
                                   boolean runIsRtl, int offset, boolean after) {

    if (runIndex < 0 || offset == (after ? mLen : 0)) {
      // Walking off end of line.  Since we don't know
      // what cursor positions are available on other lines, we can't
      // return accurate values.  These are a guess.
      if (after) {
        return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
      }
      return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
    }

    TextPaint wp = mWorkPaint;
    wp.set(mPaint);

    int spanStart = runStart;
    int spanLimit;
    if (mSpanned == null) {
      spanLimit = runLimit;
    } else {
      int target = after ? offset + 1 : offset;
      int limit = mStart + runLimit;
      while (true) {
        spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
            MetricAffectingSpan.class) - mStart;
        if (spanLimit >= target) {
          break;
        }
        spanStart = spanLimit;
      }

      MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
          mStart + spanLimit, MetricAffectingSpan.class);
      spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);

      if (spans.length > 0) {
        ReplacementSpan replacement = null;
        for (int j = 0; j < spans.length; j++) {
          MetricAffectingSpan span = spans[j];
          if (span instanceof ReplacementSpan) {
            replacement = (ReplacementSpan) span;
          } else {
            span.updateMeasureState(wp);
          }
        }

        if (replacement != null) {
          // If we have a replacement span, we're moving either to
          // the start or end of this span.
          return after ? spanLimit : spanStart;
        }
      }
    }

    int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
    int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
    if (mCharsValid) {
      return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
          flags, offset, cursorOpt);
    } else {
      return wp.getTextRunCursor(mText, mStart + spanStart,
          mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart;
    }
  }

  /**
   * @param wp
   */
  private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
    final int previousTop = fmi.top;
    final int previousAscent = fmi.ascent;
    final int previousDescent = fmi.descent;
    final int previousBottom = fmi.bottom;
    final int previousLeading = fmi.leading;

    wp.getFontMetricsInt(fmi);

    updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
        previousLeading);
  }

  static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
                            int previousDescent, int previousBottom, int previousLeading) {
    fmi.top = Math.min(fmi.top, previousTop);
    fmi.ascent = Math.min(fmi.ascent, previousAscent);
    fmi.descent = Math.max(fmi.descent, previousDescent);
    fmi.bottom = Math.max(fmi.bottom, previousBottom);
    fmi.leading = Math.max(fmi.leading, previousLeading);
  }

  /**
   * Utility function for measuring and rendering text.  The text must
   * not include a tab or emoji.
   *
   * @param wp        the working paint
   * @param start     the start of the text
   * @param end       the end of the text
   * @param runIsRtl  true if the run is right-to-left
   * @param c         the canvas, can be null if rendering is not needed
   * @param x         the edge of the run closest to the leading margin
   * @param top       the top of the line
   * @param y         the baseline
   * @param bottom    the bottom of the line
   * @param fmi       receives metrics information, can be null
   * @param needWidth true if the width of the run is needed
   * @return the signed width of the run based on the run direction; only
   * valid if needWidth is true
   */
  private float handleText(TextPaint wp, int start, int end,
                           int contextStart, int contextEnd, boolean runIsRtl,
                           Canvas c, float x, int top, int y, int bottom,
                           FontMetricsInt fmi, boolean needWidth) {

    // Get metrics first (even for empty strings or "0" width runs)
    if (fmi != null) {
      expandMetricsFromPaint(fmi, wp);
    }

    int runLen = end - start;
    // No need to do anything if the run width is "0"
    if (runLen == 0) {
      return 0f;
    }

    float ret = 0;

    int contextLen = contextEnd - contextStart;
    if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
      int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
      ret = getTextRunAdvances(wp, start, end, contextStart, contextEnd, runLen, contextLen, flags);
    }

    if (c != null) {
      if (runIsRtl) {
        x -= ret;
      }

      if (wp.bgColor != 0) {
        int previousColor = wp.getColor();
        Paint.Style previousStyle = wp.getStyle();

        wp.setColor(wp.bgColor);
        wp.setStyle(Paint.Style.FILL);
        c.drawRect(x, top, x + ret, bottom, wp);

        wp.setStyle(previousStyle);
        wp.setColor(previousColor);
      }

      if (wp.underlineColor != 0) {
        // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
        float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();

        int previousColor = wp.getColor();
        Paint.Style previousStyle = wp.getStyle();
        boolean previousAntiAlias = wp.isAntiAlias();

        wp.setStyle(Paint.Style.FILL);
        wp.setAntiAlias(true);

        wp.setColor(wp.underlineColor);
        c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);

        wp.setStyle(previousStyle);
        wp.setColor(previousColor);
        wp.setAntiAlias(previousAntiAlias);
      }

      drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
          x, y + wp.baselineShift);
    }

    return runIsRtl ? -ret : ret;
  }

  private float getTextRunAdvances(TextPaint wp, int start, int end, int contextStart, int contextEnd, int runLen, int contextLen, int flags) {
    if (sGetTextRunAdvances1 == null) {
      try {
        sGetTextRunAdvances1 = wp.getClass().getDeclaredMethod("getTextRunAdvances",
            char[].class, int.class, int.class, int.class, int.class, int.class, float[].class, int.class);
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
    if (sGetTextRunAdvances2 == null) {
      try {
        sGetTextRunAdvances2 = wp.getClass().getDeclaredMethod("drawTextRun",
            CharSequence.class, int.class, int.class, int.class, int.class, int.class, float[].class, int.class);
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
    float ret = 0;
    if (mCharsValid) {
      try {
        ret = (float) sGetTextRunAdvances1.invoke(wp, mChars, start, runLen,
            contextStart, contextLen, flags, null, 0);
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    } else {
      int delta = mStart;
      try {
        ret = (float) sGetTextRunAdvances1.invoke(wp, mText, delta + start,
            delta + end, delta + contextStart, delta + contextEnd,
            flags, null, 0);
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    }
    return ret;
  }

  /**
   * Utility function for measuring and rendering a replacement.
   *
   * @param replacement the replacement
   * @param wp          the work paint
   * @param start       the start of the run
   * @param limit       the limit of the run
   * @param runIsRtl    true if the run is right-to-left
   * @param c           the canvas, can be null if not rendering
   * @param x           the edge of the replacement closest to the leading margin
   * @param top         the top of the line
   * @param y           the baseline
   * @param bottom      the bottom of the line
   * @param fmi         receives metrics information, can be null
   * @param needWidth   true if the width of the replacement is needed
   * @return the signed width of the run based on the run direction; only
   * valid if needWidth is true
   */
  private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
                                  int start, int limit, boolean runIsRtl, Canvas c,
                                  float x, int top, int y, int bottom, FontMetricsInt fmi,
                                  boolean needWidth) {

    float ret = 0;

    int textStart = mStart + start;
    int textLimit = mStart + limit;

    if (needWidth || (c != null && runIsRtl)) {
      int previousTop = 0;
      int previousAscent = 0;
      int previousDescent = 0;
      int previousBottom = 0;
      int previousLeading = 0;

      boolean needUpdateMetrics = (fmi != null);

      if (needUpdateMetrics) {
        previousTop = fmi.top;
        previousAscent = fmi.ascent;
        previousDescent = fmi.descent;
        previousBottom = fmi.bottom;
        previousLeading = fmi.leading;
      }

      ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);

      if (needUpdateMetrics) {
        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
            previousLeading);
      }
    }

    if (c != null) {
      if (runIsRtl) {
        x -= ret;
      }
      replacement.draw(c, mText, textStart, textLimit,
          x, top, y, bottom, wp);
    }

    return runIsRtl ? -ret : ret;
  }

  private static class SpanSetCompat<E> {
    int numberOfSpans;
    E[] spans;
    int[] spanStarts;
    int[] spanEnds;
    int[] spanFlags;
    final Class<? extends E> classType;

    SpanSetCompat(Class<? extends E> type) {
      classType = type;
      numberOfSpans = 0;
    }

    @SuppressWarnings("unchecked")
    public void init(Spanned spanned, int start, int limit) {
      final E[] allSpans = spanned.getSpans(start, limit, classType);
      final int length = allSpans.length;

      if (length > 0 && (spans == null || spans.length < length)) {
        // These arrays may end up being too large because of empty spans
        spans = (E[]) Array.newInstance(classType, length);
        spanStarts = new int[length];
        spanEnds = new int[length];
        spanFlags = new int[length];
      }

      numberOfSpans = 0;
      for (int i = 0; i < length; i++) {
        final E span = allSpans[i];

        final int spanStart = spanned.getSpanStart(span);
        final int spanEnd = spanned.getSpanEnd(span);
        if (spanStart == spanEnd) continue;

        final int spanFlag = spanned.getSpanFlags(span);

        spans[numberOfSpans] = span;
        spanStarts[numberOfSpans] = spanStart;
        spanEnds[numberOfSpans] = spanEnd;
        spanFlags[numberOfSpans] = spanFlag;

        numberOfSpans++;
      }
    }

    public boolean hasSpansIntersecting(int start, int end) {
      for (int i = 0; i < numberOfSpans; i++) {
        // equal test is valid since both intervals are not empty by construction
        if (spanStarts[i] >= end || spanEnds[i] <= start) continue;
        return true;
      }
      return false;
    }

    int getNextTransition(int start, int limit) {
      for (int i = 0; i < numberOfSpans; i++) {
        final int spanStart = spanStarts[i];
        final int spanEnd = spanEnds[i];
        if (spanStart > start && spanStart < limit) limit = spanStart;
        if (spanEnd > start && spanEnd < limit) limit = spanEnd;
      }
      return limit;
    }

    public void recycle() {
      // The spans array is guaranteed to be not null when numberOfSpans is > 0
      for (int i = 0; i < numberOfSpans; i++) {
        spans[i] = null; // prevent a leak: no reference kept when TextLine is recycled
      }
    }
  }

  /**
   * Utility function for handling a unidirectional run.  The run must not
   * contain tabs or emoji but can contain styles.
   *
   * @param start        the line-relative start of the run
   * @param measureLimit the offset to measure to, between start and limit inclusive
   * @param limit        the limit of the run
   * @param runIsRtl     true if the run is right-to-left
   * @param c            the canvas, can be null
   * @param x            the end of the run closest to the leading margin
   * @param top          the top of the line
   * @param y            the baseline
   * @param bottom       the bottom of the line
   * @param fmi          receives metrics information, can be null
   * @param needWidth    true if the width is required
   * @return the signed width of the run based on the run direction; only
   * valid if needWidth is true
   */
  private float handleRun(int start, int measureLimit,
                          int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
                          int bottom, FontMetricsInt fmi, boolean needWidth) {

    // Case of an empty line, make sure we update fmi according to mPaint
    if (start == measureLimit) {
      TextPaint wp = mWorkPaint;
      wp.set(mPaint);
      if (fmi != null) {
        expandMetricsFromPaint(fmi, wp);
      }
      return 0f;
    }

    if (mSpanned == null) {
      TextPaint wp = mWorkPaint;
      wp.set(mPaint);
      final int mlimit = measureLimit;
      return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
          y, bottom, fmi, needWidth || mlimit < measureLimit);
    }

    mMetricAffectingSpanSpanSetCompat.init(mSpanned, mStart + start, mStart + limit);
    mCharacterStyleSpanSetCompat.init(mSpanned, mStart + start, mStart + limit);

    // Shaping needs to take into account context up to metric boundaries,
    // but rendering needs to take into account character style boundaries.
    // So we iterate through metric runs to get metric bounds,
    // then within each metric run iterate through character style runs
    // for the run bounds.
    final float originalX = x;
    for (int i = start, inext; i < measureLimit; i = inext) {
      TextPaint wp = mWorkPaint;
      wp.set(mPaint);

      inext = mMetricAffectingSpanSpanSetCompat.getNextTransition(mStart + i, mStart + limit) -
          mStart;
      int mlimit = Math.min(inext, measureLimit);

      ReplacementSpan replacement = null;

      for (int j = 0; j < mMetricAffectingSpanSpanSetCompat.numberOfSpans; j++) {
        // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
        // empty by construction. This special case in getSpans() explains the >= & <= tests
        if ((mMetricAffectingSpanSpanSetCompat.spanStarts[j] >= mStart + mlimit) ||
            (mMetricAffectingSpanSpanSetCompat.spanEnds[j] <= mStart + i)) continue;
        MetricAffectingSpan span = mMetricAffectingSpanSpanSetCompat.spans[j];
        if (span instanceof ReplacementSpan) {
          replacement = (ReplacementSpan) span;
        } else {
          // We might have a replacement that uses the draw
          // state, otherwise measure state would suffice.
          span.updateDrawState(wp);
        }
      }

      if (replacement != null) {
        x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
            bottom, fmi, needWidth || mlimit < measureLimit);
        continue;
      }

      if (c == null) {
        x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
            y, bottom, fmi, needWidth || mlimit < measureLimit);
      } else {
        for (int j = i, jnext; j < mlimit; j = jnext) {
          jnext = mCharacterStyleSpanSetCompat.getNextTransition(mStart + j, mStart + mlimit) -
              mStart;

          wp.set(mPaint);
          for (int k = 0; k < mCharacterStyleSpanSetCompat.numberOfSpans; k++) {
            // Intentionally using >= and <= as explained above
            if ((mCharacterStyleSpanSetCompat.spanStarts[k] >= mStart + jnext) ||
                (mCharacterStyleSpanSetCompat.spanEnds[k] <= mStart + j)) continue;

            CharacterStyle span = mCharacterStyleSpanSetCompat.spans[k];
            span.updateDrawState(wp);
          }

          x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
              top, y, bottom, fmi, needWidth || jnext < measureLimit);
        }
      }
    }

    return x - originalX;
  }

  /**
   * Render a text run with the set-up paint.
   *
   * @param c            the canvas
   * @param wp           the paint used to render the text
   * @param start        the start of the run
   * @param end          the end of the run
   * @param contextStart the start of context for the run
   * @param contextEnd   the end of the context for the run
   * @param runIsRtl     true if the run is right-to-left
   * @param x            the x position of the left edge of the run
   * @param y            the baseline of the run
   */
  private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
                           int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
    if (sDrawTextRun1 == null) {
      try {
        sDrawTextRun1 = c.getClass().getDeclaredMethod("drawTextRun",
            char[].class, int.class, int.class, int.class, int.class, float.class, float.class, int.class, Paint.class);
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
    if (sDrawTextRun2 == null) {
      try {
        sDrawTextRun2 = c.getClass().getDeclaredMethod("drawTextRun",
            CharSequence.class, int.class, int.class, int.class, int.class, float.class, float.class, int.class, Paint.class);
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
    int flags = runIsRtl ? 1 : 0;
    if (mCharsValid) {
      int count = end - start;
      int contextCount = contextEnd - contextStart;
      try {
        sDrawTextRun1.invoke(c, mChars, start, count, contextStart, contextCount,
            x, y, flags, wp);
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    } else {
      int delta = mStart;
      try {
        sDrawTextRun2.invoke(c, mText, delta + start, delta + end,
            delta + contextStart, delta + contextEnd, x, y, flags, wp);
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    }
  }

  /**
   * Returns the ascent of the text at start.  This is used for scaling
   * emoji.
   *
   * @param pos the line-relative position
   * @return the ascent of the text at start
   */
  float ascent(int pos) {
    if (mSpanned == null) {
      return mPaint.ascent();
    }

    pos += mStart;
    MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
    if (spans.length == 0) {
      return mPaint.ascent();
    }

    TextPaint wp = mWorkPaint;
    wp.set(mPaint);
    for (MetricAffectingSpan span : spans) {
      span.updateMeasureState(wp);
    }
    return wp.ascent();
  }

  /**
   * Returns the next tab position.
   *
   * @param h the (unsigned) offset from the leading margin
   * @return the (unsigned) tab position after this offset
   */
  float nextTab(float h) {
    if (mTabs != null) {
      return mTabs.nextTab(h);
    }
    return TabStops.nextDefaultStop(h, TAB_INCREMENT);
  }

  private static final int TAB_INCREMENT = 20;


  public static int idealByteArraySize(int need) {
    for (int i = 4; i < 32; i++)
      if (need <= (1 << i) - 12)
        return (1 << i) - 12;

    return need;
  }

  public static int idealBooleanArraySize(int need) {
    return idealByteArraySize(need);
  }

  public static int idealShortArraySize(int need) {
    return idealByteArraySize(need * 2) / 2;
  }

  public static int idealCharArraySize(int need) {
    return idealByteArraySize(need * 2) / 2;
  }
}

================================================
FILE: text.Textline/src/main/java/android/text/TextLineImpl23.java
================================================
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.support.annotation.RequiresApi;
import android.text.Layout.TabStops;
import android.text.style.CharacterStyle;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Log;

import com.android.internal.util.ArrayUtils;

/**
 * Represents a line of styled text, for measuring in visual order and
 * for rendering.
 * <p>
 * <p>Get a new instance using obtain(), and when finished with it, return it
 * to the pool using recycle().
 * <p>
 * <p>Call set to prepare the instance for use, then either draw, measure,
 * metrics, or caretToLeftRightOf.
 */
@RequiresApi(23)
public class TextLineImpl23 implements ITextLine {
  private static final boolean DEBUG = false;

  private TextPaint mPaint;
  private CharSequence mText;
  private int mStart;
  private int mLen;
  private int mDir;
  private Directions mDirections;
  private boolean mHasTabs;
  private TabStops mTabs;
  private char[] mChars;
  private boolean mCharsValid;
  private Spanned mSpanned;
  private final TextPaint mWorkPaint = new TextPaint();
  private final SpanSetCompat<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
      new SpanSetCompat<MetricAffectingSpan>(MetricAffectingSpan.class);
  private final SpanSetCompat<CharacterStyle> mCharacterStyleSpanSet =
      new SpanSetCompat<CharacterStyle>(CharacterStyle.class);
  private final SpanSetCompat<ReplacementSpan> mReplacementSpanSpanSet =
      new SpanSetCompat<ReplacementSpan>(ReplacementSpan.class);

  private static final TextLineImpl23[] sCached = new TextLineImpl23[3];

  /**
   * Returns a new TextLine from the shared pool.
   *
   * @return an uninitialized TextLine
   */
  public static TextLineImpl23 obtain() {
    TextLineImpl23 tl;
    synchronized (sCached) {
      for (int i = sCached.length; --i >= 0; ) {
        if (sCached[i] != null) {
          tl = sCached[i];
          sCached[i] = null;
          return tl;
        }
      }
    }
    tl = new TextLineImpl23();
    if (DEBUG) {
      Log.v("TLINE", "new: " + tl);
    }
    return tl;
  }

  /**
   * Puts a TextLine back into the shared pool. Do not use this TextLine once
   * it has been returned.
   *
   * @param tl the textLine
   * @return null, as a convenience from clearing references to the provided
   * TextLine
   */
  public static TextLineImpl23 recycle(TextLineImpl23 tl) {
    tl.mText = null;
    tl.mPaint = null;
    tl.mDirections = null;
    tl.mSpanned = null;
    tl.mTabs = null;
    tl.mChars = null;

    tl.mMetricAffectingSpanSpanSet.recycle();
    tl.mCharacterStyleSpanSet.recycle();
    tl.mReplacementSpanSpanSet.recycle();

    synchronized (sCached) {
      for (int i = 0; i < sCached.length; ++i) {
        if (sCached[i] == null) {
          sCached[i] = tl;
          break;
        }
      }
    }
    return null;
  }

  /**
   * Initializes a TextLine and prepares it for use.
   *
   * @param paint      the base paint for the line
   * @param text       the text, can be Styled
   * @param start      the start of the line relative to the text
   * @param limit      the limit of the line relative to the text
   * @param dir        the paragraph direction of this line
   * @param directions the directions information of this line
   * @param hasTabs    true if the line might contain tabs
   * @param tabStops   the tabStops. Can be null.
   */
  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
                  Directions directions, boolean hasTabs, TabStops tabStops) {
    mPaint = paint;
    mText = text;
    mStart = start;
    mLen = limit - start;
    mDir = dir;
    mDirections = directions;
    if (mDirections == null) {
      throw new IllegalArgumentException("Directions cannot be null");
    }
    mHasTabs = hasTabs;
    mSpanned = null;

    boolean hasReplacement = false;
    if (text instanceof Spanned) {
      mSpanned = (Spanned) text;
      mReplacementSpanSpanSet.init(mSpanned, start, limit);
      hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
    }

    mCharsValid = hasReplacement || hasTabs || directions != Directions.DIRS_ALL_LEFT_TO_RIGHT;

    if (mCharsValid) {
      if (mChars == null || mChars.length < mLen) {
        mChars = ArrayUtils.newUnpaddedCharArray(mLen);
      }
      TextUtils.getChars(text, start, limit, mChars, 0);
      if (hasReplacement) {
        // Handle these all at once so we don't have to do it as we go.
        // Replace the first character of each replacement run with the
        // object-replacement character and the remainder with zero width
        // non-break space aka BOM.  Cursor movement code skips these
        // zero-width characters.
        char[] chars = mChars;
        for (int i = start, inext; i < limit; i = inext) {
          inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
          if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
            // transition into a span
            chars[i - start] = '\ufffc';
            for (int j = i - start + 1, e = inext - start; j < e; ++j) {
              chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
            }
          }
        }
      }
    }
    mTabs = tabStops;
  }

  /**
   * Renders the TextLine.
   *
   * @param c      the canvas to render on
   * @param x      the leading margin position
   * @param top    the top of the line
   * @param y      the baseline
   * @param bottom the bottom of the line
   */
  public void draw(Canvas c, float x, int top, int y, int bottom) {
    if (!mHasTabs) {
      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {
        drawRun(c, 0, mLen, false, x, top, y, bottom, false);
        return;
      }
      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {
        drawRun(c, 0, mLen, true, x, top, y, bottom, false);
        return;
      }
    }

    float h = 0;
    int[] runs = mDirections.mDirections;

    int lastRunIndex = runs.length - 2;
    for (int i = 0; i < runs.length; i += 2) {
      int runStart = runs[i];
      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
      if (runLimit > mLen) {
        runLimit = mLen;
      }
      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;

      int segstart = runStart;
      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
        int codept = 0;
        if (mHasTabs && j < runLimit) {
          codept = mChars[j];
          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
            codept = Character.codePointAt(mChars, j);
            if (codept > 0xFFFF) {
              ++j;
              continue;
            }
          }
        }

        if (j == runLimit || codept == '\t') {
          h += drawRun(c, segstart, j, runIsRtl, x + h, top, y, bottom,
              i != lastRunIndex || j != mLen);

          if (codept == '\t') {
            h = mDir * nextTab(h * mDir);
          }
          segstart = j + 1;
        }
      }
    }
  }

  /**
   * Returns metrics information for the entire line.
   *
   * @param fmi receives font metrics information, can be null
   * @return the signed width of the line
   */
  public float metrics(FontMetricsInt fmi) {
    return measure(mLen, false, fmi);
  }

  /**
   * Returns information about a position on the line.
   *
   * @param offset   the line-relative character offset, between 0 and the
   *                 line length, inclusive
   * @param trailing true to measure the trailing edge of the character
   *                 before offset, false to measure the leading edge of the character
   *                 at offset.
   * @param fmi      receives metrics information about the requested
   *                 character, can be null.
   * @return the signed offset from the leading margin to the requested
   * character edge.
   */
  float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    int target = trailing ? offset - 1 : offset;
    if (target < 0) {
      return 0;
    }

    float h = 0;

    if (!mHasTabs) {
      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {
        return measureRun(0, offset, mLen, false, fmi);
      }
      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {
        return measureRun(0, offset, mLen, true, fmi);
      }
    }

    char[] chars = mChars;
    int[] runs = mDirections.mDirections;
    for (int i = 0; i < runs.length; i += 2) {
      int runStart = runs[i];
      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
      if (runLimit > mLen) {
        runLimit = mLen;
      }
      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;

      int segstart = runStart;
      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
        int codept = 0;
        if (mHasTabs && j < runLimit) {
          codept = chars[j];
          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
            codept = Character.codePointAt(chars, j);
            if (codept > 0xFFFF) {
              ++j;
              continue;
            }
          }
        }

        if (j == runLimit || codept == '\t') {
          boolean inSegment = target >= segstart && target < j;

          boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
          if (inSegment && advance) {
            return h += measureRun(segstart, offset, j, runIsRtl, fmi);
          }

          float w = measureRun(segstart, j, j, runIsRtl, fmi);
          h += advance ? w : -w;

          if (inSegment) {
            return h += measureRun(segstart, offset, j, runIsRtl, null);
          }

          if (codept == '\t') {
            if (offset == j) {
              return h;
            }
            h = mDir * nextTab(h * mDir);
            if (target == j) {
              return h;
            }
          }

          segstart = j + 1;
        }
      }
    }

    return h;
  }

  /**
   * Draws a unidirectional (but possibly multi-styled) run of text.
   *
   * @param c         the canvas to draw on
   * @param start     the line-relative start
   * @param limit     the line-relative limit
   * @param runIsRtl  true if the run is right-to-left
   * @param x         the position of the run that is closest to the leading margin
   * @param top       the top of the line
   * @param y         the baseline
   * @param bottom    the bottom of the line
   * @param needWidth true if the width value is required.
   * @return the signed width of the run, based on the paragraph direction.
   * Only valid if needWidth is true.
   */
  private float drawRun(Canvas c, int start,
                        int limit, boolean runIsRtl, float x, int top, int y, int bottom,
                        boolean needWidth) {

    if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
      float w = -measureRun(start, limit, limit, runIsRtl, null);
      handleRun(start, limit, limit, runIsRtl, c, x + w, top,
          y, bottom, null, false);
      return w;
    }

    return handleRun(start, limit, limit, runIsRtl, c, x, top,
        y, bottom, null, needWidth);
  }

  /**
   * Measures a unidirectional (but possibly multi-styled) run of text.
   *
   * @param start    the line-relative start of the run
   * @param offset   the offset to measure to, between start and limit inclusive
   * @param limit    the line-relative limit of the run
   * @param runIsRtl true if the run is right-to-left
   * @param fmi      receives metrics information about the requested
   *                 run, can be null.
   * @return the signed width from the start of the run to the leading edge
   * of the character at offset, based on the run (not paragraph) direction
   */
  private float measureRun(int start, int offset, int limit, boolean runIsRtl,
                           FontMetricsInt fmi) {
    return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
  }

  /**
   * Walk the cursor through this line, skipping conjuncts and
   * zero-width characters.
   * <p>
   * <p>This function cannot properly walk the cursor off the ends of the line
   * since it does not know about any shaping on the previous/following line
   * that might affect the cursor position. Callers must either avoid these
   * situations or handle the result specially.
   *
   * @param cursor the starting position of the cursor, between 0 and the
   *               length of the line, inclusive
   * @param toLeft true if the caret is moving to the left.
   * @return the new offset.  If it is less than 0 or greater than the length
   * of the line, the previous/following line should be examined to get the
   * actual offset.
   */
  int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    // 1) The caret marks the leading edge of a character. The character
    // logically before it might be on a different level, and the active caret
    // position is on the character at the lower level. If that character
    // was the previous character, the caret is on its trailing edge.
    // 2) Take this character/edge and move it in the indicated direction.
    // This gives you a new character and a new edge.
    // 3) This position is between two visually adjacent characters.  One of
    // these might be at a lower level.  The active position is on the
    // character at the lower level.
    // 4) If the active position is on the trailing edge of the character,
    // the new caret position is the following logical character, else it
    // is the character.

    int lineStart = 0;
    int lineEnd = mLen;
    boolean paraIsRtl = mDir == -1;
    int[] runs = mDirections.mDirections;

    int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
    boolean trailing = false;

    if (c
Download .txt
gitextract_dtlfnmzy/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── com/
│           │       ├── lsjwzh/
│           │       │   └── test/
│           │       │       ├── AutoScrollHandler.java
│           │       │       ├── Const.java
│           │       │       ├── FastTextLayoutView.java
│           │       │       ├── FastTextView.java
│           │       │       ├── FpsCalculator.java
│           │       │       ├── GhostThread.java
│           │       │       ├── StaticLayoutManager.java
│           │       │       ├── TestSingleLineTextView.java
│           │       │       ├── TestSpan.java
│           │       │       ├── TestStats.java
│           │       │       ├── TestTextView.java
│           │       │       ├── TextLineView.java
│           │       │       └── Util.java
│           │       └── wechat/
│           │           └── testdemo/
│           │               ├── EllipseFragment.java
│           │               ├── FastTextViewListTestFragment.java
│           │               ├── MainActivity.java
│           │               ├── MainActivityFragment.java
│           │               ├── NormalLayoutTestFragment.java
│           │               ├── ReadMoreFragment.java
│           │               ├── ReadMoreListTestFragment.java
│           │               ├── StaticLayoutCacheTestFragment.java
│           │               └── TestListAdapter.java
│           └── res/
│               ├── anim/
│               │   ├── popup_enter.xml
│               │   └── popup_exit.xml
│               ├── drawable/
│               │   └── popup_background.xml
│               ├── layout/
│               │   ├── activity_main.xml
│               │   ├── ellipse_demo.xml
│               │   ├── fast_layout_ui.xml
│               │   ├── fast_list_item.xml
│               │   ├── fragment_main.xml
│               │   ├── layout_cache_demo.xml
│               │   ├── normal_layout_ui.xml
│               │   ├── normal_list_item.xml
│               │   ├── read_more_demo.xml
│               │   ├── readmore_list_item.xml
│               │   ├── spinner_item.xml
│               │   ├── static_layout_ui.xml
│               │   └── static_list_item.xml
│               ├── menu/
│               │   └── menu_main.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-v23/
│                   └── styles.xml
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── text.Textline/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── android/
│                   └── text/
│                       ├── Directions.java
│                       ├── ITextLine.java
│                       ├── TextLineCompat.java
│                       ├── TextLineImpl15.java
│                       └── TextLineImpl23.java
└── widget.FastTextView/
    ├── .gitignore
    ├── build.gradle
    ├── proguard-rules.pro
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   ├── android/
            │   │   └── text/
            │   │       ├── EllipsisSpannedContainer.java
            │   │       ├── LayoutUtils.java
            │   │       ├── SpanSetCompat.java
            │   │       ├── StaticLayoutBuilderCompat.java
            │   │       ├── TextLayoutCache.java
            │   │       └── TextLayoutWarmer.java
            │   └── com/
            │       └── lsjwzh/
            │           └── widget/
            │               └── text/
            │                   ├── ClickableSpanLayoutView.java
            │                   ├── ClickableSpanUtil.java
            │                   ├── FastTextLayoutView.java
            │                   ├── FastTextView.java
            │                   ├── ItalicReplacementSpan.java
            │                   ├── LineUtil.java
            │                   ├── NestReplacementSpan.java
            │                   ├── ReadMoreTextView.java
            │                   ├── SingleLineTextView.java
            │                   ├── StrokableTextView.java
            │                   ├── StrokeReplacementSpan.java
            │                   ├── StrokeSpan.java
            │                   ├── StrokeSpanUtil.java
            │                   ├── TextMeasureUtil.java
            │                   └── TextViewAttrsHelper.java
            └── res/
                └── values/
                    └── attrs.xml
Download .txt
SYMBOL INDEX (407 symbols across 47 files)

FILE: app/src/main/java/com/lsjwzh/test/AutoScrollHandler.java
  class AutoScrollHandler (line 11) | public class AutoScrollHandler {
    method AutoScrollHandler (line 19) | public AutoScrollHandler(ListView listView, int itemCount) {
    method startAutoScrollDown (line 24) | public void startAutoScrollDown(final Callback callback) {
    method startAutoScrollUp (line 30) | public void startAutoScrollUp(final Callback callback) {
    method startPositionAndTrack (line 35) | private void startPositionAndTrack(final int position, final Callback ...
    type Callback (line 58) | public interface Callback {
      method callback (line 59) | void callback(int fps);

FILE: app/src/main/java/com/lsjwzh/test/Const.java
  class Const (line 7) | public class Const {

FILE: app/src/main/java/com/lsjwzh/test/FastTextLayoutView.java
  class FastTextLayoutView (line 16) | public class FastTextLayoutView extends com.lsjwzh.widget.text.Clickable...
    method FastTextLayoutView (line 21) | public FastTextLayoutView(Context context) {
    method FastTextLayoutView (line 25) | public FastTextLayoutView(Context context, @Nullable AttributeSet attr...
    method FastTextLayoutView (line 29) | public FastTextLayoutView(Context context, @Nullable AttributeSet attr...
    method FastTextLayoutView (line 33) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onDraw (line 38) | @Override
    method onMeasure (line 51) | @Override
    method layout (line 64) | @Override

FILE: app/src/main/java/com/lsjwzh/test/FastTextView.java
  class FastTextView (line 16) | public class FastTextView extends com.lsjwzh.widget.text.FastTextView {
    method FastTextView (line 21) | public FastTextView(Context context) {
    method FastTextView (line 25) | public FastTextView(Context context, @Nullable AttributeSet attrs) {
    method FastTextView (line 29) | public FastTextView(Context context, @Nullable AttributeSet attrs, int...
    method FastTextView (line 33) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onDraw (line 38) | @Override
    method onMeasure (line 50) | @Override
    method layout (line 62) | @Override

FILE: app/src/main/java/com/lsjwzh/test/FpsCalculator.java
  class FpsCalculator (line 12) | public class FpsCalculator {
    method instance (line 29) | public static FpsCalculator instance() {
    method startCalculate (line 39) | public void startCalculate() {
    method stopGetAvgFPS (line 45) | public int stopGetAvgFPS() {
    method syncCheckThread (line 53) | private void syncCheckThread(){
    method doFrame (line 72) | @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    method start (line 85) | @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    method stop (line 121) | public void stop() {

FILE: app/src/main/java/com/lsjwzh/test/GhostThread.java
  class GhostThread (line 15) | public class GhostThread  {
    method run (line 24) | @Override
    method start (line 36) | public static void start() {
    method stop (line 49) | public static void stop() {

FILE: app/src/main/java/com/lsjwzh/test/StaticLayoutManager.java
  class StaticLayoutManager (line 18) | public class StaticLayoutManager {
    method initLayout (line 33) | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    method getLayout (line 58) | public StaticLayout getLayout(int index) {
    method getLongStringLayout (line 63) | public StaticLayout getLongStringLayout() {
    method getInstance (line 69) | public static StaticLayoutManager getInstance() {

FILE: app/src/main/java/com/lsjwzh/test/TestSingleLineTextView.java
  class TestSingleLineTextView (line 17) | public class TestSingleLineTextView extends SingleLineTextView {
    method TestSingleLineTextView (line 21) | public TestSingleLineTextView(Context context) {
    method TestSingleLineTextView (line 25) | public TestSingleLineTextView(Context context, @Nullable AttributeSet ...
    method TestSingleLineTextView (line 29) | public TestSingleLineTextView(Context context, @Nullable AttributeSet ...
    method TestSingleLineTextView (line 33) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onDraw (line 39) | @Override
    method onMeasure (line 51) | @Override

FILE: app/src/main/java/com/lsjwzh/test/TestSpan.java
  class TestSpan (line 17) | public class TestSpan {
    method init (line 338) | public static void init(Context context) {
    method getSpanString (line 365) | public static SpannableStringBuilder getSpanString(int index) {
    method getLongSpanString (line 369) | public static SpannableString getLongSpanString() {

FILE: app/src/main/java/com/lsjwzh/test/TestStats.java
  class TestStats (line 8) | public class TestStats {
    method toString (line 20) | @Override
    method reset (line 32) | public void reset() {
    method getDrawCost (line 41) | public long getDrawCost() {
    method getDrawCount (line 45) | public long getDrawCount() {
    method getMeasureCost (line 49) | public long getMeasureCost() {
    method getMeasureCount (line 53) | public long getMeasureCount() {
    method getLayoutCost (line 57) | public long getLayoutCost() {
    method getLayoutCount (line 61) | public long getLayoutCount() {
    method drawStart (line 65) | public void drawStart() {
    method drawEnd (line 69) | public void drawEnd() {
    method measuretart (line 74) | public void measuretart() {
    method measureEnd (line 78) | public void measureEnd() {
    method layoutStart (line 83) | public void layoutStart() {
    method layoutEnd (line 87) | public void layoutEnd() {

FILE: app/src/main/java/com/lsjwzh/test/TestTextView.java
  class TestTextView (line 15) | public class TestTextView extends TextView {
    method TestTextView (line 19) | public TestTextView(Context context) {
    method TestTextView (line 23) | public TestTextView(Context context, @Nullable AttributeSet attrs) {
    method TestTextView (line 27) | public TestTextView(Context context, @Nullable AttributeSet attrs, int...
    method onMeasure (line 31) | @Override
    method onDraw (line 40) | @Override
    method layout (line 50) | @Override

FILE: app/src/main/java/com/lsjwzh/test/Util.java
  class Util (line 10) | public class Util {
    method fromDPtoPix (line 20) | public static float fromDPtoPix(Context context, int dp) {
    method getScreenWidth (line 24) | public static int getScreenWidth(Context context) {

FILE: app/src/main/java/com/wechat/testdemo/EllipseFragment.java
  class EllipseFragment (line 37) | public class EllipseFragment extends Fragment {
    method onCreateView (line 41) | @Override
    method getStaticLayout (line 61) | private StaticLayout getStaticLayout(SpannableStringBuilder spannableS...
    method getSpannable (line 104) | @NonNull

FILE: app/src/main/java/com/wechat/testdemo/FastTextViewListTestFragment.java
  class FastTextViewListTestFragment (line 24) | public class FastTextViewListTestFragment extends Fragment {
    method onCreateView (line 32) | @Nullable
    class ListAdapter (line 82) | private static class ListAdapter extends TestListAdapter {
      method ListAdapter (line 84) | private ListAdapter(Context context) {
      method bindView (line 88) | @Override
      class ViewHolder (line 105) | private class ViewHolder {

FILE: app/src/main/java/com/wechat/testdemo/MainActivity.java
  class MainActivity (line 14) | public class MainActivity extends FragmentActivity {
    method onCreate (line 16) | @Override
    method onBackPressed (line 38) | @Override
    method onCreateOptionsMenu (line 43) | @Override
    method onOptionsItemSelected (line 50) | @Override
    method onDestroy (line 65) | @Override

FILE: app/src/main/java/com/wechat/testdemo/MainActivityFragment.java
  class MainActivityFragment (line 23) | public class MainActivityFragment extends Fragment {
    method onCreateView (line 27) | @Override

FILE: app/src/main/java/com/wechat/testdemo/NormalLayoutTestFragment.java
  class NormalLayoutTestFragment (line 25) | public class NormalLayoutTestFragment extends Fragment {
    method onCreateView (line 34) | @Nullable
    class NormalListAdapter (line 85) | private static class NormalListAdapter extends TestListAdapter {
      method NormalListAdapter (line 87) | private NormalListAdapter(Context context) {
      method bindView (line 91) | @Override
      class ViewHolder (line 110) | private class ViewHolder {

FILE: app/src/main/java/com/wechat/testdemo/ReadMoreFragment.java
  class ReadMoreFragment (line 26) | public class ReadMoreFragment extends Fragment {
    method onCreateView (line 30) | @Override
    method getSpannable (line 68) | @NonNull

FILE: app/src/main/java/com/wechat/testdemo/ReadMoreListTestFragment.java
  class ReadMoreListTestFragment (line 20) | public class ReadMoreListTestFragment extends Fragment {
    method onCreateView (line 28) | @Nullable
    class ListAdapter (line 78) | private static class ListAdapter extends TestListAdapter {
      method ListAdapter (line 80) | private ListAdapter(Context context) {
      method bindView (line 84) | @Override
      class ViewHolder (line 102) | private class ViewHolder {

FILE: app/src/main/java/com/wechat/testdemo/StaticLayoutCacheTestFragment.java
  class StaticLayoutCacheTestFragment (line 19) | public class StaticLayoutCacheTestFragment extends Fragment {
    method onCreateView (line 29) | @Nullable
    class StaticListAdapter (line 79) | private static class StaticListAdapter extends TestListAdapter {
      method StaticListAdapter (line 81) | private StaticListAdapter(Context context) {
      method bindView (line 85) | @Override
      class ViewHolder (line 104) | private class ViewHolder {

FILE: app/src/main/java/com/wechat/testdemo/TestListAdapter.java
  class TestListAdapter (line 11) | public abstract class TestListAdapter extends BaseAdapter {
    method TestListAdapter (line 16) | public TestListAdapter(Context context) {
    method getCount (line 20) | @Override
    method getItem (line 25) | @Override
    method getItemId (line 30) | @Override
    method getView (line 35) | @Override
    method bindView (line 45) | public abstract View bindView(int position, View convertView, ViewGrou...

FILE: text.Textline/src/main/java/android/text/Directions.java
  class Directions (line 7) | public class Directions {
    method Directions (line 21) | Directions(int[] dirs) {

FILE: text.Textline/src/main/java/android/text/ITextLine.java
  type ITextLine (line 7) | public interface ITextLine {
    method set (line 19) | void set(TextPaint paint, CharSequence text, int start, int limit, int...
    method metrics (line 27) | float metrics(Paint.FontMetricsInt fmi);
    method draw (line 38) | void draw(Canvas c, float x, int top, int y, int bottom);

FILE: text.Textline/src/main/java/android/text/TextLineCompat.java
  class TextLineCompat (line 11) | public class TextLineCompat implements ITextLine {
    method TextLineCompat (line 14) | private TextLineCompat(ITextLine textLine) {
    method obtain (line 23) | public static TextLineCompat obtain() {
    method recycle (line 39) | public static void recycle(TextLineCompat tl) {
    method set (line 57) | @Override
    method metrics (line 68) | public float metrics(Paint.FontMetricsInt fmi) {
    method draw (line 72) | @Override

FILE: text.Textline/src/main/java/android/text/TextLineImpl15.java
  class TextLineImpl15 (line 45) | @RequiresApi(15)
    method obtain (line 79) | static TextLineImpl15 obtain() {
    method recycle (line 105) | static TextLineImpl15 recycle(TextLineImpl15 tl) {
    method set (line 137) | public void set(TextPaint paint, CharSequence text, int start, int lim...
    method draw (line 196) | public void draw(Canvas c, float x, int top, int y, int bottom) {
    method metrics (line 270) | public float metrics(FontMetricsInt fmi) {
    method measure (line 287) | float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    method drawRun (line 385) | private float drawRun(Canvas c, int start,
    method measureRun (line 412) | private float measureRun(int start, int offset, int limit, boolean run...
    method getOffsetToLeftRightOf (line 433) | int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    method getOffsetBeforeAfter (line 612) | private int getOffsetBeforeAfter(int runIndex, int runStart, int runLi...
    method expandMetricsFromPaint (line 681) | private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPai...
    method updateMetrics (line 694) | static void updateMetrics(FontMetricsInt fmi, int previousTop, int pre...
    method handleText (line 721) | private float handleText(TextPaint wp, int start, int end,
    method getTextRunAdvances (line 788) | private float getTextRunAdvances(TextPaint wp, int start, int end, int...
    method handleReplacement (line 848) | private float handleReplacement(ReplacementSpan replacement, TextPaint...
    class SpanSetCompat (line 894) | private static class SpanSetCompat<E> {
      method SpanSetCompat (line 902) | SpanSetCompat(Class<? extends E> type) {
      method init (line 907) | @SuppressWarnings("unchecked")
      method hasSpansIntersecting (line 939) | public boolean hasSpansIntersecting(int start, int end) {
      method getNextTransition (line 948) | int getNextTransition(int start, int limit) {
      method recycle (line 958) | public void recycle() {
    method handleRun (line 984) | private float handleRun(int start, int measureLimit,
    method drawTextRun (line 1086) | private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
    method ascent (line 1136) | float ascent(int pos) {
    method nextTab (line 1161) | float nextTab(float h) {
    method idealByteArraySize (line 1171) | public static int idealByteArraySize(int need) {
    method idealBooleanArraySize (line 1179) | public static int idealBooleanArraySize(int need) {
    method idealShortArraySize (line 1183) | public static int idealShortArraySize(int need) {
    method idealCharArraySize (line 1187) | public static int idealCharArraySize(int need) {

FILE: text.Textline/src/main/java/android/text/TextLineImpl23.java
  class TextLineImpl23 (line 41) | @RequiresApi(23)
    method obtain (line 71) | public static TextLineImpl23 obtain() {
    method recycle (line 97) | public static TextLineImpl23 recycle(TextLineImpl23 tl) {
    method set (line 132) | public void set(TextPaint paint, CharSequence text, int start, int lim...
    method draw (line 191) | public void draw(Canvas c, float x, int top, int y, int bottom) {
    method metrics (line 248) | public float metrics(FontMetricsInt fmi) {
    method measure (line 265) | float measure(int offset, boolean trailing, FontMetricsInt fmi) {
    method drawRun (line 354) | private float drawRun(Canvas c, int start,
    method measureRun (line 381) | private float measureRun(int start, int offset, int limit, boolean run...
    method getOffsetToLeftRightOf (line 402) | int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
    method getOffsetBeforeAfter (line 581) | private int getOffsetBeforeAfter(int runIndex, int runStart, int runLi...
    method expandMetricsFromPaint (line 650) | private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPai...
    method updateMetrics (line 663) | static void updateMetrics(FontMetricsInt fmi, int previousTop, int pre...
    method handleText (line 691) | private float handleText(TextPaint wp, int start, int end,
    method handleReplacement (line 781) | private float handleReplacement(ReplacementSpan replacement, TextPaint...
    method handleRun (line 845) | private float handleRun(int start, int measureLimit,
    method drawTextRun (line 947) | private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
    method nextTab (line 965) | float nextTab(float h) {

FILE: widget.FastTextView/src/main/java/android/text/EllipsisSpannedContainer.java
  class EllipsisSpannedContainer (line 11) | public class EllipsisSpannedContainer implements Spanned, GetChars{
    method EllipsisSpannedContainer (line 19) | public EllipsisSpannedContainer(Spanned spanned) {
    method setCustomEllipsisSpan (line 23) | public void setCustomEllipsisSpan(ReplacementSpan customEllipsisSpan) {
    method getEllipsisStart (line 27) | public int getEllipsisStart() {
    method getEllipsisEnd (line 31) | public int getEllipsisEnd() {
    method getSourceSpanned (line 35) | public Spanned getSourceSpanned() {
    method getSpanEnd (line 39) | @Override
    method getSpanFlags (line 47) | @Override
    method getSpanStart (line 55) | @Override
    method getSpans (line 63) | @Override
    method nextSpanTransition (line 86) | @Override
    method length (line 91) | @Override
    method charAt (line 96) | @Override
    method subSequence (line 101) | @Override
    method getChars (line 106) | @Override
    method ellipsize (line 119) | void ellipsize(int start, int end, int line, char[] dest, int destoff) {
    method setLayout (line 148) | public void setLayout(StaticLayout layout) {

FILE: widget.FastTextView/src/main/java/android/text/LayoutUtils.java
  class LayoutUtils (line 5) | public class LayoutUtils {
    method isEllipsizer (line 18) | public static boolean isEllipsizer(CharSequence charSequence) {

FILE: widget.FastTextView/src/main/java/android/text/SpanSetCompat.java
  class SpanSetCompat (line 30) | public class SpanSetCompat<E> {
    method SpanSetCompat (line 39) | SpanSetCompat(Class<? extends E> type) {
    method init (line 44) | @SuppressWarnings("unchecked")
    method hasSpansIntersecting (line 87) | public boolean hasSpansIntersecting(int start, int end) {
    method getNextTransition (line 99) | int getNextTransition(int start, int limit) {
    method recycle (line 112) | public void recycle() {

FILE: widget.FastTextView/src/main/java/android/text/StaticLayoutBuilderCompat.java
  class StaticLayoutBuilderCompat (line 16) | public class StaticLayoutBuilderCompat {
    method StaticLayoutBuilderCompat (line 17) | private StaticLayoutBuilderCompat() {
    method obtain (line 30) | public static StaticLayoutBuilderCompat obtain(CharSequence source, in...
    method finish (line 63) | void finish() {
    method setText (line 69) | public StaticLayoutBuilderCompat setText(CharSequence source) {
    method setText (line 83) | public StaticLayoutBuilderCompat setText(CharSequence source, int star...
    method setPaint (line 96) | public StaticLayoutBuilderCompat setPaint(TextPaint paint) {
    method setWidth (line 107) | public StaticLayoutBuilderCompat setWidth(int width) {
    method setAlignment (line 121) | public StaticLayoutBuilderCompat setAlignment(Layout.Alignment alignme...
    method setTextDirection (line 134) | public StaticLayoutBuilderCompat setTextDirection(TextDirectionHeurist...
    method setLineSpacing (line 148) | public StaticLayoutBuilderCompat setLineSpacing(float spacingAdd, floa...
    method setIncludePad (line 163) | public StaticLayoutBuilderCompat setIncludePad(boolean includePad) {
    method setEllipsizedWidth (line 177) | public StaticLayoutBuilderCompat setEllipsizedWidth(int ellipsizedWidt...
    method setEllipsize (line 194) | public StaticLayoutBuilderCompat setEllipsize(@Nullable TextUtils.Trun...
    method setMaxLines (line 208) | public StaticLayoutBuilderCompat setMaxLines(int maxLines) {
    method setBreakStrategy (line 221) | public StaticLayoutBuilderCompat setBreakStrategy(int breakStrategy) {
    method setHyphenationFrequency (line 234) | public StaticLayoutBuilderCompat setHyphenationFrequency(int hyphenati...
    method setIndents (line 247) | public StaticLayoutBuilderCompat setIndents(int[] leftIndents, int[] r...
    method build (line 262) | public StaticLayout build() {

FILE: widget.FastTextView/src/main/java/android/text/TextLayoutCache.java
  class TextLayoutCache (line 6) | public class TextLayoutCache<T extends Layout> {
    method put (line 11) | public synchronized void put(CharSequence key, T value) {
    method get (line 15) | public synchronized T get(CharSequence key) {
    method remove (line 19) | public synchronized T remove(CharSequence key) {

FILE: widget.FastTextView/src/main/java/android/text/TextLayoutWarmer.java
  class TextLayoutWarmer (line 16) | public class TextLayoutWarmer<T extends Layout> {
    method queue (line 24) | @Override
    method setLayoutFactory (line 32) | public void setLayoutFactory(LayoutFactory<T> layoutFactory) {
    method getLayoutFactory (line 36) | public LayoutFactory<T> getLayoutFactory() {
    method setWarmerExecutor (line 40) | public void setWarmerExecutor(LayoutWarmerExecutor warmerExecutor) {
    method getWarmerExecutor (line 44) | public LayoutWarmerExecutor getWarmerExecutor() {
    method contains (line 48) | public boolean contains(CharSequence text) {
    method getLayout (line 52) | public T getLayout(CharSequence text) {
    method addListener (line 57) | public void addListener(WarmListener<T> listener) {
    method removeListener (line 61) | public void removeListener(WarmListener<T> listener) {
    method getAllListeners (line 65) | public Set<WarmListener<T>> getAllListeners() {
    method addText (line 69) | public void addText(CharSequence text) {
    method removeCache (line 84) | public void removeCache(CharSequence text) {
    type LayoutWarmerExecutor (line 92) | public interface LayoutWarmerExecutor {
      method queue (line 93) | void queue(WarmerTask warmerTask);
    class WarmerTask (line 96) | public static class WarmerTask<T extends Layout> implements Runnable {
      method WarmerTask (line 102) | WarmerTask(CharSequence text, TextLayoutWarmer<T> warmer) {
      method run (line 107) | @Override
    type WarmListener (line 123) | public interface WarmListener<T extends Layout> {
      method onWarmComplete (line 124) | void onWarmComplete(CharSequence text, WarmerTask<T> warmerTask);
    type LayoutFactory (line 127) | public interface LayoutFactory<T extends Layout> {
      method makeLayout (line 128) | T makeLayout(CharSequence text);

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanLayoutView.java
  class ClickableSpanLayoutView (line 15) | public class ClickableSpanLayoutView extends FastTextLayoutView {
    method ClickableSpanLayoutView (line 17) | public ClickableSpanLayoutView(Context context) {
    method ClickableSpanLayoutView (line 21) | public ClickableSpanLayoutView(Context context, @Nullable AttributeSet...
    method ClickableSpanLayoutView (line 25) | public ClickableSpanLayoutView(Context context, @Nullable AttributeSet...
    method ClickableSpanLayoutView (line 29) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onTouchEvent (line 34) | @Override

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanUtil.java
  class ClickableSpanUtil (line 12) | public class ClickableSpanUtil {
    method handleClickableSpan (line 13) | public static boolean handleClickableSpan(View view, Layout layout, Sp...
    method handleClickableSpan (line 50) | public static boolean handleClickableSpan(View view, Layout layout, Sp...
    method getOffsetForHorizontal (line 88) | private static int getOffsetForHorizontal(View view, Layout layout, in...
    type Clickable (line 119) | public interface Clickable {
      method onClick (line 123) | void onClick(View widget);

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextLayoutView.java
  class FastTextLayoutView (line 17) | public class FastTextLayoutView extends View {
    method FastTextLayoutView (line 21) | public FastTextLayoutView(Context context) {
    method FastTextLayoutView (line 25) | public FastTextLayoutView(Context context, @Nullable AttributeSet attr...
    method FastTextLayoutView (line 29) | public FastTextLayoutView(Context context, @Nullable AttributeSet attr...
    method FastTextLayoutView (line 33) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onDraw (line 38) | @Override
    method onMeasure (line 53) | @Override
    method getMeasuredWidth (line 68) | protected int getMeasuredWidth(int size, int measureSpec) {
    method getMeasuredHeight (line 75) | protected int getMeasuredHeight(int size, int measureSpec) {
    method setTextLayout (line 82) | public void setTextLayout(Layout layout) {
    method getTextLayout (line 86) | public Layout getTextLayout() {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextView.java
  class FastTextView (line 39) | public class FastTextView extends FastTextLayoutView {
    method FastTextView (line 52) | public FastTextView(Context context) {
    method FastTextView (line 56) | public FastTextView(Context context, @Nullable AttributeSet attrs) {
    method FastTextView (line 60) | public FastTextView(Context context, @Nullable AttributeSet attrs, int...
    method FastTextView (line 65) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method init (line 72) | private void init(Context context, @Nullable AttributeSet attrs, int d...
    method updateTextColors (line 87) | private void updateTextColors() {
    method drawableStateChanged (line 102) | @Override
    method setLineSpacing (line 110) | public void setLineSpacing(int spacingAdd, float spacingMultiplier) {
    method setTextColor (line 116) | public void setTextColor(ColorStateList colorStateList) {
    method setTextColor (line 121) | public void setTextColor(@ColorInt int color) {
    method getTextColors (line 134) | public final ColorStateList getTextColors() {
    method getCurrentTextColor (line 143) | @ColorInt
    method onTouchEvent (line 148) | @Override
    method onMeasure (line 174) | @Override
    method shouldResetStaticLayout (line 209) | protected boolean shouldResetStaticLayout(int width, CharSequence text...
    method onDraw (line 216) | @Override
    method getInnerWidth (line 258) | public int getInnerWidth() {
    method getInnerHeight (line 262) | private int getInnerHeight() {
    method getPaint (line 266) | public TextPaint getPaint() {
    method getTextPaint (line 270) | @Deprecated
    method getText (line 275) | public CharSequence getText() {
    method setText (line 279) | public void setText(@android.annotation.NonNull CharSequence text) {
    method clearTextLayout (line 286) | private void clearTextLayout() {
    method clearTextLayout (line 290) | private void clearTextLayout(boolean cleanCache) {
    method getGravity (line 305) | public int getGravity() {
    method setGravity (line 316) | public void setGravity(int gravity) {
    method getMaxWidth (line 322) | public int getMaxWidth() {
    method setMaxWidth (line 326) | public void setMaxWidth(int width) {
    method getMaxLines (line 333) | public int getMaxLines() {
    method setMaxLines (line 337) | public void setMaxLines(int maxLines) {
    method setTextSize (line 351) | public void setTextSize(float textSize, int unit) {
    method getTextSize (line 360) | public float getTextSize() {
    method setTextSize (line 364) | public void setTextSize(float textSize) {
    method getEllipsize (line 368) | public int getEllipsize() {
    method setEllipsize (line 372) | public void setEllipsize(int ellipsize) {
    method getCustomEllipsisSpan (line 379) | public ReplacementSpan getCustomEllipsisSpan() {
    method setCustomEllipsisSpan (line 383) | public void setCustomEllipsisSpan(ReplacementSpan customEllipsisSpan) {
    method compressText (line 387) | public void compressText(boolean enable) {
    method addLinks (line 391) | public void addLinks(int mask) {
    method makeLayout (line 396) | @NonNull
    method createStaticLayoutBuilder (line 476) | protected StaticLayoutBuilderCompat createStaticLayoutBuilder(CharSequ...
    method beforeStaticLayoutBuild (line 482) | protected void beforeStaticLayoutBuild(StaticLayoutBuilderCompat layou...
    method getContentWidth (line 486) | protected int getContentWidth(CharSequence text) {
    method getTruncateAt (line 496) | protected TextUtils.TruncateAt getTruncateAt() {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ItalicReplacementSpan.java
  class ItalicReplacementSpan (line 16) | public class ItalicReplacementSpan extends ReplacementSpan {
    method ItalicReplacementSpan (line 21) | public ItalicReplacementSpan() {
    method ItalicReplacementSpan (line 26) | public ItalicReplacementSpan(float textSkewX) {
    method draw (line 30) | @Override
    method getSize (line 54) | @Override
    method setHeightIfNeed (line 64) | private void setHeightIfNeed(CharSequence text, @IntRange(from = 0) in...

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/LineUtil.java
  class LineUtil (line 6) | public class LineUtil {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/NestReplacementSpan.java
  class NestReplacementSpan (line 6) | public abstract class NestReplacementSpan extends ReplacementSpan {
    method isNestSpanEnabled (line 10) | public boolean isNestSpanEnabled() {
    method setNestSpanEnabled (line 14) | public void setNestSpanEnabled(boolean nestSpanEnabled) {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ReadMoreTextView.java
  class ReadMoreTextView (line 24) | public class ReadMoreTextView extends FastTextView {
    method ReadMoreTextView (line 33) | public ReadMoreTextView(Context context) {
    method ReadMoreTextView (line 37) | public ReadMoreTextView(Context context, @Nullable AttributeSet attrs) {
    method ReadMoreTextView (line 41) | public ReadMoreTextView(Context context, @Nullable AttributeSet attrs,...
    method ReadMoreTextView (line 46) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method setText (line 53) | @Override
    method onMeasure (line 63) | @Override
    method onDraw (line 99) | @Override
    method makeLayout (line 117) | @NonNull
    method createStaticLayoutBuilder (line 142) | @Override
    method beforeStaticLayoutBuild (line 149) | @Override
    method createAllStaticLayoutBuilder (line 154) | protected StaticLayoutBuilderCompat createAllStaticLayoutBuilder(CharS...
    method createEllipsisStaticLayoutBuilder (line 160) | protected StaticLayoutBuilderCompat createEllipsisStaticLayoutBuilder(...
    method beforeAllStaticLayoutBuild (line 167) | protected void beforeAllStaticLayoutBuild(StaticLayoutBuilderCompat la...
    method beforeEllipsisStaticLayoutBuild (line 171) | protected void beforeEllipsisStaticLayoutBuild(StaticLayoutBuilderComp...
    method showAll (line 176) | public void showAll() {
    method showEllipsis (line 182) | public void showEllipsis() {
    method setCustomCollapseSpan (line 188) | public void setCustomCollapseSpan(ReplacementSpan collapseSpan) {
    method isShowAll (line 192) | public boolean isShowAll() {
    class EllipsisSpan (line 196) | public static class EllipsisSpan extends ReplacementSpan implements Cl...
      method EllipsisSpan (line 199) | public EllipsisSpan(String text) {
      method draw (line 203) | @Override
      method getSize (line 210) | @Override
      method onClick (line 216) | @Override

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/SingleLineTextView.java
  class SingleLineTextView (line 23) | public class SingleLineTextView extends FastTextLayoutView {
    method SingleLineTextView (line 28) | public SingleLineTextView(Context context) {
    method SingleLineTextView (line 32) | public SingleLineTextView(Context context, @Nullable AttributeSet attr...
    method SingleLineTextView (line 36) | public SingleLineTextView(Context context, @Nullable AttributeSet attr...
    method SingleLineTextView (line 41) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method init (line 47) | private void init(Context context, @Nullable AttributeSet attrs, int d...
    method getPaint (line 55) | public TextPaint getPaint() {
    method setGravity (line 66) | public void setGravity(int gravity) {
    method getGravity (line 77) | public int getGravity() {
    method setMaxWidth (line 83) | public void setMaxWidth(int width) {
    method getMaxWidth (line 90) | public int getMaxWidth() {
    method setMaxLines (line 94) | public void setMaxLines(int maxLines) {
    method getMaxLines (line 101) | public int getMaxLines() {
    method setTextSize (line 105) | public void setTextSize(float textSize) {
    method setTextSize (line 116) | public void setTextSize(float textSize, int unit) {
    method getTextSize (line 122) | public float getTextSize() {
    method getEllipsize (line 126) | public int getEllipsize() {
    method setEllipsize (line 130) | public void setEllipsize(int ellipsize) {
    method setText (line 137) | public void setText(CharSequence text) {
    method onDraw (line 141) | @Override
    method onMeasure (line 146) | @Override

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokableTextView.java
  class StrokableTextView (line 12) | public class StrokableTextView extends FastTextView {
    method StrokableTextView (line 15) | public StrokableTextView(Context context) {
    method StrokableTextView (line 19) | public StrokableTextView(Context context, @Nullable AttributeSet attrs) {
    method StrokableTextView (line 23) | public StrokableTextView(Context context, @Nullable AttributeSet attrs...
    method StrokableTextView (line 27) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onDraw (line 33) | @Override

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeReplacementSpan.java
  class StrokeReplacementSpan (line 11) | public class StrokeReplacementSpan extends ReplacementSpan {
    method StrokeReplacementSpan (line 17) | public StrokeReplacementSpan(int textColor, int strokeColor, int strok...
    method draw (line 23) | @Override
    method getSize (line 38) | @Override

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpan.java
  class StrokeSpan (line 10) | public class StrokeSpan extends CharacterStyle {
    method updateDrawState (line 16) | @Override
    method StrokeSpan (line 29) | public StrokeSpan(int textColor, int strokeColor, int strokeWidth) {
    method startStroke (line 35) | public void startStroke() {
    method endStroke (line 39) | public void endStroke() {
    method isStrokeDrawing (line 43) | public boolean isStrokeDrawing() {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpanUtil.java
  class StrokeSpanUtil (line 5) | public class StrokeSpanUtil {
    method getStrokeSpans (line 7) | public static StrokeSpan[] getStrokeSpans(Spanned spanned) {
    method startStroke (line 11) | public static void startStroke(StrokeSpan[] strokeSpans) {
    method endStroke (line 19) | public static void endStroke(StrokeSpan[] strokeSpans) {

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextMeasureUtil.java
  class TextMeasureUtil (line 19) | public class TextMeasureUtil {
    method recursiveGetSizeWithReplacementSpan (line 31) | public int recursiveGetSizeWithReplacementSpan(CharSequence text, Repl...
    method getTextBounds (line 57) | public static void getTextBounds(Paint paint, CharSequence text, @IntR...
    method getSortedReplacementSpans (line 63) | public static List<ReplacementSpan> getSortedReplacementSpans(final Sp...

FILE: widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextViewAttrsHelper.java
  class TextViewAttrsHelper (line 18) | public class TextViewAttrsHelper {
    method init (line 29) | public void init(Context context, @Nullable AttributeSet attrs, int de...
    method setGravity (line 75) | public boolean setGravity(int gravity) {
    method getGravity (line 103) | public int getGravity() {
    method getLayoutAlignment (line 107) | public static Layout.Alignment getLayoutAlignment(View view, int gravi...
    method getAlignmentByGravity (line 144) | private static Layout.Alignment getAlignmentByGravity(int gravity) {
Condensed preview — 91 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (319K chars).
[
  {
    "path": ".gitignore",
    "chars": 118,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 4121,
    "preview": "# FastTextView\n\nFastTextView is faster than Android TextView.\nFastTextView use StaticLayout to render Spanned String,\nso"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 1288,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion \"28.0.3\"\n    defaultCo"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 926,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 788,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          pa"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/AutoScrollHandler.java",
    "chars": 1763,
    "preview": "package com.lsjwzh.test;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.widget.AbsListView;\nimport"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/Const.java",
    "chars": 134,
    "preview": "package com.lsjwzh.test;\n\n/**\n * Created by wenye on 2017/11/6.\n */\n\npublic class Const {\n  public final static int LOOP"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FastTextLayoutView.java",
    "chars": 2154,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimpor"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FastTextView.java",
    "chars": 2042,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimpor"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FpsCalculator.java",
    "chars": 2727,
    "preview": "package com.lsjwzh.test;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.util.Log;\nimport "
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/GhostThread.java",
    "chars": 1551,
    "preview": "package com.lsjwzh.test;\n\n\nimport android.util.Log;\n\nimport java.util.Random;\n\n/**\n * Created by ragnarok on 15/7/22.\n *"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/StaticLayoutManager.java",
    "chars": 2206,
    "preview": "package com.lsjwzh.test;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.C"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestSingleLineTextView.java",
    "chars": 1777,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimpor"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestSpan.java",
    "chars": 28799,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.BitmapFactory;\nimport android.text.Spa"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestStats.java",
    "chars": 1886,
    "preview": "package com.lsjwzh.test;\n\nimport android.os.SystemClock;\n\n/**\n * Created by wenye on 2017/12/15.\n */\npublic class TestSt"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestTextView.java",
    "chars": 1568,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.SystemClock;"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TextLineView.java",
    "chars": 2784,
    "preview": "//package com.lsjwzh.test;\n//\n//import android.content.Context;\n//import android.graphics.Canvas;\n//import android.graph"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/Util.java",
    "chars": 867,
    "preview": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.view.WindowManag"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/EllipseFragment.java",
    "chars": 5046,
    "preview": "package com.wechat.testdemo;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Pai"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/FastTextViewListTestFragment.java",
    "chars": 3539,
    "preview": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.SystemClock;\ni"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/MainActivity.java",
    "chars": 2015,
    "preview": "package com.wechat.testdemo;\n\nimport android.os.Bundle;\nimport android.support.design.widget.FloatingActionButton;\nimpor"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/MainActivityFragment.java",
    "chars": 5775,
    "preview": "package com.wechat.testdemo;\n\nimport android.graphics.Paint;\nimport android.os.Bundle;\nimport android.support.v4.app.Fra"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/NormalLayoutTestFragment.java",
    "chars": 3659,
    "preview": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.SystemClock;\ni"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/ReadMoreFragment.java",
    "chars": 2767,
    "preview": "package com.wechat.testdemo;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Pai"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/ReadMoreListTestFragment.java",
    "chars": 3429,
    "preview": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.support.annotatio"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/StaticLayoutCacheTestFragment.java",
    "chars": 3677,
    "preview": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.support.annotatio"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/TestListAdapter.java",
    "chars": 1040,
    "preview": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.SystemClock;\nimport android.view.View;\ni"
  },
  {
    "path": "app/src/main/res/anim/popup_enter.xml",
    "chars": 947,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2014 The Android Open Source Project\n\n     Licensed under the "
  },
  {
    "path": "app/src/main/res/anim/popup_exit.xml",
    "chars": 980,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2013 The Android Open Source Project\n\n     Licensed under the "
  },
  {
    "path": "app/src/main/res/drawable/popup_background.xml",
    "chars": 239,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\""
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 1441,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.design.widget.CoordinatorLayout\n  xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/layout/ellipse_demo.xml",
    "chars": 1535,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:"
  },
  {
    "path": "app/src/main/res/layout/fast_layout_ui.xml",
    "chars": 983,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_p"
  },
  {
    "path": "app/src/main/res/layout/fast_list_item.xml",
    "chars": 347,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.FastTextView\n\txmlns:android=\"http://schemas.android.com/apk/res/"
  },
  {
    "path": "app/src/main/res/layout/fragment_main.xml",
    "chars": 2209,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmln"
  },
  {
    "path": "app/src/main/res/layout/layout_cache_demo.xml",
    "chars": 1424,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:"
  },
  {
    "path": "app/src/main/res/layout/normal_layout_ui.xml",
    "chars": 983,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_p"
  },
  {
    "path": "app/src/main/res/layout/normal_list_item.xml",
    "chars": 408,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.TestTextView xmlns:android=\"http://schemas.android.com/apk/res/a"
  },
  {
    "path": "app/src/main/res/layout/read_more_demo.xml",
    "chars": 1098,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:"
  },
  {
    "path": "app/src/main/res/layout/readmore_list_item.xml",
    "chars": 322,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.widget.text.ReadMoreTextView\n\txmlns:android=\"http://schemas.android.c"
  },
  {
    "path": "app/src/main/res/layout/spinner_item.xml",
    "chars": 457,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        "
  },
  {
    "path": "app/src/main/res/layout/static_layout_ui.xml",
    "chars": 986,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_p"
  },
  {
    "path": "app/src/main/res/layout/static_list_item.xml",
    "chars": 249,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.FastTextLayoutView\n\txmlns:android=\"http://schemas.android.com/ap"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "chars": 395,
    "preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-aut"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 202,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#3F51B5</color>\n  <color name=\"colorPrim"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 65,
    "preview": "<resources>\n  <dimen name=\"fab_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 657,
    "preview": "<resources>\n  <string name=\"app_name\">testdemo</string>\n  <string name=\"action_settings\">Settings</string>\n  <string nam"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 1060,
    "preview": "<resources>\n\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n "
  },
  {
    "path": "app/src/main/res/values-v23/styles.xml",
    "chars": 1218,
    "preview": "<resources>\n\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n "
  },
  {
    "path": "build.gradle",
    "chars": 1092,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    r"
  },
  {
    "path": "gradle.properties",
    "chars": 730,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
  },
  {
    "path": "gradlew",
    "chars": 4971,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2314,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
  },
  {
    "path": "settings.gradle",
    "chars": 39,
    "preview": "include ':app', ':widget.FastTextView'\n"
  },
  {
    "path": "text.Textline/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "text.Textline/build.gradle",
    "chars": 571,
    "preview": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 25\n    buildToolsVersion \"25.0.3\"\n\n    defaultConfi"
  },
  {
    "path": "text.Textline/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "text.Textline/src/main/AndroidManifest.xml",
    "chars": 107,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.lsjwzh.text\"/>\n"
  },
  {
    "path": "text.Textline/src/main/java/android/text/Directions.java",
    "chars": 1405,
    "preview": "package android.text;\n\n/**\n * Stores information about bidirectional (left-to-right or right-to-left)\n * text within the"
  },
  {
    "path": "text.Textline/src/main/java/android/text/ITextLine.java",
    "chars": 1245,
    "preview": "package android.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.text.Layout.TabStop"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineCompat.java",
    "chars": 2169,
    "preview": "package android.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport an"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineImpl15.java",
    "chars": 42383,
    "preview": "/*\n * Copyright (C) 2010 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineImpl23.java",
    "chars": 35367,
    "preview": "/*\n * Copyright (C) 2010 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "widget.FastTextView/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "widget.FastTextView/build.gradle",
    "chars": 6825,
    "preview": "apply plugin: 'com.android.library'\n//apply plugin: 'com.github.dcendents.android-maven'\napply plugin: 'com.jfrog.bintra"
  },
  {
    "path": "widget.FastTextView/proguard-rules.pro",
    "chars": 926,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
  },
  {
    "path": "widget.FastTextView/src/main/AndroidManifest.xml",
    "chars": 92,
    "preview": "<manifest package=\"com.lsjwzh.widget.text\">\n\n  <application>\n\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/EllipsisSpannedContainer.java",
    "chars": 4168,
    "preview": "package android.text;\n\nimport android.text.style.ReplacementSpan;\n\nimport java.lang.reflect.Array;\nimport java.util.Arra"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/LayoutUtils.java",
    "chars": 477,
    "preview": "package android.text;\n\nimport android.util.Log;\n\npublic class LayoutUtils {\n\n  static Class sEllipsizerClazz;\n\n  static "
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/SpanSetCompat.java",
    "chars": 3918,
    "preview": "/*\n * Copyright (C) 2012 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/StaticLayoutBuilderCompat.java",
    "chars": 10497,
    "preview": "package android.text;\n\nimport android.annotation.Nullable;\nimport android.os.Build;\nimport android.support.v4.util.Pools"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/TextLayoutCache.java",
    "chars": 564,
    "preview": "package android.text;\n\nimport java.util.Map;\nimport java.util.WeakHashMap;\n\npublic class TextLayoutCache<T extends Layou"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/TextLayoutWarmer.java",
    "chars": 3648,
    "preview": "package android.text;\n\nimport android.graphics.Canvas;\n\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nim"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanLayoutView.java",
    "chars": 1420,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.support.annotat"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanUtil.java",
    "chars": 3780,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.text.Layout;\nimport android.text.Selection;\nimport android.text.Spannabl"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextLayoutView.java",
    "chars": 2666,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextView.java",
    "chars": 16816,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.annotation.ColorInt;\nimport android.content.Context;\nimport android.cont"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ItalicReplacementSpan.java",
    "chars": 3011,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics."
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/LineUtil.java",
    "chars": 101,
    "preview": "package com.lsjwzh.widget.text;\n\n/**\n * Split spannable to multi line.\n */\npublic class LineUtil {\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/NestReplacementSpan.java",
    "chars": 384,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.text.style.ReplacementSpan;\n\n// TODO\npublic abstract class NestReplaceme"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ReadMoreTextView.java",
    "chars": 8556,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/SingleLineTextView.java",
    "chars": 4507,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokableTextView.java",
    "chars": 1562,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeReplacementSpan.java",
    "chars": 2070,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics."
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpan.java",
    "chars": 1055,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Paint;\nimport android.text.TextPaint;\nimport android.text.style"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpanUtil.java",
    "chars": 646,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.text.Spanned;\n\npublic class StrokeSpanUtil {\n\n  public static StrokeSpan"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextMeasureUtil.java",
    "chars": 2739,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.support.ann"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextViewAttrsHelper.java",
    "chars": 5582,
    "preview": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport andro"
  },
  {
    "path": "widget.FastTextView/src/main/res/values/attrs.xml",
    "chars": 182,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <declare-styleable name=\"FastTextView\">\n    <attr name=\"enableLayou"
  }
]

About this extraction

This page contains the full source code of the lsjwzh/FastTextView GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 91 files (290.4 KB), approximately 83.3k tokens, and a symbol index with 407 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!