[
  {
    "path": ".gitignore",
    "content": "*.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",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# FastTextView\n\nFastTextView is faster than Android TextView.\nFastTextView use StaticLayout to render Spanned String,\nso it support most features of Android TextView.\n\nInspired by :\n\nhttps://engineering.instagram.com/improving-comment-rendering-on-android-a77d5db3d82e\n\nhttp://ragnraok.github.io/textview-pre-render-research.html\n\n## Features\n1.Faster than Android TextView\n\n2.More flexibility\n\n3.Support Stroke Text\n(More accurate method to measure stroke text and italic text)\n\n4.Correct Ellipsis handle with ImageSpan\n\n![](ellipsis.png)\n\n5.Custom Read More Support\n\n![](readmore.gif)\n\n\n## FastTextView vs Android TextView\nRendering a SpannableString of 389 chars with ClickableSpan and ImageSpan.\nCall 'onMeasure'、'onDraw' 1000 times.\nHere are the test results on MI MAX (Android 6.0.1):\n\n```\nD/FastTextLayoutView: FastTextLayoutView onMeasure cost:0\nD/FastTextView: FastTextView onMeasure cost:1\nD/TestTextView: TestTextView measure cost:104\nD/FastTextLayoutView: FastTextLayoutView onDraw cost:271\nD/FastTextView: FastTextView onDraw cost:250\nD/TestTextView: TestTextView onDraw cost:249\n```\nYou can see FastTextView's 'onMeasure' almost no time consuming.\n\n## Basic Usage\n```\nrepositories {\n    ...\n    jcenter()\n    ...\n}\n\ndependencies {\n    ...\n    compile 'com.lsjwzh.widget:FastTextView:1.2.15'\n    ...\n}\n```\njava code\n```\n    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);\n    fastTextView.setText(spannableString);\n```\n\n## Advance Usage\n### Use Layout directly\njava code\n```\n    TextPaint textPaint = new TextPaint();\n    textPaint.setAntiAlias(true);\n    textPaint.setColor(Color.WHITE);\n    float textSize = TypedValue.applyDimension(\n            TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics());\n    textPaint.setTextSize(textSize);\n    FastTextLayoutView fastTextLayoutView = (FastTextLayoutView) mRootView.findViewById(R.id.fast_tv);\n    int width = Layout.getDesiredWidth(spannableStringBuilder, textPaint);\n\n    StaticLayout layout = new StaticLayout(spannableStringBuilder, textPaint,\n            Math.min(width, getResources().getDisplayMetrics().widthPixels), Layout.Alignment.ALIGN_NORMAL,\n            1.0f, 0.0f, true);\n    fastTextLayoutView.setTextLayout(layout);\n```\n\n### Single Line Stroke Text\njava code\n```\n    ... ...\n    StrokeSpan strokeSpan = new StrokeSpan(Color.BLUE, Color.YELLOW, 20);\n    spannableStringBuilder.setSpan(strokeSpan, 0, spannableStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);\n    fastTextView.setText(spannableStringBuilder);\n```\n\n### Read More\n```\n\n    ReadMoreTextView readMoreTextView = (ReadMoreTextView) mRootView.findViewById(R.id.readmore_tv);\n    readMoreTextView.setText(spannableStringBuilder);\n    readMoreTextView.setCustomEllipsisSpan(new ReadMoreTextView.EllipsisSpan(\"  Read More\"));\n    readMoreTextView.setCustomCollapseSpan(new ReadMoreTextView.EllipsisSpan(\"  Collapse\"));\n```\n\n# Build Project\nhttps://github.com/anggrayudi/android-hidden-api\n\nYou should use 'android-hidden-api' to build project.\n\n1.Go to <SDK location>/platforms/.\n\n2.Copy, paste and replace the downloaded hidden API file into this directory, e.g. android-25/android.jar.\n\n3.Change compileSdkVersion and targetSdkVersion to 25 (for example).\n\n4.Finally, rebuild your project.\n\n# TODO List\n<del>1.Layout Cache (For List Scene)</del>\n\n<del>2.ColorState background</del>\n\n<del>3.ReadMore</del>\n\n<del>4.Ellipsis</del>\n\n5.AutoSize\n\n<del>6.ColorState textColor</del>\n\n\n# License\n```\nCopyright 2017 lsjwzh\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion \"28.0.3\"\n    defaultConfig {\n        applicationId \"com.wechat.testdemo\"\n        minSdkVersion 15\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    lintOptions {\n        abortOnError false\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n    implementation 'com.android.support.constraint:constraint-layout:1.1.3'\n    implementation 'com.android.support:design:28.0.0'\n    implementation project(':widget.FastTextView')\n//    compile project(':text.Textline')\n//    compile 'com.lsjwzh.widget:FastTextView:0.9.3'\n\n}\n\ntask cleanAndroidMock(type: Delete) {\n    description = 'Deletes the mockable Android jar'\n\n    delete fileTree(\"${project.buildDir}/generated\") {\n        include 'mockable-android*.jar'\n    }\n}\n\nproject.afterEvaluate {\n    tasks['createMockableJar'].dependsOn cleanAndroidMock\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/wenye/android_sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.wechat.testdemo\">\n\n  <application\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:roundIcon=\"@mipmap/ic_launcher_round\"\n    android:supportsRtl=\"true\"\n    android:hardwareAccelerated=\"false\"\n    android:theme=\"@style/AppTheme\">\n    <activity\n      android:name=\".MainActivity\"\n      android:label=\"@string/app_name\"\n      android:theme=\"@style/AppTheme.NoActionBar\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n  </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/AutoScrollHandler.java",
    "content": "package com.lsjwzh.test;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.widget.AbsListView;\nimport android.widget.ListView;\n\n/**\n * Created by ragnarok on 15/7/22.\n */\npublic class AutoScrollHandler {\n\n  private ListView listView;\n\n  private int itemCount;\n\n  private Handler uiHandler = new Handler(Looper.getMainLooper());\n\n  public AutoScrollHandler(ListView listView, int itemCount) {\n    this.listView = listView;\n    this.itemCount = itemCount;\n  }\n\n  public void startAutoScrollDown(final Callback callback) {\n    FpsCalculator.instance().startCalculate();\n    final int position = listView.getAdapter().getCount() - 1;\n    startPositionAndTrack(position, callback);\n  }\n\n  public void startAutoScrollUp(final Callback callback) {\n    FpsCalculator.instance().startCalculate();\n    startPositionAndTrack(0, callback);\n  }\n\n  private void startPositionAndTrack(final int position, final Callback callback) {\n    FpsCalculator.instance().startCalculate();\n    listView.smoothScrollToPosition(position);\n    listView.setOnScrollListener(new AbsListView.OnScrollListener() {\n      @Override\n      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int\n          totalItemCount) {\n\n      }\n\n      @Override\n      public void onScrollStateChanged(AbsListView view, int scrollState) {\n        if (scrollState == SCROLL_STATE_IDLE\n            && (position == 0 ? listView.getFirstVisiblePosition() == 0 :\n            listView.getLastVisiblePosition() == position)) {\n          listView.setOnScrollListener(null);\n          final int fps = FpsCalculator.instance().stopGetAvgFPS();\n          callback.callback(fps);\n        }\n      }\n    });\n  }\n\n  public interface Callback {\n    void callback(int fps);\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/Const.java",
    "content": "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_COUNT = 1;\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FastTextLayoutView.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.os.SystemClock;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.Px;\nimport android.support.annotation.RequiresApi;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\n/**\n * A custom view for rendering layout directly.\n */\npublic class FastTextLayoutView extends com.lsjwzh.widget.text.ClickableSpanLayoutView {\n  private static final String TAG = FastTextLayoutView.class.getSimpleName();\n  private boolean mIsDebug = false;\n  public static final TestStats TEST_STATS = new TestStats();\n\n  public FastTextLayoutView(Context context) {\n    super(context);\n  }\n\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    TEST_STATS.drawStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      // TODO for test\n      super.onDraw(canvas);\n    }\n    TEST_STATS.drawEnd();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onDraw cost:\" + TEST_STATS.getDrawCost());\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    TEST_STATS.measuretart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      // TODO for test\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    TEST_STATS.measureEnd();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onMeasure cost:\" + TEST_STATS.getMeasureCost());\n    }\n  }\n\n  @Override\n  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {\n    TEST_STATS.layoutStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      // TODO for test\n      super.layout(l, t, r, b);\n    }\n    TEST_STATS.layoutEnd();\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FastTextView.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.os.SystemClock;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.Px;\nimport android.support.annotation.RequiresApi;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\n/**\n * Simple and Fast TextView.\n */\npublic class FastTextView extends com.lsjwzh.widget.text.FastTextView {\n  private static final String TAG = FastTextView.class.getSimpleName();\n  private boolean mIsDebug = false;\n  public static final TestStats TEST_STATS = new TestStats();\n\n  public FastTextView(Context context) {\n    super(context);\n  }\n\n  public FastTextView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    TEST_STATS.drawStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onDraw(canvas);\n    }\n    TEST_STATS.drawEnd();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onDraw cost:\" + TEST_STATS.getDrawCost());\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    TEST_STATS.measuretart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    TEST_STATS.measureEnd();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onMeasure cost:\" + TEST_STATS.getMeasureCost());\n    }\n  }\n\n  @Override\n  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {\n    TEST_STATS.layoutStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      // TODO for test\n      super.layout(l, t, r, b);\n    }\n    TEST_STATS.layoutEnd();\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/FpsCalculator.java",
    "content": "package com.lsjwzh.test;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.util.Log;\nimport android.view.Choreographer;\nimport android.view.Choreographer.FrameCallback;\n\nimport java.lang.reflect.Field;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class FpsCalculator {\n\n\tprivate final static String TAG = \"FpsCalculator\";\n\t\n    private long mFrameIntervalNanos;\n    \n    private boolean mRunning = false;\n    \n    private static FpsCalculator instance;\n\n\tprivate AtomicInteger atom = new AtomicInteger(0);\n\tprivate Thread syncCheckThread = null;\n    \n    static {\n    \tinstance = new FpsCalculator();\n    }\n    \n    public static FpsCalculator instance() {\n    \treturn instance;\n    }\n\t\n\tprivate int totalFps;\n\tprivate int fpsCalculateCount;\n\tprivate boolean isCalculatingFPS;\n\t\n\t// calculate the average fps\n\t\n\tpublic void startCalculate() {\n\t\ttotalFps = 0;\n\t\tfpsCalculateCount = 0;\n\t\tisCalculatingFPS = true;\n\t}\n\t\n\tpublic int stopGetAvgFPS() {\n\t\tisCalculatingFPS = false;\n\t\tint avgFPS = totalFps / fpsCalculateCount;\n\t\ttotalFps = 0;\n\t\tfpsCalculateCount = 0;\n\t\treturn avgFPS;\n\t}\n\n\tprivate void syncCheckThread(){\n\t\tif(!mRunning){\n\t\t\treturn;\n\t\t}\n\t\tint val = atom.getAndSet(0);\n\t\tif (isCalculatingFPS) {\n\t\t\ttotalFps += val;\n\t\t\tfpsCalculateCount++;\n\t\t}\n\t\tandroid.util.Log.i(TAG, \"FPS: \" + val);\n\t\ttry {\n\t\t\tThread.sleep(1000);\n\t\t} catch (InterruptedException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\t    \n\tprivate FrameCallback frameCallback = new FrameCallback() {\n\n\t\t@TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n\t\t@Override\n\t\tpublic void doFrame(long frameTimeNanos) {\n\t\t\t\n\t\t\tif (!mRunning) {\n\t\t\t\treturn;\n\t\t\t}\n            \n            Choreographer.getInstance().postFrameCallback(frameCallback);\n\n\t\t\tatom.incrementAndGet();\n\t\t}};\n\t\n\t@TargetApi(Build.VERSION_CODES.JELLY_BEAN)\n\tpublic void start() {\n\t\tLog.d(TAG, \"start vsync detect\");\n\t\tif (mRunning) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tmRunning = true;\n\n\t\tsyncCheckThread = new Thread(new Runnable() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\tfor (;;) {\n\t\t\t\t\tif (!mRunning) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tsyncCheckThread();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tsyncCheckThread.start();\n\t\t\n\t\tChoreographer chor = Choreographer.getInstance();\n\t\tField field;\n\t\ttry {\n\t\t\tfield = chor.getClass().getDeclaredField(\"mFrameIntervalNanos\");\n\t\t\tfield.setAccessible(true);\n\t\t\tmFrameIntervalNanos = field.getLong(chor);\n\t\t\tLog.d(TAG, \"mFrameIntervalNanos \" + mFrameIntervalNanos);\n\t\t} catch (Exception e) {\n\t\t\tLog.e(TAG, \"error: \" + e.getMessage());\n\t\t}\n\t\tchor.postFrameCallback(frameCallback);\n\n\t}\n\t\n\tpublic void stop() {\n\t\tmRunning = false;\n\t\tif (syncCheckThread != null) {\n\t\t\ttry {\n\t\t\t\tsyncCheckThread.join();\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/GhostThread.java",
    "content": "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 * The GhostThread is used to simulate the heavily jobs behind the UI thread in the \n * real world apps\n * Currently, the implementation is work by launch several threads and calculate floating point\n * multiplication infinity\n */\npublic class GhostThread  {\n    private static final String TAG = \"GhostThread\";\n    \n    private static boolean isStart = false;\n    \n    private static Thread[] threads = new Thread[0];\n\n    private static Random random = new Random();\n    private static Runnable runnable = new Runnable() {\n        @Override\n        public void run() {\n            for (; ;) {\n                if (!isStart) {\n                    break;\n                }\n                double c = Math.PI * Math.PI * Math.PI * random.nextFloat();\n//                Log.e(\"test\", \"v:\" + c);\n            }\n        }\n    };\n\n    public static void start() {\n        if (isStart) {\n            return;\n        }\n        isStart = true;\n        for (int i = 0; i < threads.length; i++) {\n            Thread thread = new Thread(runnable);\n            thread.setPriority(Thread.NORM_PRIORITY);\n            thread.start();\n            threads[i] = thread;\n        }\n    }\n    \n    public static void stop() {\n        isStart = false;\n        for (Thread thread : threads) {\n            try {\n                thread.join();\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/StaticLayoutManager.java",
    "content": "package com.lsjwzh.test;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.text.Layout;\nimport android.text.StaticLayout;\nimport android.text.TextDirectionHeuristic;\nimport android.text.TextDirectionHeuristics;\nimport android.text.TextLayoutWarmer;\nimport android.text.TextPaint;\n\n/**\n * Created by ragnarok on 15/7/21.\n */\npublic class StaticLayoutManager {\n  public final static TextLayoutWarmer<StaticLayout> sLayoutWarmer = new TextLayoutWarmer<>();\n\n  private StaticLayout[] layout = new StaticLayout[Util.TEST_LIST_ITEM_COUNT];\n\n  private StaticLayout longStringLayout;\n\n  private TextPaint textPaint;\n  private TextDirectionHeuristic textDir;\n  private Layout.Alignment alignment;\n\n  private Canvas dummyCanvas;\n\n  private int hardCodeWidth;\n\n  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)\n  public void initLayout(Context context, CharSequence source, CharSequence longString) {\n\n    textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);\n    textPaint.density = context.getResources().getDisplayMetrics().density;\n    textPaint.setTextSize(Util.fromDPtoPix(context, Util.TEXT_SIZE_DP));\n\n    textDir = TextDirectionHeuristics.LTR;\n\n    alignment = Layout.Alignment.ALIGN_NORMAL;\n\n    hardCodeWidth = Util.getScreenWidth(context);\n\n    longStringLayout = new StaticLayout(longString, textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);\n\n    dummyCanvas = new Canvas();\n\n    longStringLayout.draw(dummyCanvas);\n\n    for (int i = 0; i < layout.length; i++) {\n      layout[i] = new StaticLayout(TestSpan.getSpanString(i), textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);\n      layout[i].draw(dummyCanvas);\n    }\n  }\n\n  public StaticLayout getLayout(int index) {\n    return layout[index] != null ? layout[index]\n        : sLayoutWarmer.getLayout(TestSpan.getSpanString(index));\n  }\n\n  public StaticLayout getLongStringLayout() {\n    return longStringLayout;\n  }\n\n  private static StaticLayoutManager INSTANCE = null;\n\n  public static StaticLayoutManager getInstance() {\n    if (INSTANCE == null) {\n      INSTANCE = new StaticLayoutManager();\n    }\n    return INSTANCE;\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestSingleLineTextView.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\nimport com.lsjwzh.widget.text.SingleLineTextView;\n\n/**\n * Created by wenye on 2017/11/5.\n */\n\npublic class TestSingleLineTextView extends SingleLineTextView {\n  private static final String TAG = \"TestSingleLineTextView\";\n  private boolean mIsDebug = true;\n\n  public TestSingleLineTextView(Context context) {\n    super(context);\n  }\n\n  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public TestSingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    long start = System.currentTimeMillis();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onDraw(canvas);\n    }\n    long end = System.currentTimeMillis();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onDraw cost:\" + (end - start));\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    long start = System.currentTimeMillis();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    long end = System.currentTimeMillis();\n    if (mIsDebug) {\n      Log.d(TAG, TAG + \" onMeasure cost:\" + (end - start));\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestSpan.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.BitmapFactory;\nimport android.text.SpannableString;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.style.ImageSpan;\n\nimport com.wechat.testdemo.R;\n\nimport java.util.Random;\n\n/**\n * Created by ragnarok on 15/7/21.\n */\npublic class TestSpan {\n\n  private static String testString = \"(\\uD83E\\uDDC0|\\uD83E\\uDD84|\\uD83E\\uDD83|\\uD83E\\uDD82|\\uD83E\" +\n      \"\\uDD81\" +\n          \"|\\uD83E\\uDD80|\\uD83E\\uDD18\\uD83C\\uDFFF|\\uD83E\\uDD18\\uD83C\\uDFFE|\\uD83E\\uDD18\\uD83C\" +\n          \"\\uDFFD|\\uD83E\\uDD18\\uD83C\\uDFFC|\\uD83E\\uDD18\\uD83C\\uDFFB|\\uD83E\\uDD18|\\uD83E\\uDD17\" +\n          \"|\\uD83E\\uDD16|\\uD83E\\uDD15|\\uD83E\\uDD14|\\uD83E\\uDD13|\\uD83E\\uDD12|\\uD83E\\uDD11|\\uD83E\" +\n          \"\\uDD10|\\uD83DuDEA4|\\uD83D\\uDEA3\\uD83C\\uDFFF|\\uD83D\\uDEA3\\uD83C\\uDFFE|\\uD83D\\uDEA3\" +\n          \"\\uD83C\\uDFFD|\\uD83D\\uDEA3\\uD83C\\uDFFC|\\uD83D\\uDEA3\\uD83C\\uDFFB|\\uD83D\\uDEA3\\u200D\" +\n          \"\\u2640\\uFE0F|\\uD83D\" +\n          \"\\uD83D\\uDC87\\uD83C\\uDFFC|\\uD83D\\uDC87\\uD83C\\uDFFB|\\uD83D\\uDC87\\u200D\\u2642\" +\n          \"\\uFE0F|\\uD83D\\uDC87|\\uD83D\\uDC86\\uD83C\\uDFFF|\\uD83D\\uDC86\\uD83C\\uDFFE\" +\n          \"|\\uD83D\\uDC86\\uD83C\\uDFFD|\\uD83D\\uDC86\\uD83C\\uDFFC|\\uD83D\\uDC86\\uD83C\" +\n          \"\\uDFFB|\\uD83D\\uDC86\\u200D\\u2642\\uFE0F|\\uD83D\\uDC86|\\uD83D\\uDC85\\uD83C\" +\n          \"\\uDFFF|\\uD83D\\uDC85\\uD83C\\uDFFE|\\uD83D\\uDC85\\uD83C\\uDFFD|\\uD83D\\uDC85\" +\n          \"\\uD83C\\uDFFC|\\uD83D\\uDC85\\uD83C\\uDFFB|\\uD83D\\uDC85|\\uD83D\\uDC84|\\uD83D\" +\n          \"\\uDC83\\uD83C\\uDFFF|\\uD83D\\uDC83\\uD83C\\uDFFE|\\uD83D\\uDC83\\uD83C\\uDFFD\" +\n          \"|\\uD83D\\uDC83\\uD83C\\uDFFC|\\uD83D\\uDC83\\uD83C\\uDFFB|\\uD83D\\uDC83|\\uD83D\" +\n          \"\\uDC82\\uD83C\\uDFFF|\\uD83D\\uDC82\\uD83C\\uDFFE|\\uD83D\\uDC82\\uD83C\\uDFFD\" +\n          \"|\\uD83D\\uDC82\\uD83C\\uDFFC|\\uD83D\\uDC82\\uD83C\\uDFFB|\\uD83D\\uDC82\\u200D\" +\n          \"\\u2640\\uFE0F|\\uD83D\\uDC82|\\uD83D\\uDC81\\uD83C\\uDFFF|\\uD83D\\uDC81\\uD83C\" +\n          \"\\uDFFE|\\uD83D\\uDC81\\uD83C\\uDFFD|\\uD83D\\uDC81\\uD83C\\uDFFC|\\uD83D\\uDC81\" +\n          \"\\uD83C\\uDFFB|\\uD83D\\uDC81\\u200D\\u2642\\uFE0F|\\uD83D\\uDC81|\\uD83D\\uDC80\" +\n          \"|\\uD83D\\uDC7F|\\uD83D\\uDC7E|\\uD83D\\uDC7D|\\uD83D\\uDC7C\\uD83C\\uDFFF|\\uD83D\" +\n          \"\\uDC7C\\uD83C\\uDFFE|\\uD83D\\uDC7C\\uD83C\\uDFFD|\\uD83D\\uDC7C\\uD83C\\uDFFC\" +\n          \"|\\uD83D\\uDC7C\\uD83C\\uDFFB|\\uD83D\\uDC7C|\\uD83D\\uDC7B|\\uD83D\\uDC7A|\\uD83D\" +\n          \"\\uDC79|\\uD83D\\uDC78\\uD83C\\uDFFF|\\uD83D\\uDC78\\uD83C\\uDFFE|\\uD83D\\uDC78\" +\n          \"\\uD83C\\uDFFD|\\uD83D\\uDC78\\uD83C\\uDFFC|\\uD83D\\uDC78\\uD83C\\uDFFB|\\uD83D\" +\n          \"\\uDC78|\\uD83D\\uDC77\\uD83C\\uDFFF|\\uD83D\\uDC77\\uD83C\\uDFFE|\\uD83D\\uDC77\" +\n          \"\\uD83C\\uDFFD|\\uD83D\\uDC77\\uD83C\\uDFFC|\\uD83D\\uDC77\\uD83C\\uDFFB|\\uD83D\" +\n          \"\\uDC77\\u200D\\u2640\\uFE0F|\\uD83D\\uDC77|\\uD83D\\uDC76\\uD83C\\uDFFF|\\uD83D\" +\n          \"\\uDC76\\uD83C\\uDFFE|\\uD83D\\uDC76\\uD83C\\uDFFD|\\uD83D\\uDC76\\uD83C\\uDFFC\" +\n          \"|\\uD83D\\uDC76\\uD83C\\uDFFB|\\uD83D\\uDC76|\\uD83D\\uDC75\\uD83C\\uDFFF|\\uD83D\" +\n          \"\\uDC75\\uD83C\\uDFFE|\\uD83D\\uDC75\\uD83C\\uDFFD|\\uD83D\\uDC75\\uD83C\\uDFFC\" +\n          \"|\\uD83D\\uDC75\\uD83C\\uDFFB|\\uD83D\\uDC75|\\uD83D\\uDC74\\uD83C\\uDFFF|\\uD83D\" +\n          \"\\uDC74\\uD83C\\uDFFE|\\uD83D\\uDC74\\uD83C\\uDFFD|\\uD83D\\uDC74\\uD83C\\uDFFC\" +\n          \"|\\uD83D\\uDC74\\uD83C\\uDFFB|\\uD83D\\uDC74|\\uD83D\\uDC73\\uD83C\\uDFFF|\\uD83D\" +\n          \"\\uDC73\\uD83C\\uDFFE|\\uD83D\\uDC73\\uD83C\\uDFFD|\\uD83D\\uDC73\\uD83C\\uDFFC\" +\n          \"|\\uD83D\\uDC73\\uD83C\\uDFFB|\\uD83D\\uDC73\\u200D\\u2640\\uFE0F|\\uD83D\\uDC73\" +\n          \"|\\uD83D\\uDC72\\uD83C\\uDFFF|\\uD83D\\uDC72\\uD83C\\uDFFE|\\uD83D\\uDC72\\uD83C\" +\n          \"\\uDFFD|\\uD83D\\uDC72\\uD83C\\uDFFC|\\uD83D\\uDC72\\uD83C\\uDFFB|\\uD83D\\uDC72\" +\n          \"|\\uD83D\\uDC71\\uD83C\\uDFFF|\\uD83D\\uDC71\\uD83C\\uDFFE|\\uD83D\\uDC71\\uD83C\" +\n          \"\\uDFFD|\\uD83D\\uDC71\\uD83C\\uDFFC|\\uD83D\\uDC71\\uD83C\\uDFFB|\\uD83D\\uDC71\" +\n          \"\\u200D\\u2640\\uFE0F|\\uD83D\\uDC71|\\uD83D\\uDC70\\uD83C\\uDFFF|\\uD83D\\uDC70\" +\n          \"\\uD83C\\uDFFE|\\uD83D\\uDC70\\uD83C\\uDFFD|\\uD83D\\uDC70\\uD83C\\uDFFC|\\uD83D\" +\n          \"\\uDC70\\uD83C\\uDFFB|\\uD83D\\uDC70|\\uD83D\\uDC6F\\u200D\\u2642\\uFE0F|\\uD83D\" +\n          \"\\uDC6F|\\uD83D\\uDC6E\\uD83C\\uDFFF|\\uD83D\\uDC6E\\uD83C\\uDFFE|\\uD83D\\uDC6E\" +\n          \"\\uD83C\\uDFFD|\\uD83D\\uDC6E\\uD83C\\uDFFC|\\uD83D\\uDC6E\\uD83C\\uDFFB|\\uD83D\" +\n          \"\\uDC6E\\u200D\\u2640\\uFE0F|\\uD83D\\uDC6E|\\uD83D\\uDC6D|\\uD83D\\uDC6C|\\uD83D\" +\n          \"\\uDC6B|\\uD83D\\uDC6A|\\uD83D\\uDC69\\uD83C\\uDFFF|\\uD83D\\uDC69\\uD83C\\uDFFE\" +\n          \"|\\uD83D\\uDC69\\uD83C\\uDFFD|\\uD83D\\uDC69\\uD83C\\uDFFC|\\uD83D\\uDC69\\uD83C\" +\n          \"\\uDFFB|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC67\" +\n          \"|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC66|\\uD83D\" +\n          \"\\uDC69\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\" +\n          \"\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC69\\u200D\" +\n          \"\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC67|\\uD83D\\uDC69\" +\n          \"\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC67|\\uD83D\" +\n          \"\\uDC69\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC69\\u200D\\uD83D\\uDC66\" +\n          \"|\\uD83D\\uDC69\\u200D\\u2764\\uFE0F\\u200D\\uD83D\\uDC8B\\u200D\\uD83D\\uDC69|\\uD83D\" +\n          \"\\uDC69\\u200D\\u2764\\uFE0F\\u200D\\uD83D\\uDC8B\\u200D\\uD83D\\uDC68|\\uD83D\\uDC69\" +\n          \"\\u200D\\u2764\\uFE0F\\u200D\\uD83D\\uDC69|\\uD83D\\uDC69\\u200D\\u2764\\uFE0F\\u200D\" +\n          \"\\uD83D\\uDC68|\\uD83D\\uDC69|\\uD83D\\uDC68\\uD83C\\uDFFF|\\uD83D\\uDC68\\uD83C\" +\n          \"\\uDFFE|\\uD83D\\uDC68\\uD83C\\uDFFD|\\uD83D\\uDC68\\uD83C\\uDFFC|\\uD83D\\uDC68\" +\n          \"\\uD83C\\uDFFB|\\uD83D\\uDC68\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\" +\n          \"\\uDC67|\\uD83D\\uDC68\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC66\" +\n          \"|\\uD83D\\uDC68\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC67|\\uD83D\\uDC68\\u200D\\uD83D\" +\n          \"\\uDC69\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\\uDC69\" +\n          \"\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\\uDC68\\u200D\\uD83D\\uDC67\\u200D\" +\n          \"\\uD83D\\uDC67|\\uD83D\\uDC68\\u200D\\uD83D\\uDC68\\u200D\\uD83D\\uDC67\\u200D\\uD83D\" +\n          \"\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\\uDC68\\u200D\\uD83D\\uDC67|\\uD83D\\uDC68\\u200D\" +\n          \"\\uD83D\\uDC68\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\" +\n          \"\\uDC68\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC67\" +\n          \"|\\uD83D\\uDC68\\u200D\\uD83D\\uDC67\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\uD83D\" +\n          \"\\uDC67|\\uD83D\\uDC68\\u200D\\uD83D\\uDC66\\u200D\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\" +\n          \"\\uD83D\\uDC66|\\uD83D\\uDC68\\u200D\\u2764\\uFE0F\\u200D\\uD83D\\uDC8B\\u200D\\uD83D\" +\n          \"\\uDC68|\\uD83D\\uDC68\\u200D\\u2764\\uFE0F\\u200D\\uD83D\\uDC68|\\uD83D\\uDC68\" +\n          \"|\\uD83D\\uDC67\\uD83C\\uDFFF|\\uD83D\\uDC67\\uD83C\\uDFFE|\\uD83D\\uDC67\\uD83C\" +\n          \"\\uDFFD|\\uD83D\\uDC67\\uD83C\\uDFFC|\\uD83D\\uDC67\\uD83C\\uDFFB|\\uD83D\\uDC67\" +\n          \"|\\uD83D\\uDC66\\uD83C\\uDFFF|\\uD83D\\uDC66\\uD83C\\uDFFE|\\uD83D\\uDC66\\uD83C\" +\n          \"\\uDFFD|\\uD83D\\uDC66\\uD83C\\uDFFC|\\uD83D\\uDC66\\uD83C\\uDFFB|\\uD83D\\uDC66\" +\n          \"|\\uD83D\\uDC65|\\uD83D\\uDC64|\\uD83D\\uDC63|\\uD83D\\uDC62|\\uD83D\\uDC61|\\uD83D\" +\n          \"\\uDC60|\\uD83D\\uDC5F|\\uD83D\\uDC5E|\\uD83D\\uDC5D|\\uD83D\\uDC5C|\\uD83D\\uDC5B\" +\n          \"|\\uD83D\\uDC5A|\\uD83D\\uDC59|\\uD83D\\uDC58|\\uD83D\\uDC57|\\uD83D\\uDC56|\\uD83D\" +\n          \"\\uDC55|\\uD83D\\uDC54|\\uD83D\\uDC53|\\uD83D\\uDC52|\\uD83D\\uDC51|\\uD83D\\uDC50\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC50\\uD83C\\uDFFE|\\uD83D\\uDC50\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC50\\uD83C\\uDFFC|\\uD83D\\uDC50\\uD83C\\uDFFB|\\uD83D\\uDC50|\\uD83D\\uDC4F\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4F\\uD83C\\uDFFE|\\uD83D\\uDC4F\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4F\\uD83C\\uDFFC|\\uD83D\\uDC4F\\uD83C\\uDFFB|\\uD83D\\uDC4F|\\uD83D\\uDC4E\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4E\\uD83C\\uDFFE|\\uD83D\\uDC4E\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4E\\uD83C\\uDFFC|\\uD83D\\uDC4E\\uD83C\\uDFFB|\\uD83D\\uDC4E|\\uD83D\\uDC4D\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4D\\uD83C\\uDFFE|\\uD83D\\uDC4D\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4D\\uD83C\\uDFFC|\\uD83D\\uDC4D\\uD83C\\uDFFB|\\uD83D\\uDC4D|\\uD83D\\uDC4C\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4C\\uD83C\\uDFFE|\\uD83D\\uDC4C\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4C\\uD83C\\uDFFC|\\uD83D\\uDC4C\\uD83C\\uDFFB|\\uD83D\\uDC4C|\\uD83D\\uDC4B\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4B\\uD83C\\uDFFE|\\uD83D\\uDC4B\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4B\\uD83C\\uDFFC|\\uD83D\\uDC4B\\uD83C\\uDFFB|\\uD83D\\uDC4B|\\uD83D\\uDC4A\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC4A\\uD83C\\uDFFE|\\uD83D\\uDC4A\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC4A\\uD83C\\uDFFC|\\uD83D\\uDC4A\\uD83C\\uDFFB|\\uD83D\\uDC4A|\\uD83D\\uDC49\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC49\\uD83C\\uDFFE|\\uD83D\\uDC49\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC49\\uD83C\\uDFFC|\\uD83D\\uDC49\\uD83C\\uDFFB|\\uD83D\\uDC49|\\uD83D\\uDC48\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC48\\uD83C\\uDFFE|\\uD83D\\uDC48\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC48\\uD83C\\uDFFC|\\uD83D\\uDC48\\uD83C\\uDFFB|\\uD83D\\uDC48|\\uD83D\\uDC47\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC47\\uD83C\\uDFFE|\\uD83D\\uDC47\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC47\\uD83C\\uDFFC|\\uD83D\\uDC47\\uD83C\\uDFFB|\\uD83D\\uDC47|\\uD83D\\uDC46\" +\n          \"\\uD83C\\uDFFF|\\uD83D\\uDC46\\uD83C\\uDFFE|\\uD83D\\uDC46\\uD83C\\uDFFD|\\uD83D\" +\n          \"\\uDC46\\uD83C\\uDFFC|\\uD83D\\uDC46\\uD83C\\uDFFB|\\uD83D\\uDC46|\\uD83D\\uDC45\" +\n          \"|\\uD83D\\uDC44|\\uD83D\\uDC43\\uD83C\\uDFFF|\\uD83D\\uDC43\\uD83C\\uDFFE|\\uD83D\" +\n          \"\\uDC43\\uD83C\\uDFFD|\\uD83D\\uDC43\\uD83C\\uDFFC|\\uD83D\\uDC43\\uD83C\\uDFFB\" +\n          \"|\\uD83D\\uDC43|\\uD83D\\uDC42\\uD83C\\uDFFF|\\uD83D\\uDC42\\uD83C\\uDFFE|\\uD83D\" +\n          \"\\uDC42\\uD83C\\uDFFD|\\uD83D\\uDC42\\uD83C\\uDFFC|\\uD83D\\uDC42\\uD83C\\uDFFB\" +\n          \"|\\uD83D\\uDC42|\\uD83D\\uDC41\\u200D\\uD83D\\uDDE8|\\uD83D\\uDC41|\\uD83D\\uDC40\" +\n          \"|\\uD83D\\uDC3F|\\uD83D\\uDC3E|\\uD83D\\uDC3D|\\uD83D\\uDC3C|\\uD83D\\uDC3B|\\uD83D\" +\n          \"\\uDC3A|\\uD83D\\uDC39|\\uD83D\\uDC38|\\uD83D\\uDC37|\\uD83D\\uDC36|\\uD83D\\uDC35\" +\n          \"|\\uD83D\\uDC34|\\uD83D\\uDC33|\\uD83D\\uDC32|\\uD83D\\uDC31|\\uD83D\\uDC30|\\uD83D\" +\n          \"\\uDC2F|\\uD83D\\uDC2E|\\uD83D\\uDC2D|\\uD83D\\uDC2C|\\uD83D\\uDC2B|\\uD83D\\uDC2A\" +\n          \"|\\uD83D\\uDC29|\\uD83D\\uDC28|\\uD83D\\uDC27|\\uD83D\\uDC26|\\uD83D\\uDC25|\\uD83D\" +\n          \"\\uDC24|\\uD83D\\uDC23|\\uD83D\\uDC22|\\uD83D\\uDC21|\\uD83D\\uDC20|\\uD83D\\uDC1F\" +\n          \"|\\uD83D\\uDC1E|\\uD83D\\uDC1D|\\uD83D\\uDC1C|\\uD83D\\uDC1B|\\uD83D\\uDC1A|\\uD83D\" +\n          \"\\uDC19|\\uD83D\\uDC18|\\uD83D\\uDC17|\\uD83D\\uDC16|\\uD83D\\uDC15|\\uD83D\\uDC14\" +\n          \"|\\uD83D\\uDC13|\\uD83D\\uDC12|\\uD83D\\uDC11|\\uD83D\\uDC10|\\uD83D\\uDC0F|\\uD83D\" +\n          \"\\uDC0E|\\uD83D\\uDC0D|\\uD83D\\uDC0C|\\uD83D\\uDC0B|\\uD83D\\uDC0A|\\uD83D\\uDC09\" +\n          \"|\\uD83D\\uDC08|\\uD83D\\uDC07|\\uD83D\\uDC06|\\uD83D\\uDC05|\\uD83D\\uDC04|\\uD83D\" +\n          \"\\uDC03|\\uD83D\\uDC02|\\uD83D\\uDC01|\\uD83D\\uDC00|\\uD83C\\uDFFF|\\uD83C\\uDFFE\" +\n          \"|\\uD83C\\uDFFD|\\uD83C\\uDFFC|\\uD83C\\uDFFB|\\uD83C\\uDFFA|\\uD83C\\uDFF9|\\uD83C\" +\n          \"\\uDFF8|\\uD83C\\uDFF7|\\uD83C\\uDFF5|\\uD83C\\uDFF4|\\uD83C\\uDFF3\\uFE0F\\u200D\" +\n          \"\\uD83C\\uDF08|\\uD83C\\uDFF3|\\uD83C\\uDFF0|\\uD83C\\uDFEF|\\uD83C\\uDFEE|\\uD83C\" +\n          \"\\uDFED|\\uD83C\\uDFEC|\\uD83C\\uDFEB|\\uD83C\\uDFEA|\\uD83C\\uDFE9|\\uD83C\\uDFE8\" +\n          \"|\\uD83C\\uDFE7|\\uD83C\\uDFE6|\\uD83C\\uDFE5|\\uD83C\\uDFE4|\\uD83C\\uDFE3|\\uD83C\" +\n          \"\\uDFE2|\\uD83C\\uDFE1|\\uD83C\\uDFE0|\\uD83C\\uDFDF|\\uD83C\\uDFDE|\\uD83C\\uDFDD\" +\n          \"|\\uD83C\\uDFDC|\\uD83C\\uDFDB|\\uD83C\\uDFDA|\\uD83C\\uDFD9|\\uD83C\\uDFD8|\\uD83C\" +\n          \"\\uDFD7|\\uD83C\\uDFD6|\\uD83C\\uDFD5|\\uD83C\\uDFD4|\\uD83C\\uDFD3|\\uD83C\\uDFD2\" +\n          \"|\\uD83C\\uDFD1|\\uD83C\\uDFD0|\\uD83C\\uDFCF|\\uD83C\\uDFCE|\\uD83C\\uDFCD|\\uD83C\" +\n          \"\\uDFCC\\uFE0F\\u200D\\u2640\\uFE0F|\\uD83C\\uDFCC|\\uD83C\\uDFCB\\uFE0F\\u200D\\u2640\" +\n          \"\\uFE0F|\\uD83C\\uDFCB\\uD83C\\uDFFF|\\uD83C\\uDFCB\\uD83C\\uDFFE|\\uD83C\\uDFCB\" +\n          \"\\uD83C\\uDFFD|\\uD83C\\uDFCB\\uD83C\\uDFFC|\\uD83C\\uDFCB\\uD83C\\uDFFB|\\uD83C\" +\n          \"\\uDFCB|\\uD83C\\uDFCA\\uD83C\\uDFFF|\\uD83C\\uDFCA\\uD83C\\uDFFE|\\uD83C\\uDFCA\" +\n          \"\\uD83C\\uDFFD|\\uD83C\\uDFCA\\uD83C\\uDFFC|\\uD83C\\uDFCA\\uD83C\\uDFFB|\\uD83C\" +\n          \"\\uDFCA\\u200D\\u2640\\uFE0F|\\uD83C\\uDFCA|\\uD83C\\uDFC9|\\uD83C\\uDFC8|\\uD83C\" +\n          \"\\uDFC7|\\uD83C\\uDFC6|\\uD83C\\uDFC5|\\uD83C\\uDFC4\\uD83C\\uDFFF|\\uD83C\\uDFC4\" +\n          \"\\uD83C\\uDFFE|\\uD83C\\uDFC4\\uD83C\\uDFFD|\\uD83C\\uDFC4\\uD83C\\uDFFC|\\uD83C\" +\n          \"\\uDFC4\\uD83C\\uDFFB|\\uD83C\\uDFC4\\u200D\\u2640\\uFE0F|\\uD83C\\uDFC4|\\uD83C\" +\n          \"\\uDFC3\\uD83C\\uDFFF|\\uD83C\\uDFC3\\uD83C\\uDFFE|\\uD83C\\uDFC3\\uD83C\\uDFFD\" +\n          \"|\\uD83C\\uDFC3\\uD83C\\uDFFC|\\uD83C\\uDFC3\\uD83C\\uDFFB|\\uD83C\\uDFC3\\u200D\" +\n          \"\\u2640\\uFE0F|\\uD83C\\uDFC3|\\uD83C\\uDFC2|\\uD83C\\uDFC1|\\uD83C\\uDFC0|\\uD83C\" +\n          \"\\uDFBF|\\uD83C\\uDFBE|\\uD83C\\uDFBD|\\uD83C\\uDFBC|\\uD83C\\uDFBB|\\uD83C\\uDFBA\" +\n          \"|\\uD83C\\uDFB9|\\uD83C\\uDFB8|\\uD83C\\uDFB7|\\uD83C\\uDFB6|\\uD83C\\uDFB5|\\uD83C\" +\n          \"\\uDFB4|\\uD83C\\uDFB3|\\uD83C\\uDFB2|\\uD83C\\uDFB1|\\uD83C\\uDFB0|\\uD83C\\uDFAF\" +\n          \"|\\uD83C\\uDFAE|\\uD83C\\uDFAD|\\uD83C\\uDFAC|\\uD83C\\uDFAB|\\uD83C\\uDFAA|\\uD83C\" +\n          \"\\uDFA9|\\uD83C\\uDFA8|\\uD83C\\uDFA7|\\uD83C\\uDFA6|\\uD83C\\uDFA5|\\uD83C\\uDFA4\" +\n          \"|\\uD83C\\uDFA3|\\uD83C\\uDFA2|\\uD83C\\uDFA1|\\uD83C\\uDFA0|\\uD83C\\uDF9F|\\uD83C\" +\n          \"\\uDF9E|\\uD83C\\uDF9B|\\uD83C\\uDF9A|\\uD83C\\uDF99|\\uD83C\\uDF97|\\uD83C\\uDF96\" +\n          \"|\\uD83C\\uDF93|\\uD83C\\uDF92|\\uD83C\\uDF91|\\uD83C\\uDF90|\\uD83C\\uDF8F|\\uD83C\" +\n          \"\\uDF8E|\\uD83C\\uDF8D|\\uD83C\\uDF8C|\\uD83C\\uDF8B|\\uD83C\\uDF8A|\\uD83C\\uDF89\" +\n          \"|\\uD83C\\uDF88|\\uD83C\\uDF87|\\uD83C\\uDF86|\\uD83C\\uDF85\\uD83C\\uDFFF|\\uD83C\" +\n          \"\\uDF85\\uD83C\\uDFFE|\\uD83C\\uDF85\\uD83C\\uDFFD|\\uD83C\\uDF85\\uD83C\\uDFFC\" +\n          \"|\\uD83C\\uDF85\\uD83C\\uDFFB|\\uD83C\\uDF85|\\uD83C\\uDF84|\\uD83C\\uDF83|\\uD83C\" +\n          \"\\uDF82|\\uD83C\\uDF81|\\uD83C\\uDF80|\\uD83C\\uDF7F|\\uD83C\\uDF7E|\\uD83C\\uDF7D\" +\n          \"|\\uD83C\\uDF7C|\\uD83C\\uDF7B|\\uD83C\\uDF7A|\\uD83C\\uDF79|\\uD83C\\uDF78|\\uD83C\" +\n          \"\\uDF77|\\uD83C\\uDF76|\\uD83C\\uDF75|\\uD83C\\uDF74|\\uD83C\\uDF73|\\uD83C\\uDF72\" +\n          \"|\\uD83C\\uDF71|\\uD83C\\uDF70|\\uD83C\\uDF6F|\\uD83C\\uDF6E|\\uD83C\\uDF6D|\\uD83C\" +\n          \"\\uDF6C|\\uD83C\\uDF6B|\\uD83C\\uDF6A|\\uD83C\\uDF69|\\uD83C\\uDF68|\\uD83C\\uDF67\" +\n          \"|\\uD83C\\uDF66|\\uD83C\\uDF65|\\uD83C\\uDF64|\\uD83C\\uDF63|\\uD83C\\uDF62|\\uD83C\" +\n          \"\\uDF61|\\uD83C\\uDF60|\\uD83C\\uDF5F|\\uD83C\\uDF5E|\\uD83C\\uDF5D|\\uD83C\\uDF5C\" +\n          \"|\\uD83C\\uDF5B|\\uD83C\\uDF5A|\\uD83C\\uDF59|\\uD83C\\uDF58|\\uD83C\\uDF57|\\uD83C\" +\n          \"\\uDF56|\\uD83C\\uDF55|\\uD83C\\uDF54|\\uD83C\\uDF53|\\uD83C\\uDF52|\\uD83C\\uDF51\" +\n          \"|\\uD83C\\uDF50|\\uD83C\\uDF4F|\\uD83C\\uDF4E|\\uD83C\\uDF4D|\\uD83C\\uDF4C|\\uD83C\" +\n          \"\\uDF4B|\\uD83C\\uDF4A|\\uD83C\\uDF49|\\uD83C\\uDF48|\\uD83C\\uDF47|\\uD83C\\uDF46\" +\n          \"|\\uD83C\\uDF45|\\uD83C\\uDF44|\\uD83C\\uDF43|\\uD83C\\uDF42|\\uD83C\\uDF41|\\uD83C\" +\n          \"\\uDF40|\\uD83C\\uDF3F|\\uD83C\\uDF3E|\\uD83C\\uDF3D|\\uD83C\\uDF3C|\\uD83C\\uDF3B\" +\n          \"|\\uD83C\\uDF3A|\\uD83C\\uDF39|\\uD83C\\uDF38|\\uD83C\\uDF37|\\uD83C\\uDF36|\\uD83C\" +\n          \"\\uDF35|\\uD83C\\uDF34|\\uD83C\\uDF33|\\uD83C\\uDF32|\\uD83C\\uDF31|\\uD83C\\uDF30\" +\n          \"|\\uD83C\\uDF2F|\\uD83C\\uDF2E|\\uD83C\\uDF2D|\\uD83C\\uDF2C|\\uD83C\\uDF2B|\\uD83C\" +\n          \"\\uDF2A|\\uD83C\\uDF29|\\uD83C\\uDF28|\\uD83C\\uDF27|\\uD83C\\uDF26|\\uD83C\\uDF25\" +\n          \"|\\uD83C\\uDF24|\\uD83C\\uDF21|\\uD83C\\uDF20|\\uD83C\\uDF1F|\\uD83C\\uDF1E|\\uD83C\" +\n          \"\\uDF1D|\\uD83C\\uDF1C|\\uD83C\\uDF1B|\\uD83C\\uDF1A|\\uD83C\\uDF19|\\uD83C\\uDF18\" +\n          \"|\\uD83C\\uDF17|\\uD83C\\uDF16|\\uD83C\\uDF15|\\uD83C\\uDF14|\\uD83C\\uDF13|\\uD83C\" +\n          \"\\uDF12|\\uD83C\\uDF11|\\uD83C\\uDF10|\\uD83C\\uDF0F|\\uD83C\\uDF0E|\\uD83C\\uDF0D\" +\n          \"|\\uD83C\\uDF0C|\\uD83C\\uDF0B|\\uD83C\\uDF0A|\\uD83C\\uDF09|\\uD83C\\uDF08|\\uD83C\" +\n          \"\\uDF07|\\uD83C\\uDF06|\\uD83C\\uDF05|\\uD83C\\uDF04|\\uD83C\\uDF03|\\uD83C\\uDF02\" +\n          \"|\\uD83C\\uDF01|\\uD83C\\uDF00|\\uD83C\\uDE51|\\uD83C\\uDE50|\\uD83C\\uDE3A|\\uD83C\" +\n          \"\\uDE39|\\uD83C\\uDE38|\\uD83C\\uDE37|\\uD83C\\uDE36|\\uD83C\\uDE35|\\uD83C\\uDE34\" +\n          \"|\\uD83C\\uDE33|\\uD83C\\uDE32|\\uD83C\\uDE2F|\\uD83C\\uDE1A|\\uD83C\\uDE02|\\uD83C\" +\n          \"\\uDE01|\\uD83C\\uDDFF\\uD83C\\uDDFC|\\uD83C\\uDDFF\\uD83C\\uDDF2|\\uD83C\\uDDFF\" +\n          \"\\uD83C\\uDDE6|\\uD83C\\uDDFE\\uD83C\\uDDF9|\\uD83C\\uDDFE\\uD83C\\uDDEA|\\uD83C\" +\n          \"\\uDDFD\\uD83C\\uDDF0|\\uD83C\\uDDFC\\uD83C\\uDDF8|\\uD83C\\uDDFC\\uD83C\\uDDEB\" +\n          \"|\\uD83C\\uDDFB\\uD83C\\uDDFA|\\uD83C\\uDDFB\\uD83C\\uDDF3|\\uD83C\\uDDFB\\uD83C\" +\n          \"\\uDDEE|\\uD83C\\uDDFB\\uD83C\\uDDEC|\\uD83C\\uDDFB\\uD83C\\uDDEA|\\uD83C\\uDDFB\" +\n          \"\\uD83C\\uDDE8|\\uD83C\\uDDFB\\uD83C\\uDDE6|\\uD83C\\uDDFA\\uD83C\\uDDFF|\\uD83C\" +\n          \"\\uDDFA\\uD83C\\uDDFE|\\uD83C\\uDDFA\\uD83C\\uDDF8|\\uD83C\\uDDFA\\uD83C\\uDDF2\" +\n          \"|\\uD83C\\uDDFA\\uD83C\\uDDEC|\\uD83C\\uDDFA\\uD83C\\uDDE6|\\uD83C\\uDDF9\\uD83C\" +\n          \"\\uDDFF|\\uD83C\\uDDF9\\uD83C\\uDDFC|\\uD83C\\uDDF9\\uD83C\\uDDFB|\\uD83C\\uDDF9\" +\n          \"\\uD83C\\uDDF9|\\uD83C\\uDDF9\\uD83C\\uDDF7|\\uD83C\\uDDF9\\uD83C\\uDDF4|\\uD83C\" +\n          \"\\uDDF9\\uD83C\\uDDF3|\\uD83C\\uDDF9\\uD83C\\uDDF2|\\uD83C\\uDDF9\\uD83C\\uDDF1\" +\n          \"|\\uD83C\\uDDF9\\uD83C\\uDDF0|\\uD83C\\uDDF9\\uD83C\\uDDEF|\\uD83C\\uDDF9\\uD83C\" +\n          \"\\uDDED|\\uD83C\\uDDF9\\uD83C\\uDDEC|\\uD83C\\uDDF9\\uD83C\\uDDEB|\\uD83C\\uDDF9\" +\n          \"\\uD83C\\uDDE9|\\uD83C\\uDDF9\\uD83C\\uDDE8|\\uD83C\\uDDF9\\uD83C\\uDDE6|\\uD83C\" +\n          \"\\uDDF8\\uD83C\\uDDFF|\\uD83C\\uDDF8\\uD83C\\uDDFE|\\uD83C\\uDDF8\\uD83C\\uDDFD\" +\n          \"|\\uD83C\\uDDF8\\uD83C\\uDDFB|\\uD83C\\uDDF8\\uD83C\\uDDF9|\\uD83C\\uDDF8\\uD83C\" +\n          \"\\uDDF8|\\uD83C\\uDDF8\\uD83C\\uDDF7|\\uD83C\\uDDF8\\uD83C\\uDDF4|\\uD83C\\uDDF8\" +\n          \"\\uD83C\\uDDF3|\\uD83C\\uDDF8\\uD83C\\uDDF2|\\uD83C\\uDDF8\\uD83C\\uDDF1|\\uD83C\" +\n          \"\\uDDF8\\uD83C\\uDDF0|\\uD83C\\uDDF8\\uD83C\\uDDEF|\\uD83C\\uDDF8\\uD83C\\uDDEE\" +\n          \"|\\uD83C\\uDDF8\\uD83C\\uDDED|\\uD83C\\uDDF8\\uD83C\\uDDEC|\\uD83C\\uDDF8\\uD83C\" +\n          \"\\uDDEA|\\uD83C\\uDDF8\\uD83C\\uDDE9|\\uD83C\\uDDF8\\uD83C\\uDDE8|\\uD83C\\uDDF8\" +\n          \"\\uD83C\\uDDE7|\\uD83C\\uDDF8\\uD83C\\uDDE6|\\uD83C\\uDDF7\\uD83C\\uDDFC|\\uD83C\" +\n          \"\\uDDF7\\uD83C\\uDDFA|\\uD83C\\uDDF7\\uD83C\\uDDF8|\\uD83C\\uDDF7\\uD83C\\uDDF4\" +\n          \"|\\uD83C\\uDDF7\\uD83C\\uDDEA|\\uD83C\\uDDF6\\uD83C\\uDDE6|\\uD83C\\uDDF5\\uD83C\" +\n          \"\\uDDFE|\\uD83C\\uDDF5\\uD83C\\uDDFC|\\uD83C\\uDDF5\\uD83C\\uDDF9|\\uD83C\\uDDF5\" +\n          \"\\uD83C\\uDDF8|\\uD83C\\uDDF5\\uD83C\\uDDF7|\\uD83C\\uDDF5\\uD83C\\uDDF3|\\uD83C\" +\n          \"\\uDDF5\\uD83C\\uDDF2|\\uD83C\\uDDF5\\uD83C\\uDDF1|\\uD83C\\uDDF5\\uD83C\\uDDF0\" +\n          \"|\\uD83C\\uDDF5\\uD83C\\uDDED|\\uD83C\\uDDF5\\uD83C\\uDDEC|\\uD83C\\uDDF5\\uD83C\" +\n          \"\\uDDEB|\\uD83C\\uDDF5\\uD83C\\uDDEA|\\uD83C\\uDDF5\\uD83C\\uDDE6|\\uD83C\\uDDF4\" +\n          \"\\uD83C\\uDDF2|\\uD83C\\uDDF3\\uD83C\\uDDFF|\\uD83C\\uDDF3\\uD83C\\uDDFA|\\uD83C\" +\n          \"\\uDDF3\\uD83C\\uDDF7|\\uD83C\\uDDF3\\uD83C\\uDDF5|\\uD83C\\uDDF3\\uD83C\\uDDF4\" +\n          \"|\\uD83C\\uDDF3\\uD83C\\uDDF1|\\uD83C\\uDDF3\\uD83C\\uDDEE|\\uD83C\\uDDF3\\uD83C\" +\n          \"\\uDDEC|\\uD83C\\uDDF3\\uD83C\\uDDEB|\\uD83C\\uDDF3\\uD83C\\uDDEA|\\uD83C\\uDDF3\" +\n          \"\\uD83C\\uDDE8|\\uD83C\\uDDF3\\uD83C\\uDDE6|\\uD83C\\uDDF2\\uD83C\\uDDFF|\\uD83C\" +\n          \"\\uDDF2\\uD83C\\uDDFE|\\uD83C\\uDDF2\\uD83C\\uDDFD|\\uD83C\\uDDF2\\uD83C\\uDDFC\" +\n          \"|\\uD83C\\uDDF2\\uD83C\\uDDFB|\\uD83C\\uDDF2\\uD83C\\uDDFA|\\uD83C\\uDDF2\\uD83C\" +\n          \"\\uDDF9|\\uD83C\\uDDF2\\uD83C\\uDDF8|\\uD83C\\uDDF2\\uD83C\\uDDF7|\\uD83C\\uDDF2\" +\n          \"\\uD83C\\uDDF6|\\uD83C\\uDDF2\\uD83C\\uDDF5|\\uD83C\\uDDF2\\uD83C\\uDDF4|\\uD83C\" +\n          \"\\uDDF2\\uD83C\\uDDF3|\\uD83C\\uDDF2\\uD83C\\uDDF2|\\uD83C\\uDDF2\\uD83C\\uDDF1\" +\n          \"|\\uD83C\\uDDF2\\uD83C\\uDDF0|\\uD83C\\uDDF2\\uD83C\\uDDED|\\uD83C\\uDDF2\\uD83C\" +\n          \"\\uDDEC|\\uD83C\\uDDF2\\uD83C\\uDDEB|\\uD83C\\uDDF2\\uD83C\\uDDEA|\\uD83C\\uDDF2\" +\n          \"\\uD83C\\uDDE9|\\uD83C\\uDDF2\\uD83C\\uDDE8|\\uD83C\\uDDF2\\uD83C\\uDDE6|\\uD83C\" +\n          \"\\uDDF1\\uD83C\\uDDFE|\\uD83C\\uDDF1\\uD83C\\uDDFB|\\uD83C\\uDDF1\\uD83C\\uDDFA\" +\n          \"|\\uD83C\\uDDF1\\uD83C\\uDDF9|\\uD83C\\uDDF1\\uD83C\\uDDF8|\\uD83C\\uDDF1\\uD83C\" +\n          \"\\uDDF7|\\uD83C\\uDDF1\\uD83C\\uDDF0|\\uD83C\\uDDF1\\uD83C\\uDDEE|\\uD83C\\uDDF1\" +\n          \"\\uD83C\\uDDE8|\\uD83C\\uDDF1\\uD83C\\uDDE7|\\uD83C\\uDDF1\\uD83C\\uDDE6|\\uD83C\" +\n          \"\\uDDF0\\uD83C\\uDDFF|\\uD83C\\uDDF0\\uD83C\\uDDFE|\\uD83C\\uDDF0\\uD83C\\uDDFC\" +\n          \"|\\uD83C\\uDDF0\\uD83C\\uDDF7|\\uD83C\\uDDF0\\uD83C\\uDDF5|\\uD83C\\uDDF0\\uD83C\" +\n          \"\\uDDF3|\\uD83C\\uDDF0\\uD83C\\uDDF2|\\uD83C\\uDDF0\\uD83C\\uDDEE|\\uD83C\\uDDF0\" +\n          \"\\uD83C\\uDDED|\\uD83C\\uDDF0\\uD83C\\uDDEC|\\uD83C\\uDDF0\\uD83C\\uDDEA|\\uD83C\" +\n          \"\\uDDEF\\uD83C\\uDDF5|\\uD83C\\uDDEF\\uD83C\\uDDF4|\\uD83C\\uDDEF\\uD83C\\uDDF2\" +\n          \"|\\uD83C\\uDDEF\\uD83C\\uDDEA|\\uD83C\\uDDEE\\uD83C\\uDDF9|\\uD83C\\uDDEE\\uD83C\" +\n          \"\\uDDF8|\\uD83C\\uDDEE\\uD83C\\uDDF7|\\uD83C\\uDDEE\\uD83C\\uDDF6|\\uD83C\\uDDEE\" +\n          \"\\uD83C\\uDDF4|\\uD83C\\uDDEE\\uD83C\\uDDF3|\\uD83C\\uDDEE\\uD83C\\uDDF2|\\uD83C\" +\n          \"\\uDDEE\\uD83C\\uDDF1|\\uD83C\\uDDEE\\uD83C\\uDDEA|\\uD83C\\uDDEE\\uD83C\\uDDE9\" +\n          \"|\\uD83C\\uDDEE\\uD83C\\uDDE8|\\uD83C\\uDDED\\uD83C\\uDDFA|\\uD83C\\uDDED\\uD83C\" +\n          \"\\uDDF9|\\uD83C\\uDDED\\uD83C\\uDDF7|\\uD83C\\uDDED\\uD83C\\uDDF3|\\uD83C\\uDDED\" +\n          \"\\uD83C\\uDDF2|\\uD83C\\uDDED\\uD83C\\uDDF0|\\uD83C\\uDDEC\\uD83C\\uDDFE|\\uD83C\" +\n          \"\\uDDEC\\uD83C\\uDDFC|\\uD83C\\uDDEC\\uD83C\\uDDFA|\\uD83C\\uDDEC\\uD83C\\uDDF9\" +\n          \"|\\uD83C\\uDDEC\\uD83C\\uDDF8|\\uD83C\\uDDEC\\uD83C\\uDDF7|\\uD83C\\uDDEC\\uD83C\" +\n          \"\\uDDF6|\\uD83C\\uDDEC\\uD83C\\uDDF5|\\uD83C\\uDDEC\\uD83C\\uDDF3|\\uD83C\\uDDEC\" +\n          \"\\uD83C\\uDDF2|\\uD83C\\uDDEC\\uD83C\\uDDF1|\\uD83C\\uDDEC\\uD83C\\uDDEE|\\uD83C\" +\n          \"\\uDDEC\\uD83C\\uDDED|\\uD83C\\uDDEC\\uD83C\\uDDEC|\\uD83C\\uDDEC\\uD83C\\uDDEB\" +\n          \"|\\uD83C\\uDDEC\\uD83C\\uDDEA|\\uD83C\\uDDEC\\uD83C\\uDDE9|\\uD83C\\uDDEC\\uD83C\" +\n          \"\\uDDE7|\\uD83C\\uDDEC\\uD83C\\uDDE6|\\uD83C\\uDDEB\\uD83C\\uDDF7|\\uD83C\\uDDEB\" +\n          \"\\uD83C\\uDDF4|\\uD83C\\uDDEB\\uD83C\\uDDF2|\\uD83C\\uDDEB\\uD83C\\uDDF0|\\uD83C\" +\n          \"\\uDDEB\\uD83C\\uDDEF|\\uD83C\\uDDEB\\uD83C\\uDDEE|\\uD83C\\uDDEA\\uD83C\\uDDFA\" +\n          \"|\\uD83C\\uDDEA\\uD83C\\uDDF9|\\uD83C\\uDDEA\\uD83C\\uDDF8|\\uD83C\\uDDEA\\uD83C\" +\n          \"\\uDDF7|\\uD83C\\uDDEA\\uD83C\\uDDED|\\uD83C\\uDDEA\\uD83C\\uDDEC|\\uD83C\\uDDEA\" +\n          \"\\uD83C\\uDDEA|\\uD83C\\uDDEA\\uD83C\\uDDE8|\\uD83C\\uDDEA\\uD83C\\uDDE6|\\uD83C\" +\n          \"\\uDDE9\\uD83C\\uDDFF|\\uD83C\\uDDE9\\uD83C\\uDDF4|\\uD83C\\uDDE9\\uD83C\\uDDF2\" +\n          \"|\\uD83C\\uDDE9\\uD83C\\uDDF0|\\uD83C\\uDDE9\\uD83C\\uDDEF|\\uD83C\\uDDE9\\uD83C\" +\n          \"\\uDDEC|\\uD83C\\uDDE9\\uD83C\\uDDEA|\\uD83C\\uDDE8\\uD83C\\uDDFF|\\uD83C\\uDDE8\" +\n          \"\\uD83C\\uDDFE|\\uD83C\\uDDE8\\uD83C\\uDDFD|\\uD83C\\uDDE8\\uD83C\\uDDFC|\\uD83C\" +\n          \"\\uDDE8\\uD83C\\uDDFB|\\uD83C\\uDDE8\\uD83C\\uDDFA|\\uD83C\\uDDE8\\uD83C\\uDDF7\" +\n          \"|\\uD83C\\uDDE8\\uD83C\\uDDF5|\\uD83C\\uDDE8\\uD83C\\uDDF4|\\uD83C\\uDDE8\\uD83C\" +\n          \"\\uDDF3|\\uD83C\\uDDE8\\uD83C\\uDDF2|\\uD83C\\uDDE8\\uD83C\\uDDF1|\\uD83C\\uDDE8\" +\n          \"\\uD83C\\uDDF0|\\uD83C\\uDDE8\\uD83C\\uDDEE|\\uD83C\\uDDE8\\uD83C\\uDDED|\\uD83C\" +\n          \"\\uDDE8\\uD83C\\uDDEC|\\uD83C\\uDDE8\\uD83C\\uDDEB|\\uD83C\\uDDE8\\uD83C\\uDDE9\" +\n          \"|\\uD83C\\uDDE8\\uD83C\\uDDE8|\\uD83C\\uDDE8\\uD83C\\uDDE6|\\uD83C\\uDDE7\\uD83C\" +\n          \"\\uDDFF|\\uD83C\\uDDE7\\uD83C\\uDDFE|\\uD83C\\uDDE7\\uD83C\\uDDFC|\\uD83C\\uDDE7\" +\n          \"\\uD83C\\uDDFB|\\uD83C\\uDDE7\\uD83C\\uDDF9|\\uD83C\\uDDE7\\uD83C\\uDDF8|\\uD83C\" +\n          \"\\uDDE7\\uD83C\\uDDF7|\\uD83C\\uDDE7\\uD83C\\uDDF6|\\uD83C\\uDDE7\\uD83C\\uDDF4\" +\n          \"|\\uD83C\\uDDE7\\uD83C\\uDDF3|\\uD83C\\uDDE7\\uD83C\\uDDF2|\\uD83C\\uDDE7\\uD83C\" +\n          \"\\uDDF1|\\uD83C\\uDDE7\\uD83C\\uDDEF|\\uD83C\\uDDE7\\uD83C\\uDDEE|\\uD83C\\uDDE7\" +\n          \"\\uD83C\\uDDED|\\uD83C\\uDDE7\\uD83C\\uDDEC|\\uD83C\\uDDE7\\uD83C\\uDDEB|\\uD83C\" +\n          \"\\uDDE7\\uD83C\\uDDEA|\\uD83C\\uDDE7\\uD83C\\uDDE9|\\uD83C\\uDDE7\\uD83C\\uDDE7\" +\n          \"|\\uD83C\\uDDE7\\uD83C\\uDDE6|\\uD83C\\uDDE6\\uD83C\\uDDFF|\\uD83C\\uDDE6\\uD83C\" +\n          \"\\uDDFD|\\uD83C\\uDDE6\\uD83C\\uDDFC|\\uD83C\\uDDE6\\uD83C\\uDDFA|\\uD83C\\uDDE6\" +\n          \"\\uD83C\\uDDF9|\\uD83C\\uDDE6\\uD83C\\uDDF8|\\uD83C\\uDDE6\\uD83C\\uDDF7|\\uD83C\" +\n          \"\\uDDE6\\uD83C\\uDDF6|\\uD83C\\uDDE6\\uD83C\\uDDF4|\\uD83C\\uDDE6\\uD83C\\uDDF2\" +\n          \"|\\uD83C\\uDDE6\\uD83C\\uDDF1|\\uD83C\\uDDE6\\uD83C\\uDDEE|\\uD83C\\uDDE6\\uD83C\" +\n          \"\\uDDEC|\\uD83C\\uDDE6\\uD83C\\uDDEB|\\uD83C\\uDDE6\\uD83C\\uDDEA|\\uD83C\\uDDE6\" +\n          \"\\uD83C\\uDDE9|\\uD83C\\uDDE6\\uD83C\\uDDE8|\\uD83C\\uDD9A|\\uD83C\\uDD99|\\uD83C\" +\n          \"\\uDD98|\\uD83C\\uDD97|\\uD83C\\uDD96|\\uD83C\\uDD95|\\uD83C\\uDD94|\\uD83C\\uDD93\" +\n          \"|\\uD83C\\uDD92|\\uD83C\\uDD91|\\uD83C\\uDD8E|\\uD83C\\uDD7F|\\uD83C\\uDD7E|\\uD83C\" +\n          \"\\uDD71|\\uD83C\\uDD70|\\uD83C\\uDCCF|\\uD83C\\uDC04|\\u3299|\\u3297|\\u303D|\\u3030\" +\n          \"|\\u2B55|\\u2B50|\\u2B1C|\\u2B1B|\\u2B07|\\u2B06|\\u2B05|\\u2935|\\u2934|\\u27BF\" +\n          \"|\\u27B0|\\u27A1|\\u2797|\\u2796|\\u2795|\\u2764|\\u2763|\\u2757|\\u2755|\\u2754\" +\n          \"|\\u2753|\\u274E|\\u274C|\\u2747|\\u2744|\\u2734|\\u2733|\\u2728|\\u2721|\\u271D\" +\n          \"|\\u2716|\\u2714|\\u2712|\\u270F|\\u270D\\uD83C\\uDFFF|\\u270D\\uD83C\\uDFFE|\\u270D\" +\n          \"\\uD83C\\uDFFD|\\u270D\\uD83C\\uDFFC|\\u270D\\uD83C\\uDFFB|\\u270D|\\u270C\\uD83C\" +\n          \"\\uDFFF|\\u270C\\uD83C\\uDFFE|\\u270C\\uD83C\\uDFFD|\\u270C\\uD83C\\uDFFC|\\u270C\" +\n          \"\\uD83C\\uDFFB|\\u270C|\\u270B\\uD83C\\uDFFF|\\u270B\\uD83C\\uDFFE|\\u270B\\uD83C\" +\n          \"\\uDFFD|\\u270B\\uD83C\\uDFFC|\\u270B\\uD83C\\uDFFB|\\u270B|\\u270A\\uD83C\\uDFFF\" +\n          \"|\\u270A\\uD83C\\uDFFE|\\u270A\\uD83C\\uDFFD|\\u270A\\uD83C\\uDFFC|\\u270A\\uD83C\" +\n          \"\\uDFFB|\\u270A|\\u2709|\\u2708|\\u2705|\\u2702|\\u26FD|\\u26FA|\\u26F9\\uFE0F\\u200D\" +\n          \"\\u2640\\uFE0F|\\u26F9\\uD83C\\uDFFF|\\u26F9\\uD83C\\uDFFE|\\u26F9\\uD83C\\uDFFD\" +\n          \"|\\u26F9\\uD83C\\uDFFC|\\u26F9\\uD83C\\uDFFB|\\u26F9|\\u26F8|\\u26F7|\\u26F5|\\u26F4\" +\n          \"|\\u26F3|\\u26F2|\\u26F1|\\u26F0|\\u26EA|\\u26E9|\\u26D4|\\u26D3|\\u26D1|\\u26CF\" +\n          \"|\\u26CE|\\u26C8|\\u26C5|\\u26C4|\\u26BE|\\u26BD|\\u26B1|\\u26B0|\\u26AB|\\u26AA\" +\n          \"|\\u26A1|\\u26A0|\\u269C|\\u269B|\\u2699|\\u2697|\\u2696|\\u2694|\\u2693|\\u2692\" +\n          \"|\\u267F|\\u267B|\\u2668|\\u2666|\\u2665|\\u2663|\\u2660|\\u2653|\\u2652|\\u2651\" +\n          \"|\\u2650|\\u264F|\\u264E|\\u264D|\\u264C|\\u264B|\\u264A|\\u2649|\\u2648|\\u263A\" +\n          \"|\\u2639|\\u2638|\\u262F|\\u262E|\\u262A|\\u2626|\\u2623|\\u2622|\\u2620|\\u261D\" +\n          \"\\uD83C\\uDFFF|\\u261D\\uD83C\\uDFFE|\\u261D\\uD83C\\uDFFD|\\u261D\\uD83C\\uDFFC\" +\n          \"|\\u261D\\uD83C\\uDFFB|\\u261D|\\u2618|\\u2615|\\u2614|\\u2611|\\u260E|\\u2604|\\u2603\" +\n          \"|\\u2602|\\u2601|\\u2600|\\u25FE|\\u25FD|\\u25FC|\\u25FB|\\u25C0|\\u25B6|\\u25AB\" +\n          \"|\\u25AA|\\u24C2|\\u23FA|\\u23F9|\\u23F8|\\u23F3|\\u23F2|\\u23F1|\\u23F0|\\u23EF\" +\n          \"|\\u23EE|\\u23ED|\\u23EC|\\u23EB|\\u23EA|\\u23E9|\\u23CF|\\u2328|\\u231B|\\u231A\" +\n          \"|\\u21AA|\\u21A9|\\u2199|\\u2198|\\u2197|\\u2196|\\u2195|\\u2194|\\u2139|\\u2122\" +\n          \"|\\u2049|\\u203C|\\u00AE|\\u00A9|\\u0039\\uFE0F\\u20E3|\\u0038\\uFE0F\\u20E3|\\u0037\" +\n          \"\\uFE0F\\u20E3|\\u0036\\uFE0F\\u20E3|\\u0035\\uFE0F\\u20E3|\\u0034\\uFE0F\\u20E3\" +\n          \"|\\u0033\\uFE0F\\u20E3|\\u0032\\uFE0F\\u20E3|\\u0031\\uFE0F\\u20E3|\\u0030\\uFE0F\" +\n          \"\\u20E3|\\u002A\\uFE0F\\u20E3|\\u0023\\uFE0F\\u20E3)\";\n\n  private static SpannableString testLongString =\n      new SpannableString\n          (\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\" +\n          \"AAAAAAAAAAAAAAAAAAAAAAAAAAAA\");\n\n  private static SpannableStringBuilder[] allTestStrings = new SpannableStringBuilder[Util.TEST_LIST_ITEM_COUNT];\n\n  public static void init(Context context) {\n\n    Random random = new Random();\n\n    for (int i = 0; i < allTestStrings.length; i++) {\n      int start = i;\n      int len = 50 + random.nextInt(100);\n      len = len > 1 ? len : 1;\n      int end = start + len > testString.length() ? testString.length() : start + len;\n      allTestStrings[i] =  new SpannableStringBuilder(i + \":\" + testString.subSequence(start, end));\n\n      ImageSpan imgSpan = new ImageSpan(context,\n          BitmapFactory.decodeResource(context.getResources(), R.drawable.test));\n      int spanStart = random.nextInt(len - 1);\n      allTestStrings[i].setSpan(imgSpan, spanStart, spanStart + 1,\n          Spanned.SPAN_INCLUSIVE_INCLUSIVE);\n    }\n\n    for (int i = 0; i < testLongString.length(); i++) {\n      if (i + 1 < testLongString.length()) {\n        ImageSpan imgSpan = new ImageSpan(context, BitmapFactory.decodeResource(context\n            .getResources(), R.drawable.test));\n        testLongString.setSpan(imgSpan, i, i + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);\n      }\n    }\n  }\n\n  public static SpannableStringBuilder getSpanString(int index) {\n    return allTestStrings[index];\n  }\n\n  public static SpannableString getLongSpanString() {\n    return testLongString;\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestStats.java",
    "content": "package com.lsjwzh.test;\n\nimport android.os.SystemClock;\n\n/**\n * Created by wenye on 2017/12/15.\n */\npublic class TestStats {\n  private volatile long drawCost = 0;\n  private volatile long drawCount = 0;\n  private volatile long measureCost = 0;\n  private volatile long measureCount = 0;\n  private volatile long layoutCost = 0;\n  private volatile long layoutCount = 0;\n\n  private long drawStart = 0;\n  private long measureStart = 0;\n  private long layoutStart = 0;\n\n  @Override\n  public String toString() {\n    return \"TestStats{\" +\n        \"drawCost=\" + drawCost +\n        \", drawCount=\" + drawCount +\n        \", measureCost=\" + measureCost +\n        \", measureCount=\" + measureCount +\n        \", layoutCost=\" + layoutCost +\n        \", layoutCount=\" + layoutCount +\n        '}';\n  }\n\n  public void reset() {\n    drawCost = 0;\n    drawCount = 0;\n    measureCost = 0;\n    measureCount = 0;\n    layoutCost = 0;\n    layoutCount = 0;\n  }\n\n  public long getDrawCost() {\n    return drawCost;\n  }\n\n  public long getDrawCount() {\n    return drawCount;\n  }\n\n  public long getMeasureCost() {\n    return measureCost;\n  }\n\n  public long getMeasureCount() {\n    return measureCount;\n  }\n\n  public long getLayoutCost() {\n    return layoutCost;\n  }\n\n  public long getLayoutCount() {\n    return layoutCount;\n  }\n\n  public void drawStart() {\n    drawStart = SystemClock.elapsedRealtime();\n  }\n\n  public void drawEnd() {\n    drawCost += SystemClock.elapsedRealtime() - drawStart;\n    drawCount++;\n  }\n\n  public void measuretart() {\n    measureStart = SystemClock.elapsedRealtime();\n  }\n\n  public void measureEnd() {\n    measureCost += SystemClock.elapsedRealtime() - measureStart;\n    measureCount++;\n  }\n\n  public void layoutStart() {\n    layoutStart = SystemClock.elapsedRealtime();\n  }\n\n  public void layoutEnd() {\n    layoutCost += SystemClock.elapsedRealtime() - layoutStart;\n    layoutCount++;\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TestTextView.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.SystemClock;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.Px;\nimport android.util.AttributeSet;\nimport android.widget.TextView;\n\n/**\n * Created by wenye on 2017/10/12.\n */\n\npublic class TestTextView extends TextView {\n  private static final String TAG = TestTextView.class.getSimpleName();\n  public static final TestStats TEST_STATS = new TestStats();\n\n  public TestTextView(Context context) {\n    super(context);\n  }\n\n  public TestTextView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public TestTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    TEST_STATS.measuretart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    TEST_STATS.measureEnd();\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    TEST_STATS.drawStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      super.onDraw(canvas);\n    }\n    TEST_STATS.drawEnd();\n//    Log.d(TAG, TAG + \" onDraw cost:\" + (end - start));\n  }\n\n  @Override\n  public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {\n    TEST_STATS.layoutStart();\n    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n      // TODO for test\n      super.layout(l, t, r, b);\n    }\n    TEST_STATS.layoutEnd();\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/TextLineView.java",
    "content": "//package com.lsjwzh.test;\n//\n//import android.content.Context;\n//import android.graphics.Canvas;\n//import android.graphics.Color;\n//import android.graphics.Paint;\n//import android.os.Build;\n//import android.os.SystemClock;\n//import android.support.annotation.Nullable;\n//import android.support.annotation.RequiresApi;\n//import android.text.Directions;\n//import android.text.Layout;\n//import android.text.TextLineCompat;\n//import android.text.TextPaint;\n//import android.util.AttributeSet;\n//import android.util.Log;\n//import android.view.View;\n//\n///**\n// * Just for test.\n// * Created by wenye on 2017/11/5.\n// */\n//public class TextLineView extends View {\n//  private static final String TAG = \"TextLineView\";\n//  private CharSequence mText;\n//  private TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);\n//  private final Paint.FontMetricsInt mFontMetric = new Paint.FontMetricsInt();\n//  TextLineCompat mTextLineCompat;\n//\n//  public TextLineView(Context context) {\n//    super(context);\n//  }\n//\n//  public TextLineView(Context context, @Nullable AttributeSet attrs) {\n//    super(context, attrs);\n//  }\n//\n//  public TextLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n//    super(context, attrs, defStyleAttr);\n//  }\n//\n//  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n//  public TextLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n//    super(context, attrs, defStyleAttr, defStyleRes);\n//  }\n//\n//  public void setText(CharSequence text) {\n//    mText = text;\n//    mTextPaint.setTextSize(20);\n//    mTextPaint.setColor(Color.RED);\n//    mTextLineCompat = TextLineCompat.obtain();\n//    mTextLineCompat.set(mTextPaint, mText, 0, mText.length(), Layout.DIR_LEFT_TO_RIGHT, Directions.DIRS_ALL_LEFT_TO_RIGHT, false, null);\n//    mTextPaint.getFontMetricsInt(mFontMetric);\n//  }\n//\n//  @Override\n//  protected void onDraw(Canvas canvas) {\n//    long start = SystemClock.elapsedRealtime();\n//    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n//      super.onDraw(canvas);\n//      int offset = getHeight() - (mFontMetric.bottom - mFontMetric.top);\n//      int baseLine = offset / 2 - mFontMetric.top;\n//      mTextLineCompat.draw(canvas, 0, 0, baseLine, baseLine + mFontMetric.bottom);\n//    }\n//    long end = SystemClock.elapsedRealtime();\n//    Log.d(TAG, TAG + \" onDraw cost:\" + (end - start));\n//  }\n//\n//  @Override\n//  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n//    long start = SystemClock.elapsedRealtime();\n//    for (int i = 0; i < Const.LOOP_COUNT; i++) {\n//      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n//    }\n//    long end = SystemClock.elapsedRealtime();\n//    Log.d(TAG, TAG + \" measure cost:\" + (end - start));\n//\n//  }\n//\n//\n//}\n"
  },
  {
    "path": "app/src/main/java/com/lsjwzh/test/Util.java",
    "content": "package com.lsjwzh.test;\n\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.view.WindowManager;\n\n/**\n * Created by ragnarok on 15/7/21.\n */\npublic class Util {\n    \n    public static final int TEST_LIST_ITEM_COUNT = 500;\n    \n    public static final int TEXT_SIZE_DP = 25;\n    \n    public static final int AUTO_SCROLL_INTERVAL = 1;\n    \n    public static final int AUTO_SCROLL_STEP = 10;\n    \n    public static float fromDPtoPix(Context context, int dp) {\n        return context.getResources().getDisplayMetrics().density * dp;\n    }\n    \n    public static int getScreenWidth(Context context) {\n        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n        \n        Point size = new Point();\n        windowManager.getDefaultDisplay().getSize(size);\n        \n        return size.x;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/EllipseFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.os.SystemClock;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.v4.app.Fragment;\nimport android.text.Layout;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.StaticLayout;\nimport android.text.StaticLayoutBuilderCompat;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.text.style.ImageSpan;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport com.lsjwzh.test.TestSingleLineTextView;\nimport com.lsjwzh.widget.text.FastTextLayoutView;\nimport com.lsjwzh.widget.text.FastTextView;\nimport com.lsjwzh.widget.text.ReadMoreTextView;\nimport com.lsjwzh.widget.text.StrokeSpan;\n\n/**\n * A placeholder fragment containing a simple view.\n */\npublic class EllipseFragment extends Fragment {\n\n  private View mRootView;\n\n  @Override\n  public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                           Bundle savedInstanceState) {\n    if (mRootView != null) {\n      return mRootView;\n    }\n    mRootView = inflater.inflate(R.layout.ellipse_demo, container, false);\n    SpannableStringBuilder spannableStringBuilder = getSpannable();\n    FastTextView fastTextView = (FastTextView) mRootView.findViewById(R.id.fast_tv2);\n    fastTextView.setText(spannableStringBuilder);\n//    fastTextView.setCustomEllipsisSpan(new ImageSpan(drawable));\n\n    TextView tv = (TextView) mRootView.findViewById(R.id.system_tv);\n    tv.setText(spannableStringBuilder);\n//    tv.setMovementMethod(LinkMovementMethod.getInstance());\n\n\n    return mRootView;\n  }\n\n  private StaticLayout getStaticLayout(SpannableStringBuilder spannableStringBuilder, TextPaint textPaint, float textSize, Rect bounds, String text) {\n    long start = SystemClock.elapsedRealtime();\n    for (int i = 0; i < 1000; i++) {\n      textPaint.getTextBounds(text, 0, spannableStringBuilder.length(), bounds);\n    }\n    long end = SystemClock.elapsedRealtime();\n    float withWithTextBounds = bounds.width();\n    Log.d(\"test\", \"withWithTextBounds:\" + withWithTextBounds + \" offset：\" + 0.5f * textSize + \" cost:\" + (end - start));\n\n    start = SystemClock.elapsedRealtime();\n    float withWithMeasureText = 0;\n    for (int i = 0; i < 1000; i++) {\n      withWithMeasureText = textPaint.measureText(spannableStringBuilder, 0, spannableStringBuilder.length());\n    }\n    end = SystemClock.elapsedRealtime();\n    Log.d(\"test\", \"withWithMeasureText:\" + withWithMeasureText + \" cost:\" + (end - start));\n\n    start = SystemClock.elapsedRealtime();\n    float withWithDesiredWidth = 0;\n    for (int i = 0; i < 1000; i++) {\n      withWithDesiredWidth = Layout.getDesiredWidth(spannableStringBuilder, textPaint);\n    }\n    end = SystemClock.elapsedRealtime();\n    Log.d(\"test\", \"withWithDesiredWidth:\" + withWithDesiredWidth + \" cost:\" + (end - start));\n\n\n    start = SystemClock.elapsedRealtime();\n    for (int i = 0; i < 1000; i++) {\n      spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), StrokeSpan.class);\n    }\n    end = SystemClock.elapsedRealtime();\n    Log.d(\"test\", \"getSpans cost:\" + (end - start));\n\n    int width = (int) Math.ceil(Math.max(Math.max(withWithTextBounds, withWithMeasureText), withWithDesiredWidth));\n\n    return StaticLayoutBuilderCompat.obtain(spannableStringBuilder, 0, spannableStringBuilder.length(),\n        textPaint, Math.min(width, getResources().getDisplayMetrics().widthPixels))\n        .setAlignment(Layout.Alignment.ALIGN_NORMAL)\n        .setLineSpacing(0f, 1f)\n        .setEllipsize(TextUtils.TruncateAt.END)\n        .setMaxLines(2).setIncludePad(true).build();\n  }\n\n  @NonNull\n  private SpannableStringBuilder getSpannable() {\n    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.content_cn));\n    Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);\n    drawable.setBounds(0, 0, 35, 35);\n\n    spannableStringBuilder.setSpan(new ImageSpan(drawable)\n        , 36, 37, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n\n    spannableStringBuilder.setSpan(new ImageSpan(drawable)\n        , 37, 38, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n\n    spannableStringBuilder.setSpan(new ImageSpan(drawable)\n        , 38, 39, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n\n    spannableStringBuilder.setSpan(new ImageSpan(drawable)\n        , 39, 40, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n//    ItalicReplacementSpan italicSpan = new ItalicReplacementSpan(-0.25f);\n//    StrokeSpan strokeSpan = new StrokeSpan(Color.BLUE, Color.YELLOW, 20);\n//    spannableStringBuilder.setSpan(strokeSpan, 0, spannableStringBuilder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n    return spannableStringBuilder;\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/FastTextViewListTestFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.SystemClock;\nimport android.support.annotation.Nullable;\nimport android.support.v4.app.Fragment;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\nimport android.widget.ListView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.lsjwzh.test.AutoScrollHandler;\nimport com.lsjwzh.test.FastTextView;\nimport com.lsjwzh.test.TestSpan;\nimport com.lsjwzh.test.TestTextView;\nimport com.lsjwzh.test.Util;\n\npublic class FastTextViewListTestFragment extends Fragment {\n\n  private ListView listView;\n\n  private ListAdapter adapter;\n\n  private AutoScrollHandler autoScrollHandler;\n\n  @Nullable\n  @Override\n  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable\n      Bundle savedInstanceState) {\n    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);\n\n    listView = (ListView) viewRoot.findViewById(R.id.test_list);\n\n    adapter = new ListAdapter(getActivity());\n\n    listView.setAdapter(adapter);\n\n    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);\n\n    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        FastTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestFastTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestFastTextViewStats\", FastTextView.TEST_STATS.toString());\n          }\n        });\n      }\n    });\n\n    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        FastTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestFastTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestFastTextViewStats\", FastTextView.TEST_STATS.toString());\n          }\n        });\n      }\n    });\n    return viewRoot;\n  }\n\n  private static class ListAdapter extends TestListAdapter {\n\n    private ListAdapter(Context context) {\n      super(context);\n    }\n\n    @Override\n    public View bindView(int position, View convertView, ViewGroup parent) {\n\n      if (convertView == null) {\n        convertView = LayoutInflater.from(context).inflate(R.layout.fast_list_item, parent,\n            false);\n\n        ViewHolder viewHolder = new ViewHolder();\n        viewHolder.textView = (FastTextView) convertView.findViewById(R.id.fast_text_view);\n        convertView.setTag(viewHolder);\n      }\n\n      ViewHolder holder = (ViewHolder) convertView.getTag();\n      holder.textView.setText(TestSpan.getSpanString(position));\n      return convertView;\n    }\n\n    private class ViewHolder {\n      FastTextView textView;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/MainActivity.java",
    "content": "package com.wechat.testdemo;\n\nimport android.os.Bundle;\nimport android.support.design.widget.FloatingActionButton;\nimport android.support.design.widget.Snackbar;\nimport android.support.v4.app.FragmentActivity;\nimport android.view.View;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport com.lsjwzh.test.FpsCalculator;\nimport com.lsjwzh.test.GhostThread;\n\npublic class MainActivity extends FragmentActivity {\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.activity_main);\n//    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);\n//    setSupportActionBar(toolbar);\n\n    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);\n    fab.setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View view) {\n        getSupportFragmentManager().popBackStackImmediate();\n      }\n    });\n    getSupportFragmentManager().beginTransaction()\n        .replace(R.id.fragment, new MainActivityFragment())\n        .commit();\n\n//    GhostThread.start();\n    FpsCalculator.instance().start();\n  }\n\n  @Override\n  public void onBackPressed() {\n    super.onBackPressed();\n  }\n\n  @Override\n  public boolean onCreateOptionsMenu(Menu menu) {\n    // Inflate the menu; this adds items to the action bar if it is present.\n    getMenuInflater().inflate(R.menu.menu_main, menu);\n    return true;\n  }\n\n  @Override\n  public boolean onOptionsItemSelected(MenuItem item) {\n    // Handle action bar item clicks here. The action bar will\n    // automatically handle clicks on the Home/Up button, so long\n    // as you specify a parent activity in AndroidManifest.xml.\n    int id = item.getItemId();\n\n    //noinspection SimplifiableIfStatement\n    if (id == R.id.action_settings) {\n      return true;\n    }\n\n    return super.onOptionsItemSelected(item);\n  }\n\n  @Override\n  protected void onDestroy() {\n    super.onDestroy();\n    FpsCalculator.instance().stop();\n    GhostThread.stop();\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/MainActivityFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.graphics.Paint;\nimport android.os.Bundle;\nimport android.support.v4.app.Fragment;\nimport android.text.Layout;\nimport android.text.StaticLayout;\nimport android.text.TextLayoutCache;\nimport android.text.TextLayoutWarmer;\nimport android.text.TextPaint;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Toast;\n\nimport com.lsjwzh.test.StaticLayoutManager;\nimport com.lsjwzh.test.TestSpan;\nimport com.lsjwzh.test.Util;\n\n/**\n * A placeholder fragment containing a simple view.\n */\npublic class MainActivityFragment extends Fragment {\n\n  private View mRootView;\n\n  @Override\n  public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                           Bundle savedInstanceState) {\n    if (mRootView != null) {\n      return mRootView;\n    }\n    mRootView = inflater.inflate(R.layout.fragment_main, container, false);\n    mRootView.findViewById(R.id.demo_measure_test).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new EllipseFragment())\n            .addToBackStack(null)\n            .commit();\n      }\n    });\n    mRootView.findViewById(R.id.demo_ellipsis).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new EllipseFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n    mRootView.findViewById(R.id.demo_read_more).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new ReadMoreFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n    mRootView.findViewById(R.id.demo_read_more_list).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new ReadMoreListTestFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n\n    mRootView.findViewById(R.id.prepare_layout_cache).setOnClickListener(new View.OnClickListener\n        () {\n      @Override\n      public void onClick(View v) {\n        StaticLayoutManager.sLayoutWarmer.setLayoutFactory(new TextLayoutWarmer\n            .LayoutFactory<StaticLayout>() {\n\n          @Override\n          public StaticLayout makeLayout(CharSequence text) {\n            // 公用TextPaint会导致fps下降？ why\n            final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);\n            textPaint.setTextSize(Util.fromDPtoPix(getActivity(), Util.TEXT_SIZE_DP));\n            int width = (int) Layout.getDesiredWidth(text, textPaint);\n            return new StaticLayout(text, textPaint,\n                Math.min(width, Util.getScreenWidth(getActivity())),\n                Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, true);\n          }\n        });\n        StaticLayoutManager.sLayoutWarmer.addListener(new TextLayoutWarmer\n            .WarmListener<StaticLayout>() {\n          int count = 0;\n\n          @Override\n          public void onWarmComplete(CharSequence text, TextLayoutWarmer.WarmerTask<StaticLayout>\n              warmerTask) {\n            TextLayoutCache.STATIC_LAYOUT_CACHE.put(text, warmerTask.mLayout);\n            count++;\n            if (count == Util.TEST_LIST_ITEM_COUNT) {\n              mRootView.post(new Runnable() {\n                @Override\n                public void run() {\n                  Toast.makeText(getActivity(), \"init layout and span finish\", Toast.LENGTH_LONG)\n                      .show();\n                }\n              });\n            }\n          }\n        });\n\n        for (int i = 0; i < Util.TEST_LIST_ITEM_COUNT; i++) {\n          StaticLayoutManager.sLayoutWarmer.addText(TestSpan.getSpanString(i));\n        }\n//        new Thread(new Runnable() {\n//          @Override\n//          public void run() {\n//            StaticLayoutManager.getInstance().initLayout(getActivity(), TestSpan.getSpanString\n// (), TestSpan.getLongSpanString());\n//            getActivity().runOnUiThread(new Runnable() {\n//              @Override\n//              public void run() {\n//                Toast.makeText(getActivity(), \"init layout and span finish\", Toast.LENGTH_LONG)\n// .show();\n//              }\n//            });\n//          }\n//        }).start();\n\n\n      }\n    });\n    mRootView.findViewById(R.id.demo_layout_cache).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new StaticLayoutCacheTestFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n    mRootView.findViewById(R.id.demo_fast_tv).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new FastTextViewListTestFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n    mRootView.findViewById(R.id.demo_normal_list).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        getFragmentManager().beginTransaction()\n            .replace(R.id.fragment, new NormalLayoutTestFragment())\n            .addToBackStack(null)\n            .commitAllowingStateLoss();\n      }\n    });\n    TestSpan.init(getActivity());\n    return mRootView;\n  }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/NormalLayoutTestFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.SystemClock;\nimport android.support.annotation.Nullable;\nimport android.support.v4.app.Fragment;\nimport android.text.method.LinkMovementMethod;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\nimport android.widget.ListView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.lsjwzh.test.AutoScrollHandler;\nimport com.lsjwzh.test.FastTextView;\nimport com.lsjwzh.test.TestSpan;\nimport com.lsjwzh.test.TestTextView;\nimport com.lsjwzh.test.Util;\n\npublic class NormalLayoutTestFragment extends Fragment {\n\n  private ListView listView;\n\n  private NormalListAdapter adapter;\n\n  private AutoScrollHandler autoScrollHandler;\n\n\n  @Nullable\n  @Override\n  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable\n      Bundle savedInstanceState) {\n    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);\n\n    listView = (ListView) viewRoot.findViewById(R.id.test_list);\n\n    adapter = new NormalListAdapter(getActivity());\n\n    listView.setAdapter(adapter);\n\n    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);\n\n    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        TestTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestTextViewStats\", TestTextView.TEST_STATS.toString());\n          }\n        });\n      }\n    });\n\n    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        TestTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestTextViewStats\", TestTextView.TEST_STATS.toString());\n\n          }\n        });\n      }\n    });\n    return viewRoot;\n  }\n\n  private static class NormalListAdapter extends TestListAdapter {\n\n    private NormalListAdapter(Context context) {\n      super(context);\n    }\n\n    @Override\n    public View bindView(int position, View convertView, ViewGroup parent) {\n\n      if (convertView == null) {\n        convertView = LayoutInflater.from(context).inflate(R.layout.normal_list_item, parent,\n            false);\n\n        ViewHolder viewHolder = new ViewHolder();\n        viewHolder.textView = (TextView) convertView.findViewById(R.id.normal_text);\n\n        convertView.setTag(viewHolder);\n      }\n\n      ViewHolder holder = (ViewHolder) convertView.getTag();\n      holder.textView.setMovementMethod(LinkMovementMethod.getInstance());\n      holder.textView.setText(TestSpan.getSpanString(position));\n      return convertView;\n    }\n\n    private class ViewHolder {\n      TextView textView;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/ReadMoreFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.v4.app.Fragment;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.TextPaint;\nimport android.text.style.ClickableSpan;\nimport android.text.style.ImageSpan;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Toast;\n\nimport com.lsjwzh.widget.text.ReadMoreTextView;\n\n/**\n * A placeholder fragment containing a simple view.\n */\npublic class ReadMoreFragment extends Fragment {\n\n  private View mRootView;\n\n  @Override\n  public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                           Bundle savedInstanceState) {\n    if (mRootView != null) {\n      return mRootView;\n    }\n    mRootView = inflater.inflate(R.layout.read_more_demo, container, false);\n    SpannableStringBuilder spannableStringBuilder = getSpannable();\n    final ReadMoreTextView readMoreTextView = mRootView.findViewById(R.id.readmore_tv);\n    readMoreTextView.setText(spannableStringBuilder);\n    readMoreTextView.setCustomEllipsisSpan(new ReadMoreTextView.EllipsisSpan(\"  Read More\") {\n      @Override\n      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) {\n        int oldColor = paint.getColor();\n        paint.setColor(Color.RED);\n        super.draw(canvas, text, start, end, x, top, y, bottom, paint);\n        paint.setColor(oldColor);\n      }\n    });\n    readMoreTextView.setCustomCollapseSpan(new ReadMoreTextView.EllipsisSpan(\"  Collapse\") {\n      @Override\n      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) {\n        int oldColor = paint.getColor();\n        paint.setColor(Color.RED);\n        super.draw(canvas, text, start, end, x, top, y, bottom, paint);\n        paint.setColor(oldColor);\n      }\n    });\n    readMoreTextView.setOnLongClickListener(new View.OnLongClickListener() {\n      @Override\n      public boolean onLongClick(View v) {\n        readMoreTextView.setTextColor(Color.RED);\n        return false;\n      }\n    });\n    return mRootView;\n  }\n\n  @NonNull\n  private SpannableStringBuilder getSpannable() {\n    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getResources().getString(R.string.content_cn));\n    return spannableStringBuilder;\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/ReadMoreListTestFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.support.annotation.Nullable;\nimport android.support.v4.app.Fragment;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ListView;\nimport android.widget.Toast;\n\nimport com.lsjwzh.test.AutoScrollHandler;\nimport com.lsjwzh.test.FastTextView;\nimport com.lsjwzh.test.TestSpan;\nimport com.lsjwzh.test.Util;\nimport com.lsjwzh.widget.text.ReadMoreTextView;\n\npublic class ReadMoreListTestFragment extends Fragment {\n\n  private ListView listView;\n\n  private ListAdapter adapter;\n\n  private AutoScrollHandler autoScrollHandler;\n\n  @Nullable\n  @Override\n  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable\n      Bundle savedInstanceState) {\n    View viewRoot = inflater.inflate(R.layout.normal_layout_ui, container, false);\n\n    listView = (ListView) viewRoot.findViewById(R.id.test_list);\n\n    adapter = new ListAdapter(getActivity());\n\n    listView.setAdapter(adapter);\n\n    autoScrollHandler = new AutoScrollHandler(listView, Util.TEST_LIST_ITEM_COUNT);\n\n    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        FastTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestFastTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestFastTextViewStats\", FastTextView.TEST_STATS.toString());\n          }\n        });\n      }\n    });\n\n    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        FastTextView.TEST_STATS.reset();\n        adapter.bindCost = 0;\n        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int fps) {\n            Toast.makeText(listView.getContext(), \"Average FPS: \" + fps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"TestFastTextView.avgFps\" + fps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"TestFastTextViewStats\", FastTextView.TEST_STATS.toString());\n          }\n        });\n      }\n    });\n    return viewRoot;\n  }\n\n  private static class ListAdapter extends TestListAdapter {\n\n    private ListAdapter(Context context) {\n      super(context);\n    }\n\n    @Override\n    public View bindView(int position, View convertView, ViewGroup parent) {\n\n      if (convertView == null) {\n        convertView = LayoutInflater.from(context).inflate(R.layout.readmore_list_item, parent,\n            false);\n\n        ViewHolder viewHolder = new ViewHolder();\n        viewHolder.textView = (ReadMoreTextView) convertView.findViewById(R.id.fast_text_view);\n\n        convertView.setTag(viewHolder);\n      }\n\n      ViewHolder holder = (ViewHolder) convertView.getTag();\n      holder.textView.setText(TestSpan.getSpanString(position));\n      return convertView;\n    }\n\n    private class ViewHolder {\n      ReadMoreTextView textView;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/StaticLayoutCacheTestFragment.java",
    "content": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.support.annotation.Nullable;\nimport android.support.v4.app.Fragment;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ListView;\nimport android.widget.Toast;\n\nimport com.lsjwzh.test.AutoScrollHandler;\nimport com.lsjwzh.test.StaticLayoutManager;\nimport com.lsjwzh.test.Util;\nimport com.lsjwzh.widget.text.FastTextLayoutView;\n\npublic class StaticLayoutCacheTestFragment extends Fragment {\n\n  private static final String TAG = \"StaticLayoutUI\";\n\n  private ListView listview;\n\n  private StaticListAdapter adapter;\n\n  private AutoScrollHandler autoScrollHandler;\n\n  @Nullable\n  @Override\n  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable\n      Bundle savedInstanceState) {\n    View viewRoot = inflater.inflate(R.layout.static_layout_ui, container, false);\n    listview = (ListView) viewRoot.findViewById(R.id.test_list);\n\n    adapter = new StaticListAdapter(getActivity());\n\n    listview.setAdapter(adapter);\n\n    autoScrollHandler = new AutoScrollHandler(listview, Util.TEST_LIST_ITEM_COUNT);\n\n    viewRoot.findViewById(R.id.scroll_down_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        com.lsjwzh.test.FastTextLayoutView.TEST_STATS.reset();\n        autoScrollHandler.startAutoScrollDown(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int avgFps) {\n            Toast.makeText(getActivity(), \"Average FPS: \" + avgFps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"FastTextLayoutView.avgFps\" + avgFps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"FastTextLayoutViewStats\", com.lsjwzh.test.FastTextLayoutView.TEST_STATS\n                .toString());\n          }\n        });\n      }\n    });\n\n    viewRoot.findViewById(R.id.scroll_up_button).setOnClickListener(new View.OnClickListener() {\n      @Override\n      public void onClick(View v) {\n        com.lsjwzh.test.FastTextLayoutView.TEST_STATS.reset();\n        autoScrollHandler.startAutoScrollUp(new AutoScrollHandler.Callback() {\n          @Override\n          public void callback(int avgFps) {\n            Toast.makeText(getActivity(), \"Average FPS: \" + avgFps, Toast.LENGTH_LONG).show();\n            Log.e(\"drawFps\", \"FastTextLayoutView.avgFps\" + avgFps);\n            Log.e(\"bindCost\", \"bindCost\" + adapter.bindCost);\n            Log.e(\"FastTextLayoutViewStats\", com.lsjwzh.test.FastTextLayoutView.TEST_STATS\n                .toString());\n          }\n        });\n      }\n    });\n\n    return viewRoot;\n  }\n\n  private static class StaticListAdapter extends TestListAdapter {\n\n    private StaticListAdapter(Context context) {\n      super(context);\n    }\n\n    @Override\n    public View bindView(int position, View convertView, ViewGroup parent) {\n      if (convertView == null) {\n        convertView = LayoutInflater.from(context).inflate(R.layout.static_list_item, parent,\n            false);\n\n        ViewHolder viewHolder = new ViewHolder();\n        viewHolder.staticLayoutView = (FastTextLayoutView) convertView.findViewById(R.id\n            .static_layout_view);\n\n        convertView.setTag(viewHolder);\n      }\n\n      ViewHolder holder = (ViewHolder) convertView.getTag();\n      holder.staticLayoutView.setTextLayout(StaticLayoutManager.getInstance().getLayout(position));\n      holder.staticLayoutView.requestLayout();\n      return convertView;\n    }\n\n    private class ViewHolder {\n      FastTextLayoutView staticLayoutView;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/wechat/testdemo/TestListAdapter.java",
    "content": "package com.wechat.testdemo;\n\nimport android.content.Context;\nimport android.os.SystemClock;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\n\nimport com.lsjwzh.test.Util;\n\npublic abstract class TestListAdapter extends BaseAdapter {\n\n  protected Context context;\n  public long bindCost = 0;\n\n  public TestListAdapter(Context context) {\n    this.context = context;\n  }\n\n  @Override\n  public int getCount() {\n    return Util.TEST_LIST_ITEM_COUNT;\n  }\n\n  @Override\n  public Object getItem(int position) {\n    return position;\n  }\n\n  @Override\n  public long getItemId(int position) {\n    return position;\n  }\n\n  @Override\n  public final View getView(int position, View convertView, ViewGroup parent) {\n    long start = SystemClock.elapsedRealtime();\n    convertView = bindView(position, convertView, parent);\n    long end = SystemClock.elapsedRealtime();\n    bindCost += (end - start);\n    return convertView;\n  }\n\n\n  public abstract View bindView(int position, View convertView, ViewGroup parent);\n\n}"
  },
  {
    "path": "app/src/main/res/anim/popup_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2014 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n     android:shareInterpolator=\"false\" >\n    <alpha android:fromAlpha=\"1.0\" android:toAlpha=\"1.0\"\n           android:interpolator=\"@android:anim/decelerate_interpolator\"\n           android:duration=\"100\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/popup_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2013 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n     android:shareInterpolator=\"false\" >\n    <alpha android:fromAlpha=\"1.0\" android:toAlpha=\"0.0\"\n           android:interpolator=\"@android:anim/decelerate_interpolator\"\n           android:duration=\"@integer/abc_config_activityShortDur\" />\n</set>"
  },
  {
    "path": "app/src/main/res/drawable/popup_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\"rectangle\" android:innerRadius=\"10dp\">\n  <solid android:color=\"#33ffffff\"/>\n  <corners android:radius=\"10dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.design.widget.CoordinatorLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:context=\"com.wechat.testdemo.MainActivity\">\n\n  <android.support.design.widget.AppBarLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:theme=\"@style/AppTheme.AppBarOverlay\">\n\n    <android.support.v7.widget.Toolbar\n      android:id=\"@+id/toolbar\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"?attr/actionBarSize\"\n      android:background=\"?attr/colorPrimary\"\n      app:popupTheme=\"@style/AppTheme.PopupOverlay\"/>\n\n  </android.support.design.widget.AppBarLayout>\n\n  <FrameLayout\n    android:id=\"@+id/fragment\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n  </FrameLayout>\n\n  <android.support.design.widget.FloatingActionButton\n    android:id=\"@+id/fab\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"bottom|end\"\n    android:layout_margin=\"@dimen/fab_margin\"\n    app:srcCompat=\"@drawable/ic_ab_back_holo_dark_am\"/>\n\n</android.support.design.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/ellipse_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:fillViewport=\"true\"\n  tools:context=\"com.wechat.testdemo.MainActivityFragment\"\n  tools:showIn=\"@layout/activity_main\">\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"Android TextView:\"/>\n\n    <com.lsjwzh.test.TestTextView\n      android:id=\"@+id/system_tv\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#6600ffff\"\n      android:ellipsize=\"end\"\n      android:maxLines=\"2\"\n      android:textSize=\"20sp\"/>\n\n\n    <TextView\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"FastTextView:\"/>\n\n    <com.lsjwzh.test.FastTextView\n      android:id=\"@+id/fast_tv2\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#6600ffff\"\n      android:clickable=\"true\"\n      android:ellipsize=\"end\"\n      android:maxLines=\"2\"\n      android:text=\"FastTextView\"\n      android:textSize=\"20sp\"/>\n\n    <View\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n  </LinearLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/fast_layout_ui.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n  <LinearLayout\n    android:id=\"@+id/op_layout\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_alignParentTop=\"true\"\n    android:layout_centerHorizontal=\"true\"\n    android:gravity=\"center\">\n\n    <Button\n      android:id=\"@+id/scroll_down_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollDown\"\n      />\n\n    <Button\n      android:id=\"@+id/scroll_up_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollUp\"/>\n  </LinearLayout>\n\n  <ListView\n    android:id=\"@+id/test_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@id/op_layout\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fast_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.FastTextView\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n\txmlns:app=\"http://schemas.android.com/apk/res-auto\"\n\tandroid:id=\"@+id/fast_text_view\"\n\tapp:enableLayoutCache=\"true\"\n\tandroid:textSize=\"25sp\"\n\tandroid:layout_width=\"wrap_content\"\n\tandroid:layout_height=\"wrap_content\"/>"
  },
  {
    "path": "app/src/main/res/layout/fragment_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:orientation=\"vertical\"\n  tools:context=\"com.wechat.testdemo.MainActivityFragment\"\n  tools:showIn=\"@layout/activity_main\">\n\n  <TextView\n    android:id=\"@+id/demo_measure_test\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Measure Test Demo\"/>\n\n  <TextView\n    android:id=\"@+id/demo_ellipsis\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Ellipsis Demo\"/>\n\n  <TextView\n    android:id=\"@+id/demo_read_more\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Read More Demo\"/>\n\n  <TextView\n    android:id=\"@+id/demo_read_more_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Read More List Demo\"/>\n\n\n  <TextView\n    android:id=\"@+id/prepare_layout_cache\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Prepare Cache\"/>\n\n  <TextView\n    android:id=\"@+id/demo_layout_cache\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Layout Cache Demo\"/>\n\n  <TextView\n    android:id=\"@+id/demo_fast_tv\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"FastTextView With Cache Layout\"/>\n\n  <TextView\n    android:id=\"@+id/demo_normal_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:textSize=\"30sp\"\n    android:text=\"Normal List Demo\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_cache_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:fillViewport=\"true\"\n  tools:context=\"com.wechat.testdemo.MainActivityFragment\"\n  tools:showIn=\"@layout/activity_main\">\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"Android TextView:\"/>\n\n    <com.lsjwzh.test.TestTextView\n      android:id=\"@+id/system_tv\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#6600ffff\"\n      android:ellipsize=\"end\"\n      android:maxLines=\"2\"\n      android:textSize=\"20sp\"/>\n\n    <TextView\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"FastTextLayoutView:\"/>\n\n    <com.lsjwzh.test.FastTextLayoutView\n      android:id=\"@+id/fast_tv\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#6600ffff\"\n      android:clickable=\"true\"/>\n\n    <View\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n  </LinearLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/normal_layout_ui.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n  <LinearLayout\n    android:id=\"@+id/op_layout\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_alignParentTop=\"true\"\n    android:layout_centerHorizontal=\"true\"\n    android:gravity=\"center\">\n\n    <Button\n      android:id=\"@+id/scroll_down_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollDown\"\n      />\n\n    <Button\n      android:id=\"@+id/scroll_up_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollUp\"/>\n  </LinearLayout>\n\n  <ListView\n    android:id=\"@+id/test_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@id/op_layout\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/normal_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.TestTextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                              android:id=\"@+id/normal_text\"\n                              android:layout_width=\"wrap_content\"\n                              android:layout_height=\"wrap_content\"\n                              android:textSize=\"25sp\">\n\n</com.lsjwzh.test.TestTextView>"
  },
  {
    "path": "app/src/main/res/layout/read_more_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  android:fillViewport=\"true\"\n  tools:context=\"com.wechat.testdemo.MainActivityFragment\"\n  tools:showIn=\"@layout/activity_main\">\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <TextView\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ReadMoreTextView:\"/>\n\n    <com.lsjwzh.widget.text.ReadMoreTextView\n      android:id=\"@+id/readmore_tv\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#6600ffff\"\n      android:clickable=\"true\"\n      android:ellipsize=\"end\"\n      android:maxLines=\"2\"\n      android:textSize=\"20sp\"/>\n\n\n    <View\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n  </LinearLayout>\n\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/readmore_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.widget.text.ReadMoreTextView\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:id=\"@+id/fast_text_view\"\n\tandroid:textSize=\"25sp\"\n\tandroid:ellipsize=\"end\"\n\tandroid:maxLines=\"2\"\n\tandroid:layout_width=\"wrap_content\"\n\tandroid:layout_height=\"wrap_content\"/>"
  },
  {
    "path": "app/src/main/res/layout/spinner_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:orientation=\"vertical\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\">\n\n  <TextView\n    android:id=\"@+id/text\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:textSize=\"20sp\"\n    android:textColor=\"#ffffff\"/>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/static_layout_ui.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n  >\n\n  <LinearLayout\n    android:id=\"@+id/op_layout\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_alignParentTop=\"true\"\n    android:layout_centerHorizontal=\"true\"\n    android:gravity=\"center\">\n\n    <Button\n      android:id=\"@+id/scroll_down_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollDown\"\n      />\n\n    <Button\n      android:id=\"@+id/scroll_up_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"ScrollUp\"/>\n  </LinearLayout>\n\n  <ListView\n    android:id=\"@+id/test_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_below=\"@id/op_layout\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/static_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.lsjwzh.test.FastTextLayoutView\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:id=\"@+id/static_layout_view\"\n\tandroid:layout_width=\"wrap_content\"\n\tandroid:layout_height=\"wrap_content\"/>"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n      xmlns:tools=\"http://schemas.android.com/tools\"\n      tools:context=\"com.wechat.testdemo.MainActivity\">\n  <item\n    android:id=\"@+id/action_settings\"\n    android:orderInCategory=\"100\"\n    android:title=\"@string/action_settings\"\n    app:showAsAction=\"never\"/>\n</menu>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"colorPrimary\">#3F51B5</color>\n  <color name=\"colorPrimaryDark\">#303F9F</color>\n  <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n  <dimen name=\"fab_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string name=\"app_name\">testdemo</string>\n  <string name=\"action_settings\">Settings</string>\n  <string name=\"content_eng\">Jarlsberg lancashire edam  .Dolcelatte hard cheese brie st. agur blue\n    cheese caerphilly bavarian bergkase cheese and biscuits mascarpone. Cheeseburger swiss bavarian\n    bergkase cream cheese fromage frais cheesy feet port-salut airedale. St. agur blue cheese rubber\n    cheese caerphilly cheddar cheesecake cream cheese manchego lancashire. Roquefort squirty cheese\n    the big cheese.</string>\n  <string name=\"content_cn\">东临碣石，以观沧海。水何澹澹，山岛竦峙。树木丛生，百草丰茂。秋风萧瑟，洪波涌起。日月之行，若出其中。星汉灿烂，若出其里。幸甚至哉，歌以咏志。</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n    <!-- Customize your theme here. -->\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n\n  <style name=\"AppTheme.NoActionBar\">\n    <item name=\"windowActionBar\">false</item>\n    <item name=\"windowNoTitle\">true</item>\n  </style>\n\n  <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\"/>\n\n  <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\">\n    \n  </style>\n\n  <style name=\"DropDownPopup\" />\n\n  <style name=\"DropDownPopup.DropDownDown\">\n    <item name=\"android:windowEnterAnimation\">@null</item>\n    <item name=\"android:windowExitAnimation\">@null</item>\n  </style>\n\n  <style name=\"DropDownPopup.DropDownUp\">\n    <item name=\"android:windowEnterAnimation\">@null</item>\n    <item name=\"android:windowExitAnimation\">@null</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v23/styles.xml",
    "content": "<resources>\n\n  <!-- Base application theme. -->\n  <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n    <!-- Customize your theme here. -->\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n  </style>\n\n  <style name=\"AppTheme.NoActionBar\">\n    <item name=\"windowActionBar\">false</item>\n    <item name=\"windowNoTitle\">true</item>\n  </style>\n\n  <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\"/>\n\n  <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\">\n    <item name=\"android:popupEnterTransition\">@null</item>\n    <item name=\"android:popupExitTransition\">@null</item>\n  </style>\n\n  <style name=\"DropDownPopup\" />\n\n  <style name=\"DropDownPopup.DropDownDown\">\n    <item name=\"android:windowEnterAnimation\">@anim/popup_enter</item>\n    <item name=\"android:windowExitAnimation\">@anim/popup_exit</item>\n  </style>\n\n  <style name=\"DropDownPopup.DropDownUp\">\n    <item name=\"android:windowEnterAnimation\">@anim/popup_enter</item>\n    <item name=\"android:windowExitAnimation\">@anim/popup_exit</item>\n  </style>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        mavenCentral()\n        jcenter()\n        google()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.5.1'\n        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'\n//        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'\n    }\n}\n\nProperties properties = new Properties()\nproperties.load(project.rootProject.file('local.properties').newDataInputStream())\nallprojects {\n    gradle.taskGraph.whenReady {\n        tasks.each { task ->\n            if (task.name.equals('createMockableJar')) {\n                task.enabled = false\n            }\n        }\n    }\n    repositories {\n        google()\n        maven {\n            url \"https://dl.bintray.com/lsjwzh/maven\"\n        }\n\n        maven {\n            url properties.getProperty(\"sdk.dir\") + \"/extras/android/m2repository\"\n        }\n        mavenCentral()\n        jcenter()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':widget.FastTextView'\n"
  },
  {
    "path": "text.Textline/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "text.Textline/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 25\n    buildToolsVersion \"25.0.3\"\n\n    defaultConfig {\n        minSdkVersion 15\n        targetSdkVersion 25\n        versionCode 4\n        versionName '1.0'\n\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    compile fileTree(dir: 'libs', include: ['*.jar'])\n\n}\n"
  },
  {
    "path": "text.Textline/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "text.Textline/src/main/AndroidManifest.xml",
    "content": "<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",
    "content": "package android.text;\n\n/**\n * Stores information about bidirectional (left-to-right or right-to-left)\n * text within the layout of a line.\n */\npublic class Directions {\n  // Directions represents directional runs within a line of text.\n  // Runs are pairs of ints listed in visual order, starting from the\n  // leading margin.  The first int of each pair is the offset from\n  // the first character of the line to the start of the run.  The\n  // second int represents both the length and level of the run.\n  // The length is in the lower bits, accessed by masking with\n  // DIR_LENGTH_MASK.  The level is in the higher bits, accessed\n  // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.\n  // To simply test for an RTL direction, test the bit using\n  // DIR_RTL_FLAG, if set then the direction is rtl.\n\n  /* package */ int[] mDirections;\n\n  /* package */ Directions(int[] dirs) {\n    mDirections = dirs;\n  }\n\n\n  /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;\n  /* package */ static final int RUN_LEVEL_SHIFT = 26;\n  /* package */ static final int RUN_LEVEL_MASK = 0x3f;\n  /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;\n  public static final Directions DIRS_ALL_LEFT_TO_RIGHT =\n      new Directions(new int[]{0, RUN_LENGTH_MASK});\n  public static final Directions DIRS_ALL_RIGHT_TO_LEFT =\n      new Directions(new int[]{0, RUN_LENGTH_MASK | RUN_RTL_FLAG});\n}"
  },
  {
    "path": "text.Textline/src/main/java/android/text/ITextLine.java",
    "content": "package android.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.text.Layout.TabStops;\n\npublic interface ITextLine {\n\n  /**\n   * Initializes a TextLine and prepares it for use.\n   *\n   * @param paint      the base paint for the line\n   * @param text       the text, can be Styled\n   * @param start      the start of the line relative to the text\n   * @param limit      the limit of the line relative to the text\n   * @param dir        the paragraph direction of this line\n   * @param directions the directions information of this line\n   */\n  void set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops);\n\n  /**\n   * Returns metrics information for the entire line.\n   *\n   * @param fmi receives font metrics information, can be null\n   * @return the signed width of the line\n   */\n  float metrics(Paint.FontMetricsInt fmi);\n\n  /**\n   * Renders the TextLine.\n   *\n   * @param c      the canvas to render on\n   * @param x      the leading margin position\n   * @param top    the top of the line\n   * @param y      the baseline\n   * @param bottom the bottom of the line\n   */\n  void draw(Canvas c, float x, int top, int y, int bottom);\n}\n"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineCompat.java",
    "content": "package android.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.text.Layout.TabStops;\n\n/**\n * Make TextLine can be accessed by other package.\n */\npublic class TextLineCompat implements ITextLine {\n  private ITextLine mTextLine;\n\n  private TextLineCompat(ITextLine textLine) {\n    mTextLine = textLine;\n  }\n\n  /**\n   * Returns a new TextLine from the shared pool.\n   *\n   * @return an uninitialized TextLine\n   */\n  public static TextLineCompat obtain() {\n\n    if (Build.VERSION.SDK_INT >= 23) {\n      return new TextLineCompat(TextLineImpl23.obtain());\n    } else {\n      return new TextLineCompat(TextLineImpl15.obtain());\n    }\n  }\n\n  /**\n   * Puts a TextLine back into the shared pool. Do not use this TextLine once\n   * it has been returned.\n   *\n   * @param tl the textLine\n   *           TextLine\n   */\n  public static void recycle(TextLineCompat tl) {\n    if (Build.VERSION.SDK_INT >= 23) {\n      TextLineImpl23.recycle((TextLineImpl23) tl.mTextLine);\n    } else {\n      TextLineImpl15.recycle((TextLineImpl15) tl.mTextLine);\n    }\n  }\n\n  /**\n   * Initializes a TextLine and prepares it for use.\n   *\n   * @param paint      the base paint for the line\n   * @param text       the text, can be Styled\n   * @param start      the start of the line relative to the text\n   * @param limit      the limit of the line relative to the text\n   * @param dir        the paragraph direction of this line\n   * @param directions the directions information of this line\n   */\n  @Override\n  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops) {\n    mTextLine.set(paint, text, start, limit, dir, directions, hasTabs, tabStops);\n  }\n\n  /**\n   * Returns metrics information for the entire line.\n   *\n   * @param fmi receives font metrics information, can be null\n   * @return the signed width of the line\n   */\n  public float metrics(Paint.FontMetricsInt fmi) {\n    return mTextLine.metrics(fmi);\n  }\n\n  @Override\n  public void draw(Canvas c, float x, int top, int y, int bottom) {\n    mTextLine.draw(c, x, top, y, bottom);\n  }\n}\n"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineImpl15.java",
    "content": "/*\n * Copyright (C) 2010 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage android.text;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Paint.FontMetricsInt;\nimport android.graphics.RectF;\nimport android.support.annotation.RequiresApi;\nimport android.text.Layout.TabStops;\nimport android.text.style.CharacterStyle;\nimport android.text.style.MetricAffectingSpan;\nimport android.text.style.ReplacementSpan;\nimport android.util.Log;\n\nimport java.lang.reflect.Array;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * Represents a line of styled text, for measuring in visual order and\n * for rendering.\n * <p>\n * <p>Get a new instance using obtain(), and when finished with it, return it\n * to the pool using recycle().\n * <p>\n * <p>Call set to prepare the instance for use, then either draw, measure,\n * metrics, or caretToLeftRightOf.\n */\n@RequiresApi(15)\npublic class TextLineImpl15 implements ITextLine {\n  private static final boolean DEBUG = false;\n  private static Method sDrawTextRun1;\n  private static Method sDrawTextRun2;\n  private static Method sGetTextRunAdvances1;\n  private static Method sGetTextRunAdvances2;\n\n  private TextPaint mPaint;\n  private CharSequence mText;\n  private int mStart;\n  private int mLen;\n  private int mDir;\n  private Directions mDirections;\n  private boolean mHasTabs;\n  private TabStops mTabs;\n  private char[] mChars;\n  private boolean mCharsValid;\n  private Spanned mSpanned;\n  private final TextPaint mWorkPaint = new TextPaint();\n  private final SpanSetCompat<MetricAffectingSpan> mMetricAffectingSpanSpanSetCompat =\n      new SpanSetCompat<MetricAffectingSpan>(MetricAffectingSpan.class);\n  private final SpanSetCompat<CharacterStyle> mCharacterStyleSpanSetCompat =\n      new SpanSetCompat<CharacterStyle>(CharacterStyle.class);\n  private final SpanSetCompat<ReplacementSpan> mReplacementSpanSpanSetCompat =\n      new SpanSetCompat<ReplacementSpan>(ReplacementSpan.class);\n\n  private static final TextLineImpl15[] sCached = new TextLineImpl15[3];\n\n  /**\n   * Returns a new TextLine from the shared pool.\n   *\n   * @return an uninitialized TextLine\n   */\n  static TextLineImpl15 obtain() {\n    TextLineImpl15 tl;\n    synchronized (sCached) {\n      for (int i = sCached.length; --i >= 0; ) {\n        if (sCached[i] != null) {\n          tl = sCached[i];\n          sCached[i] = null;\n          return tl;\n        }\n      }\n    }\n    tl = new TextLineImpl15();\n    if (DEBUG) {\n      Log.v(\"TLINE\", \"new: \" + tl);\n    }\n    return tl;\n  }\n\n  /**\n   * Puts a TextLine back into the shared pool. Do not use this TextLine once\n   * it has been returned.\n   *\n   * @param tl the textLine\n   * @return null, as a convenience from clearing references to the provided\n   * TextLine\n   */\n  static TextLineImpl15 recycle(TextLineImpl15 tl) {\n    tl.mText = null;\n    tl.mPaint = null;\n    tl.mDirections = null;\n\n    tl.mMetricAffectingSpanSpanSetCompat.recycle();\n    tl.mCharacterStyleSpanSetCompat.recycle();\n    tl.mReplacementSpanSpanSetCompat.recycle();\n\n    synchronized (sCached) {\n      for (int i = 0; i < sCached.length; ++i) {\n        if (sCached[i] == null) {\n          sCached[i] = tl;\n          break;\n        }\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Initializes a TextLine and prepares it for use.\n   *\n   * @param paint      the base paint for the line\n   * @param text       the text, can be Styled\n   * @param start      the start of the line relative to the text\n   * @param limit      the limit of the line relative to the text\n   * @param dir        the paragraph direction of this line\n   * @param directions the directions information of this line\n   * @param hasTabs    true if the line might contain tabs or emoji\n   * @param tabStops   the tabStops. Can be null.\n   */\n  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,\n                  Directions directions, boolean hasTabs, TabStops tabStops) {\n    mPaint = paint;\n    mText = text;\n    mStart = start;\n    mLen = limit - start;\n    mDir = dir;\n    mDirections = directions;\n    if (mDirections == null) {\n      throw new IllegalArgumentException(\"Directions cannot be null\");\n    }\n    mHasTabs = hasTabs;\n    mSpanned = null;\n\n    boolean hasReplacement = false;\n    if (text instanceof Spanned) {\n      mSpanned = (Spanned) text;\n      mReplacementSpanSpanSetCompat.init(mSpanned, start, limit);\n      hasReplacement = mReplacementSpanSpanSetCompat.numberOfSpans > 0;\n    }\n\n    mCharsValid = hasReplacement || hasTabs || directions != Directions.DIRS_ALL_LEFT_TO_RIGHT;\n\n    if (mCharsValid) {\n      if (mChars == null || mChars.length < mLen) {\n        mChars = new char[idealCharArraySize(mLen)];\n      }\n      TextUtils.getChars(text, start, limit, mChars, 0);\n      if (hasReplacement) {\n        // Handle these all at once so we don't have to do it as we go.\n        // Replace the first character of each replacement run with the\n        // object-replacement character and the remainder with zero width\n        // non-break space aka BOM.  Cursor movement code skips these\n        // zero-width characters.\n        char[] chars = mChars;\n        for (int i = start, inext; i < limit; i = inext) {\n          inext = mReplacementSpanSpanSetCompat.getNextTransition(i, limit);\n          if (mReplacementSpanSpanSetCompat.hasSpansIntersecting(i, inext)) {\n            // transition into a span\n            chars[i - start] = '\\ufffc';\n            for (int j = i - start + 1, e = inext - start; j < e; ++j) {\n              chars[j] = '\\ufeff'; // used as ZWNBS, marks positions to skip\n            }\n          }\n        }\n      }\n    }\n    mTabs = tabStops;\n  }\n\n  /**\n   * Renders the TextLine.\n   *\n   * @param c      the canvas to render on\n   * @param x      the leading margin position\n   * @param top    the top of the line\n   * @param y      the baseline\n   * @param bottom the bottom of the line\n   */\n  public void draw(Canvas c, float x, int top, int y, int bottom) {\n    if (!mHasTabs) {\n      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {\n        drawRun(c, 0, mLen, false, x, top, y, bottom, false);\n        return;\n      }\n      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {\n        drawRun(c, 0, mLen, true, x, top, y, bottom, false);\n        return;\n      }\n    }\n\n    float h = 0;\n    int[] runs = mDirections.mDirections;\n    RectF emojiRect = null;\n\n    int lastRunIndex = runs.length - 2;\n    for (int i = 0; i < runs.length; i += 2) {\n      int runStart = runs[i];\n      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);\n      if (runLimit > mLen) {\n        runLimit = mLen;\n      }\n      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;\n\n      int segstart = runStart;\n      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {\n        int codept = 0;\n        Bitmap bm = null;\n\n        if (mHasTabs && j < runLimit) {\n          codept = mChars[j];\n          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {\n            codept = Character.codePointAt(mChars, j);\n            if (codept > 0xFFFF) {\n              ++j;\n              continue;\n            }\n          }\n        }\n\n        if (j == runLimit || codept == '\\t' || bm != null) {\n          h += drawRun(c, segstart, j, runIsRtl, x + h, top, y, bottom,\n              i != lastRunIndex || j != mLen);\n\n          if (codept == '\\t') {\n            h = mDir * nextTab(h * mDir);\n          } else if (bm != null) {\n            float bmAscent = ascent(j);\n            float bitmapHeight = bm.getHeight();\n            float scale = -bmAscent / bitmapHeight;\n            float width = bm.getWidth() * scale;\n\n            if (emojiRect == null) {\n              emojiRect = new RectF();\n            }\n            emojiRect.set(x + h, y + bmAscent,\n                x + h + width, y);\n            c.drawBitmap(bm, null, emojiRect, mPaint);\n            h += width;\n            j++;\n          }\n          segstart = j + 1;\n        }\n      }\n    }\n  }\n\n  /**\n   * Returns metrics information for the entire line.\n   *\n   * @param fmi receives font metrics information, can be null\n   * @return the signed width of the line\n   */\n  public float metrics(FontMetricsInt fmi) {\n    return measure(mLen, false, fmi);\n  }\n\n  /**\n   * Returns information about a position on the line.\n   *\n   * @param offset   the line-relative character offset, between 0 and the\n   *                 line length, inclusive\n   * @param trailing true to measure the trailing edge of the character\n   *                 before offset, false to measure the leading edge of the character\n   *                 at offset.\n   * @param fmi      receives metrics information about the requested\n   *                 character, can be null.\n   * @return the signed offset from the leading margin to the requested\n   * character edge.\n   */\n  float measure(int offset, boolean trailing, FontMetricsInt fmi) {\n    int target = trailing ? offset - 1 : offset;\n    if (target < 0) {\n      return 0;\n    }\n\n    float h = 0;\n\n    if (!mHasTabs) {\n      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {\n        return measureRun(0, offset, mLen, false, fmi);\n      }\n      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {\n        return measureRun(0, offset, mLen, true, fmi);\n      }\n    }\n\n    char[] chars = mChars;\n    int[] runs = mDirections.mDirections;\n    for (int i = 0; i < runs.length; i += 2) {\n      int runStart = runs[i];\n      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);\n      if (runLimit > mLen) {\n        runLimit = mLen;\n      }\n      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;\n\n      int segstart = runStart;\n      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {\n        int codept = 0;\n        Bitmap bm = null;\n\n        if (mHasTabs && j < runLimit) {\n          codept = chars[j];\n          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {\n            codept = Character.codePointAt(mChars, j);\n            if (codept > 0xFFFF) {\n              ++j;\n              continue;\n            }\n          }\n        }\n\n        if (j == runLimit || codept == '\\t' || bm != null) {\n          boolean inSegment = target >= segstart && target < j;\n\n          boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;\n          if (inSegment && advance) {\n            return h += measureRun(segstart, offset, j, runIsRtl, fmi);\n          }\n\n          float w = measureRun(segstart, j, j, runIsRtl, fmi);\n          h += advance ? w : -w;\n\n          if (inSegment) {\n            return h += measureRun(segstart, offset, j, runIsRtl, null);\n          }\n\n          if (codept == '\\t') {\n            if (offset == j) {\n              return h;\n            }\n            h = mDir * nextTab(h * mDir);\n            if (target == j) {\n              return h;\n            }\n          }\n\n          if (bm != null) {\n            float bmAscent = ascent(j);\n            float wid = bm.getWidth() * -bmAscent / bm.getHeight();\n            h += mDir * wid;\n            j++;\n          }\n\n          segstart = j + 1;\n        }\n      }\n    }\n\n    return h;\n  }\n\n  /**\n   * Draws a unidirectional (but possibly multi-styled) run of text.\n   *\n   * @param c         the canvas to draw on\n   * @param start     the line-relative start\n   * @param limit     the line-relative limit\n   * @param runIsRtl  true if the run is right-to-left\n   * @param x         the position of the run that is closest to the leading margin\n   * @param top       the top of the line\n   * @param y         the baseline\n   * @param bottom    the bottom of the line\n   * @param needWidth true if the width value is required.\n   * @return the signed width of the run, based on the paragraph direction.\n   * Only valid if needWidth is true.\n   */\n  private float drawRun(Canvas c, int start,\n                        int limit, boolean runIsRtl, float x, int top, int y, int bottom,\n                        boolean needWidth) {\n\n    if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {\n      float w = -measureRun(start, limit, limit, runIsRtl, null);\n      handleRun(start, limit, limit, runIsRtl, c, x + w, top,\n          y, bottom, null, false);\n      return w;\n    }\n\n    return handleRun(start, limit, limit, runIsRtl, c, x, top,\n        y, bottom, null, needWidth);\n  }\n\n  /**\n   * Measures a unidirectional (but possibly multi-styled) run of text.\n   *\n   * @param start    the line-relative start of the run\n   * @param offset   the offset to measure to, between start and limit inclusive\n   * @param limit    the line-relative limit of the run\n   * @param runIsRtl true if the run is right-to-left\n   * @param fmi      receives metrics information about the requested\n   *                 run, can be null.\n   * @return the signed width from the start of the run to the leading edge\n   * of the character at offset, based on the run (not paragraph) direction\n   */\n  private float measureRun(int start, int offset, int limit, boolean runIsRtl,\n                           FontMetricsInt fmi) {\n    return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);\n  }\n\n  /**\n   * Walk the cursor through this line, skipping conjuncts and\n   * zero-width characters.\n   * <p>\n   * <p>This function cannot properly walk the cursor off the ends of the line\n   * since it does not know about any shaping on the previous/following line\n   * that might affect the cursor position. Callers must either avoid these\n   * situations or handle the result specially.\n   *\n   * @param cursor the starting position of the cursor, between 0 and the\n   *               length of the line, inclusive\n   * @param toLeft true if the caret is moving to the left.\n   * @return the new offset.  If it is less than 0 or greater than the length\n   * of the line, the previous/following line should be examined to get the\n   * actual offset.\n   */\n  int getOffsetToLeftRightOf(int cursor, boolean toLeft) {\n    // 1) The caret marks the leading edge of a character. The character\n    // logically before it might be on a different level, and the active caret\n    // position is on the character at the lower level. If that character\n    // was the previous character, the caret is on its trailing edge.\n    // 2) Take this character/edge and move it in the indicated direction.\n    // This gives you a new character and a new edge.\n    // 3) This position is between two visually adjacent characters.  One of\n    // these might be at a lower level.  The active position is on the\n    // character at the lower level.\n    // 4) If the active position is on the trailing edge of the character,\n    // the new caret position is the following logical character, else it\n    // is the character.\n\n    int lineStart = 0;\n    int lineEnd = mLen;\n    boolean paraIsRtl = mDir == -1;\n    int[] runs = mDirections.mDirections;\n\n    int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;\n    boolean trailing = false;\n\n    if (cursor == lineStart) {\n      runIndex = -2;\n    } else if (cursor == lineEnd) {\n      runIndex = runs.length;\n    } else {\n      // First, get information about the run containing the character with\n      // the active caret.\n      for (runIndex = 0; runIndex < runs.length; runIndex += 2) {\n        runStart = lineStart + runs[runIndex];\n        if (cursor >= runStart) {\n          runLimit = runStart + (runs[runIndex + 1] & Layout.RUN_LENGTH_MASK);\n          if (runLimit > lineEnd) {\n            runLimit = lineEnd;\n          }\n          if (cursor < runLimit) {\n            runLevel = (runs[runIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &\n                Layout.RUN_LEVEL_MASK;\n            if (cursor == runStart) {\n              // The caret is on a run boundary, see if we should\n              // use the position on the trailing edge of the previous\n              // logical character instead.\n              int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;\n              int pos = cursor - 1;\n              for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {\n                prevRunStart = lineStart + runs[prevRunIndex];\n                if (pos >= prevRunStart) {\n                  prevRunLimit = prevRunStart +\n                      (runs[prevRunIndex + 1] & Layout.RUN_LENGTH_MASK);\n                  if (prevRunLimit > lineEnd) {\n                    prevRunLimit = lineEnd;\n                  }\n                  if (pos < prevRunLimit) {\n                    prevRunLevel = (runs[prevRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT)\n                        & Layout.RUN_LEVEL_MASK;\n                    if (prevRunLevel < runLevel) {\n                      // Start from logically previous character.\n                      runIndex = prevRunIndex;\n                      runLevel = prevRunLevel;\n                      runStart = prevRunStart;\n                      runLimit = prevRunLimit;\n                      trailing = true;\n                      break;\n                    }\n                  }\n                }\n              }\n            }\n            break;\n          }\n        }\n      }\n\n      // caret might be == lineEnd.  This is generally a space or paragraph\n      // separator and has an associated run, but might be the end of\n      // text, in which case it doesn't.  If that happens, we ran off the\n      // end of the run list, and runIndex == runs.length.  In this case,\n      // we are at a run boundary so we skip the below test.\n      if (runIndex != runs.length) {\n        boolean runIsRtl = (runLevel & 0x1) != 0;\n        boolean advance = toLeft == runIsRtl;\n        if (cursor != (advance ? runLimit : runStart) || advance != trailing) {\n          // Moving within or into the run, so we can move logically.\n          newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,\n              runIsRtl, cursor, advance);\n          // If the new position is internal to the run, we're at the strong\n          // position already so we're finished.\n          if (newCaret != (advance ? runLimit : runStart)) {\n            return newCaret;\n          }\n        }\n      }\n    }\n\n    // If newCaret is -1, we're starting at a run boundary and crossing\n    // into another run. Otherwise we've arrived at a run boundary, and\n    // need to figure out which character to attach to.  Note we might\n    // need to run this twice, if we cross a run boundary and end up at\n    // another run boundary.\n    while (true) {\n      boolean advance = toLeft == paraIsRtl;\n      int otherRunIndex = runIndex + (advance ? 2 : -2);\n      if (otherRunIndex >= 0 && otherRunIndex < runs.length) {\n        int otherRunStart = lineStart + runs[otherRunIndex];\n        int otherRunLimit = otherRunStart +\n            (runs[otherRunIndex + 1] & Layout.RUN_LENGTH_MASK);\n        if (otherRunLimit > lineEnd) {\n          otherRunLimit = lineEnd;\n        }\n        int otherRunLevel = (runs[otherRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &\n            Layout.RUN_LEVEL_MASK;\n        boolean otherRunIsRtl = (otherRunLevel & 1) != 0;\n\n        advance = toLeft == otherRunIsRtl;\n        if (newCaret == -1) {\n          newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,\n              otherRunLimit, otherRunIsRtl,\n              advance ? otherRunStart : otherRunLimit, advance);\n          if (newCaret == (advance ? otherRunLimit : otherRunStart)) {\n            // Crossed and ended up at a new boundary,\n            // repeat a second and final time.\n            runIndex = otherRunIndex;\n            runLevel = otherRunLevel;\n            continue;\n          }\n          break;\n        }\n\n        // The new caret is at a boundary.\n        if (otherRunLevel < runLevel) {\n          // The strong character is in the other run.\n          newCaret = advance ? otherRunStart : otherRunLimit;\n        }\n        break;\n      }\n\n      if (newCaret == -1) {\n        // We're walking off the end of the line.  The paragraph\n        // level is always equal to or lower than any internal level, so\n        // the boundaries get the strong caret.\n        newCaret = advance ? mLen + 1 : -1;\n        break;\n      }\n\n      // Else we've arrived at the end of the line.  That's a strong position.\n      // We might have arrived here by crossing over a run with no internal\n      // breaks and dropping out of the above loop before advancing one final\n      // time, so reset the caret.\n      // Note, we use '<=' below to handle a situation where the only run\n      // on the line is a counter-directional run.  If we're not advancing,\n      // we can end up at the 'lineEnd' position but the caret we want is at\n      // the lineStart.\n      if (newCaret <= lineEnd) {\n        newCaret = advance ? lineEnd : lineStart;\n      }\n      break;\n    }\n\n    return newCaret;\n  }\n\n  /**\n   * Returns the next valid offset within this directional run, skipping\n   * conjuncts and zero-width characters.  This should not be called to walk\n   * off the end of the line, since the returned values might not be valid\n   * on neighboring lines.  If the returned offset is less than zero or\n   * greater than the line length, the offset should be recomputed on the\n   * preceding or following line, respectively.\n   *\n   * @param runIndex the run index\n   * @param runStart the start of the run\n   * @param runLimit the limit of the run\n   * @param runIsRtl true if the run is right-to-left\n   * @param offset   the offset\n   * @param after    true if the new offset should logically follow the provided\n   *                 offset\n   * @return the new offset\n   */\n  private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,\n                                   boolean runIsRtl, int offset, boolean after) {\n\n    if (runIndex < 0 || offset == (after ? mLen : 0)) {\n      // Walking off end of line.  Since we don't know\n      // what cursor positions are available on other lines, we can't\n      // return accurate values.  These are a guess.\n      if (after) {\n        return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;\n      }\n      return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;\n    }\n\n    TextPaint wp = mWorkPaint;\n    wp.set(mPaint);\n\n    int spanStart = runStart;\n    int spanLimit;\n    if (mSpanned == null) {\n      spanLimit = runLimit;\n    } else {\n      int target = after ? offset + 1 : offset;\n      int limit = mStart + runLimit;\n      while (true) {\n        spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,\n            MetricAffectingSpan.class) - mStart;\n        if (spanLimit >= target) {\n          break;\n        }\n        spanStart = spanLimit;\n      }\n\n      MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,\n          mStart + spanLimit, MetricAffectingSpan.class);\n      spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);\n\n      if (spans.length > 0) {\n        ReplacementSpan replacement = null;\n        for (int j = 0; j < spans.length; j++) {\n          MetricAffectingSpan span = spans[j];\n          if (span instanceof ReplacementSpan) {\n            replacement = (ReplacementSpan) span;\n          } else {\n            span.updateMeasureState(wp);\n          }\n        }\n\n        if (replacement != null) {\n          // If we have a replacement span, we're moving either to\n          // the start or end of this span.\n          return after ? spanLimit : spanStart;\n        }\n      }\n    }\n\n    int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;\n    int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;\n    if (mCharsValid) {\n      return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,\n          flags, offset, cursorOpt);\n    } else {\n      return wp.getTextRunCursor(mText, mStart + spanStart,\n          mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart;\n    }\n  }\n\n  /**\n   * @param wp\n   */\n  private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {\n    final int previousTop = fmi.top;\n    final int previousAscent = fmi.ascent;\n    final int previousDescent = fmi.descent;\n    final int previousBottom = fmi.bottom;\n    final int previousLeading = fmi.leading;\n\n    wp.getFontMetricsInt(fmi);\n\n    updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,\n        previousLeading);\n  }\n\n  static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,\n                            int previousDescent, int previousBottom, int previousLeading) {\n    fmi.top = Math.min(fmi.top, previousTop);\n    fmi.ascent = Math.min(fmi.ascent, previousAscent);\n    fmi.descent = Math.max(fmi.descent, previousDescent);\n    fmi.bottom = Math.max(fmi.bottom, previousBottom);\n    fmi.leading = Math.max(fmi.leading, previousLeading);\n  }\n\n  /**\n   * Utility function for measuring and rendering text.  The text must\n   * not include a tab or emoji.\n   *\n   * @param wp        the working paint\n   * @param start     the start of the text\n   * @param end       the end of the text\n   * @param runIsRtl  true if the run is right-to-left\n   * @param c         the canvas, can be null if rendering is not needed\n   * @param x         the edge of the run closest to the leading margin\n   * @param top       the top of the line\n   * @param y         the baseline\n   * @param bottom    the bottom of the line\n   * @param fmi       receives metrics information, can be null\n   * @param needWidth true if the width of the run is needed\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleText(TextPaint wp, int start, int end,\n                           int contextStart, int contextEnd, boolean runIsRtl,\n                           Canvas c, float x, int top, int y, int bottom,\n                           FontMetricsInt fmi, boolean needWidth) {\n\n    // Get metrics first (even for empty strings or \"0\" width runs)\n    if (fmi != null) {\n      expandMetricsFromPaint(fmi, wp);\n    }\n\n    int runLen = end - start;\n    // No need to do anything if the run width is \"0\"\n    if (runLen == 0) {\n      return 0f;\n    }\n\n    float ret = 0;\n\n    int contextLen = contextEnd - contextStart;\n    if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {\n      int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;\n      ret = getTextRunAdvances(wp, start, end, contextStart, contextEnd, runLen, contextLen, flags);\n    }\n\n    if (c != null) {\n      if (runIsRtl) {\n        x -= ret;\n      }\n\n      if (wp.bgColor != 0) {\n        int previousColor = wp.getColor();\n        Paint.Style previousStyle = wp.getStyle();\n\n        wp.setColor(wp.bgColor);\n        wp.setStyle(Paint.Style.FILL);\n        c.drawRect(x, top, x + ret, bottom, wp);\n\n        wp.setStyle(previousStyle);\n        wp.setColor(previousColor);\n      }\n\n      if (wp.underlineColor != 0) {\n        // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h\n        float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();\n\n        int previousColor = wp.getColor();\n        Paint.Style previousStyle = wp.getStyle();\n        boolean previousAntiAlias = wp.isAntiAlias();\n\n        wp.setStyle(Paint.Style.FILL);\n        wp.setAntiAlias(true);\n\n        wp.setColor(wp.underlineColor);\n        c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);\n\n        wp.setStyle(previousStyle);\n        wp.setColor(previousColor);\n        wp.setAntiAlias(previousAntiAlias);\n      }\n\n      drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,\n          x, y + wp.baselineShift);\n    }\n\n    return runIsRtl ? -ret : ret;\n  }\n\n  private float getTextRunAdvances(TextPaint wp, int start, int end, int contextStart, int contextEnd, int runLen, int contextLen, int flags) {\n    if (sGetTextRunAdvances1 == null) {\n      try {\n        sGetTextRunAdvances1 = wp.getClass().getDeclaredMethod(\"getTextRunAdvances\",\n            char[].class, int.class, int.class, int.class, int.class, int.class, float[].class, int.class);\n      } catch (NoSuchMethodException e) {\n        e.printStackTrace();\n      }\n    }\n    if (sGetTextRunAdvances2 == null) {\n      try {\n        sGetTextRunAdvances2 = wp.getClass().getDeclaredMethod(\"drawTextRun\",\n            CharSequence.class, int.class, int.class, int.class, int.class, int.class, float[].class, int.class);\n      } catch (NoSuchMethodException e) {\n        e.printStackTrace();\n      }\n    }\n    float ret = 0;\n    if (mCharsValid) {\n      try {\n        ret = (float) sGetTextRunAdvances1.invoke(wp, mChars, start, runLen,\n            contextStart, contextLen, flags, null, 0);\n      } catch (IllegalAccessException e) {\n        e.printStackTrace();\n      } catch (InvocationTargetException e) {\n        e.printStackTrace();\n      }\n    } else {\n      int delta = mStart;\n      try {\n        ret = (float) sGetTextRunAdvances1.invoke(wp, mText, delta + start,\n            delta + end, delta + contextStart, delta + contextEnd,\n            flags, null, 0);\n      } catch (IllegalAccessException e) {\n        e.printStackTrace();\n      } catch (InvocationTargetException e) {\n        e.printStackTrace();\n      }\n    }\n    return ret;\n  }\n\n  /**\n   * Utility function for measuring and rendering a replacement.\n   *\n   * @param replacement the replacement\n   * @param wp          the work paint\n   * @param start       the start of the run\n   * @param limit       the limit of the run\n   * @param runIsRtl    true if the run is right-to-left\n   * @param c           the canvas, can be null if not rendering\n   * @param x           the edge of the replacement closest to the leading margin\n   * @param top         the top of the line\n   * @param y           the baseline\n   * @param bottom      the bottom of the line\n   * @param fmi         receives metrics information, can be null\n   * @param needWidth   true if the width of the replacement is needed\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleReplacement(ReplacementSpan replacement, TextPaint wp,\n                                  int start, int limit, boolean runIsRtl, Canvas c,\n                                  float x, int top, int y, int bottom, FontMetricsInt fmi,\n                                  boolean needWidth) {\n\n    float ret = 0;\n\n    int textStart = mStart + start;\n    int textLimit = mStart + limit;\n\n    if (needWidth || (c != null && runIsRtl)) {\n      int previousTop = 0;\n      int previousAscent = 0;\n      int previousDescent = 0;\n      int previousBottom = 0;\n      int previousLeading = 0;\n\n      boolean needUpdateMetrics = (fmi != null);\n\n      if (needUpdateMetrics) {\n        previousTop = fmi.top;\n        previousAscent = fmi.ascent;\n        previousDescent = fmi.descent;\n        previousBottom = fmi.bottom;\n        previousLeading = fmi.leading;\n      }\n\n      ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);\n\n      if (needUpdateMetrics) {\n        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,\n            previousLeading);\n      }\n    }\n\n    if (c != null) {\n      if (runIsRtl) {\n        x -= ret;\n      }\n      replacement.draw(c, mText, textStart, textLimit,\n          x, top, y, bottom, wp);\n    }\n\n    return runIsRtl ? -ret : ret;\n  }\n\n  private static class SpanSetCompat<E> {\n    int numberOfSpans;\n    E[] spans;\n    int[] spanStarts;\n    int[] spanEnds;\n    int[] spanFlags;\n    final Class<? extends E> classType;\n\n    SpanSetCompat(Class<? extends E> type) {\n      classType = type;\n      numberOfSpans = 0;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void init(Spanned spanned, int start, int limit) {\n      final E[] allSpans = spanned.getSpans(start, limit, classType);\n      final int length = allSpans.length;\n\n      if (length > 0 && (spans == null || spans.length < length)) {\n        // These arrays may end up being too large because of empty spans\n        spans = (E[]) Array.newInstance(classType, length);\n        spanStarts = new int[length];\n        spanEnds = new int[length];\n        spanFlags = new int[length];\n      }\n\n      numberOfSpans = 0;\n      for (int i = 0; i < length; i++) {\n        final E span = allSpans[i];\n\n        final int spanStart = spanned.getSpanStart(span);\n        final int spanEnd = spanned.getSpanEnd(span);\n        if (spanStart == spanEnd) continue;\n\n        final int spanFlag = spanned.getSpanFlags(span);\n\n        spans[numberOfSpans] = span;\n        spanStarts[numberOfSpans] = spanStart;\n        spanEnds[numberOfSpans] = spanEnd;\n        spanFlags[numberOfSpans] = spanFlag;\n\n        numberOfSpans++;\n      }\n    }\n\n    public boolean hasSpansIntersecting(int start, int end) {\n      for (int i = 0; i < numberOfSpans; i++) {\n        // equal test is valid since both intervals are not empty by construction\n        if (spanStarts[i] >= end || spanEnds[i] <= start) continue;\n        return true;\n      }\n      return false;\n    }\n\n    int getNextTransition(int start, int limit) {\n      for (int i = 0; i < numberOfSpans; i++) {\n        final int spanStart = spanStarts[i];\n        final int spanEnd = spanEnds[i];\n        if (spanStart > start && spanStart < limit) limit = spanStart;\n        if (spanEnd > start && spanEnd < limit) limit = spanEnd;\n      }\n      return limit;\n    }\n\n    public void recycle() {\n      // The spans array is guaranteed to be not null when numberOfSpans is > 0\n      for (int i = 0; i < numberOfSpans; i++) {\n        spans[i] = null; // prevent a leak: no reference kept when TextLine is recycled\n      }\n    }\n  }\n\n  /**\n   * Utility function for handling a unidirectional run.  The run must not\n   * contain tabs or emoji but can contain styles.\n   *\n   * @param start        the line-relative start of the run\n   * @param measureLimit the offset to measure to, between start and limit inclusive\n   * @param limit        the limit of the run\n   * @param runIsRtl     true if the run is right-to-left\n   * @param c            the canvas, can be null\n   * @param x            the end of the run closest to the leading margin\n   * @param top          the top of the line\n   * @param y            the baseline\n   * @param bottom       the bottom of the line\n   * @param fmi          receives metrics information, can be null\n   * @param needWidth    true if the width is required\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleRun(int start, int measureLimit,\n                          int limit, boolean runIsRtl, Canvas c, float x, int top, int y,\n                          int bottom, FontMetricsInt fmi, boolean needWidth) {\n\n    // Case of an empty line, make sure we update fmi according to mPaint\n    if (start == measureLimit) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n      if (fmi != null) {\n        expandMetricsFromPaint(fmi, wp);\n      }\n      return 0f;\n    }\n\n    if (mSpanned == null) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n      final int mlimit = measureLimit;\n      return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,\n          y, bottom, fmi, needWidth || mlimit < measureLimit);\n    }\n\n    mMetricAffectingSpanSpanSetCompat.init(mSpanned, mStart + start, mStart + limit);\n    mCharacterStyleSpanSetCompat.init(mSpanned, mStart + start, mStart + limit);\n\n    // Shaping needs to take into account context up to metric boundaries,\n    // but rendering needs to take into account character style boundaries.\n    // So we iterate through metric runs to get metric bounds,\n    // then within each metric run iterate through character style runs\n    // for the run bounds.\n    final float originalX = x;\n    for (int i = start, inext; i < measureLimit; i = inext) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n\n      inext = mMetricAffectingSpanSpanSetCompat.getNextTransition(mStart + i, mStart + limit) -\n          mStart;\n      int mlimit = Math.min(inext, measureLimit);\n\n      ReplacementSpan replacement = null;\n\n      for (int j = 0; j < mMetricAffectingSpanSpanSetCompat.numberOfSpans; j++) {\n        // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT\n        // empty by construction. This special case in getSpans() explains the >= & <= tests\n        if ((mMetricAffectingSpanSpanSetCompat.spanStarts[j] >= mStart + mlimit) ||\n            (mMetricAffectingSpanSpanSetCompat.spanEnds[j] <= mStart + i)) continue;\n        MetricAffectingSpan span = mMetricAffectingSpanSpanSetCompat.spans[j];\n        if (span instanceof ReplacementSpan) {\n          replacement = (ReplacementSpan) span;\n        } else {\n          // We might have a replacement that uses the draw\n          // state, otherwise measure state would suffice.\n          span.updateDrawState(wp);\n        }\n      }\n\n      if (replacement != null) {\n        x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,\n            bottom, fmi, needWidth || mlimit < measureLimit);\n        continue;\n      }\n\n      if (c == null) {\n        x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,\n            y, bottom, fmi, needWidth || mlimit < measureLimit);\n      } else {\n        for (int j = i, jnext; j < mlimit; j = jnext) {\n          jnext = mCharacterStyleSpanSetCompat.getNextTransition(mStart + j, mStart + mlimit) -\n              mStart;\n\n          wp.set(mPaint);\n          for (int k = 0; k < mCharacterStyleSpanSetCompat.numberOfSpans; k++) {\n            // Intentionally using >= and <= as explained above\n            if ((mCharacterStyleSpanSetCompat.spanStarts[k] >= mStart + jnext) ||\n                (mCharacterStyleSpanSetCompat.spanEnds[k] <= mStart + j)) continue;\n\n            CharacterStyle span = mCharacterStyleSpanSetCompat.spans[k];\n            span.updateDrawState(wp);\n          }\n\n          x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,\n              top, y, bottom, fmi, needWidth || jnext < measureLimit);\n        }\n      }\n    }\n\n    return x - originalX;\n  }\n\n  /**\n   * Render a text run with the set-up paint.\n   *\n   * @param c            the canvas\n   * @param wp           the paint used to render the text\n   * @param start        the start of the run\n   * @param end          the end of the run\n   * @param contextStart the start of context for the run\n   * @param contextEnd   the end of the context for the run\n   * @param runIsRtl     true if the run is right-to-left\n   * @param x            the x position of the left edge of the run\n   * @param y            the baseline of the run\n   */\n  private void drawTextRun(Canvas c, TextPaint wp, int start, int end,\n                           int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {\n    if (sDrawTextRun1 == null) {\n      try {\n        sDrawTextRun1 = c.getClass().getDeclaredMethod(\"drawTextRun\",\n            char[].class, int.class, int.class, int.class, int.class, float.class, float.class, int.class, Paint.class);\n      } catch (NoSuchMethodException e) {\n        e.printStackTrace();\n      }\n    }\n    if (sDrawTextRun2 == null) {\n      try {\n        sDrawTextRun2 = c.getClass().getDeclaredMethod(\"drawTextRun\",\n            CharSequence.class, int.class, int.class, int.class, int.class, float.class, float.class, int.class, Paint.class);\n      } catch (NoSuchMethodException e) {\n        e.printStackTrace();\n      }\n    }\n    int flags = runIsRtl ? 1 : 0;\n    if (mCharsValid) {\n      int count = end - start;\n      int contextCount = contextEnd - contextStart;\n      try {\n        sDrawTextRun1.invoke(c, mChars, start, count, contextStart, contextCount,\n            x, y, flags, wp);\n      } catch (IllegalAccessException e) {\n        e.printStackTrace();\n      } catch (InvocationTargetException e) {\n        e.printStackTrace();\n      }\n    } else {\n      int delta = mStart;\n      try {\n        sDrawTextRun2.invoke(c, mText, delta + start, delta + end,\n            delta + contextStart, delta + contextEnd, x, y, flags, wp);\n      } catch (IllegalAccessException e) {\n        e.printStackTrace();\n      } catch (InvocationTargetException e) {\n        e.printStackTrace();\n      }\n    }\n  }\n\n  /**\n   * Returns the ascent of the text at start.  This is used for scaling\n   * emoji.\n   *\n   * @param pos the line-relative position\n   * @return the ascent of the text at start\n   */\n  float ascent(int pos) {\n    if (mSpanned == null) {\n      return mPaint.ascent();\n    }\n\n    pos += mStart;\n    MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);\n    if (spans.length == 0) {\n      return mPaint.ascent();\n    }\n\n    TextPaint wp = mWorkPaint;\n    wp.set(mPaint);\n    for (MetricAffectingSpan span : spans) {\n      span.updateMeasureState(wp);\n    }\n    return wp.ascent();\n  }\n\n  /**\n   * Returns the next tab position.\n   *\n   * @param h the (unsigned) offset from the leading margin\n   * @return the (unsigned) tab position after this offset\n   */\n  float nextTab(float h) {\n    if (mTabs != null) {\n      return mTabs.nextTab(h);\n    }\n    return TabStops.nextDefaultStop(h, TAB_INCREMENT);\n  }\n\n  private static final int TAB_INCREMENT = 20;\n\n\n  public static int idealByteArraySize(int need) {\n    for (int i = 4; i < 32; i++)\n      if (need <= (1 << i) - 12)\n        return (1 << i) - 12;\n\n    return need;\n  }\n\n  public static int idealBooleanArraySize(int need) {\n    return idealByteArraySize(need);\n  }\n\n  public static int idealShortArraySize(int need) {\n    return idealByteArraySize(need * 2) / 2;\n  }\n\n  public static int idealCharArraySize(int need) {\n    return idealByteArraySize(need * 2) / 2;\n  }\n}"
  },
  {
    "path": "text.Textline/src/main/java/android/text/TextLineImpl23.java",
    "content": "/*\n * Copyright (C) 2010 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage android.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Paint.FontMetricsInt;\nimport android.support.annotation.RequiresApi;\nimport android.text.Layout.TabStops;\nimport android.text.style.CharacterStyle;\nimport android.text.style.MetricAffectingSpan;\nimport android.text.style.ReplacementSpan;\nimport android.util.Log;\n\nimport com.android.internal.util.ArrayUtils;\n\n/**\n * Represents a line of styled text, for measuring in visual order and\n * for rendering.\n * <p>\n * <p>Get a new instance using obtain(), and when finished with it, return it\n * to the pool using recycle().\n * <p>\n * <p>Call set to prepare the instance for use, then either draw, measure,\n * metrics, or caretToLeftRightOf.\n */\n@RequiresApi(23)\npublic class TextLineImpl23 implements ITextLine {\n  private static final boolean DEBUG = false;\n\n  private TextPaint mPaint;\n  private CharSequence mText;\n  private int mStart;\n  private int mLen;\n  private int mDir;\n  private Directions mDirections;\n  private boolean mHasTabs;\n  private TabStops mTabs;\n  private char[] mChars;\n  private boolean mCharsValid;\n  private Spanned mSpanned;\n  private final TextPaint mWorkPaint = new TextPaint();\n  private final SpanSetCompat<MetricAffectingSpan> mMetricAffectingSpanSpanSet =\n      new SpanSetCompat<MetricAffectingSpan>(MetricAffectingSpan.class);\n  private final SpanSetCompat<CharacterStyle> mCharacterStyleSpanSet =\n      new SpanSetCompat<CharacterStyle>(CharacterStyle.class);\n  private final SpanSetCompat<ReplacementSpan> mReplacementSpanSpanSet =\n      new SpanSetCompat<ReplacementSpan>(ReplacementSpan.class);\n\n  private static final TextLineImpl23[] sCached = new TextLineImpl23[3];\n\n  /**\n   * Returns a new TextLine from the shared pool.\n   *\n   * @return an uninitialized TextLine\n   */\n  public static TextLineImpl23 obtain() {\n    TextLineImpl23 tl;\n    synchronized (sCached) {\n      for (int i = sCached.length; --i >= 0; ) {\n        if (sCached[i] != null) {\n          tl = sCached[i];\n          sCached[i] = null;\n          return tl;\n        }\n      }\n    }\n    tl = new TextLineImpl23();\n    if (DEBUG) {\n      Log.v(\"TLINE\", \"new: \" + tl);\n    }\n    return tl;\n  }\n\n  /**\n   * Puts a TextLine back into the shared pool. Do not use this TextLine once\n   * it has been returned.\n   *\n   * @param tl the textLine\n   * @return null, as a convenience from clearing references to the provided\n   * TextLine\n   */\n  public static TextLineImpl23 recycle(TextLineImpl23 tl) {\n    tl.mText = null;\n    tl.mPaint = null;\n    tl.mDirections = null;\n    tl.mSpanned = null;\n    tl.mTabs = null;\n    tl.mChars = null;\n\n    tl.mMetricAffectingSpanSpanSet.recycle();\n    tl.mCharacterStyleSpanSet.recycle();\n    tl.mReplacementSpanSpanSet.recycle();\n\n    synchronized (sCached) {\n      for (int i = 0; i < sCached.length; ++i) {\n        if (sCached[i] == null) {\n          sCached[i] = tl;\n          break;\n        }\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Initializes a TextLine and prepares it for use.\n   *\n   * @param paint      the base paint for the line\n   * @param text       the text, can be Styled\n   * @param start      the start of the line relative to the text\n   * @param limit      the limit of the line relative to the text\n   * @param dir        the paragraph direction of this line\n   * @param directions the directions information of this line\n   * @param hasTabs    true if the line might contain tabs\n   * @param tabStops   the tabStops. Can be null.\n   */\n  public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,\n                  Directions directions, boolean hasTabs, TabStops tabStops) {\n    mPaint = paint;\n    mText = text;\n    mStart = start;\n    mLen = limit - start;\n    mDir = dir;\n    mDirections = directions;\n    if (mDirections == null) {\n      throw new IllegalArgumentException(\"Directions cannot be null\");\n    }\n    mHasTabs = hasTabs;\n    mSpanned = null;\n\n    boolean hasReplacement = false;\n    if (text instanceof Spanned) {\n      mSpanned = (Spanned) text;\n      mReplacementSpanSpanSet.init(mSpanned, start, limit);\n      hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;\n    }\n\n    mCharsValid = hasReplacement || hasTabs || directions != Directions.DIRS_ALL_LEFT_TO_RIGHT;\n\n    if (mCharsValid) {\n      if (mChars == null || mChars.length < mLen) {\n        mChars = ArrayUtils.newUnpaddedCharArray(mLen);\n      }\n      TextUtils.getChars(text, start, limit, mChars, 0);\n      if (hasReplacement) {\n        // Handle these all at once so we don't have to do it as we go.\n        // Replace the first character of each replacement run with the\n        // object-replacement character and the remainder with zero width\n        // non-break space aka BOM.  Cursor movement code skips these\n        // zero-width characters.\n        char[] chars = mChars;\n        for (int i = start, inext; i < limit; i = inext) {\n          inext = mReplacementSpanSpanSet.getNextTransition(i, limit);\n          if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {\n            // transition into a span\n            chars[i - start] = '\\ufffc';\n            for (int j = i - start + 1, e = inext - start; j < e; ++j) {\n              chars[j] = '\\ufeff'; // used as ZWNBS, marks positions to skip\n            }\n          }\n        }\n      }\n    }\n    mTabs = tabStops;\n  }\n\n  /**\n   * Renders the TextLine.\n   *\n   * @param c      the canvas to render on\n   * @param x      the leading margin position\n   * @param top    the top of the line\n   * @param y      the baseline\n   * @param bottom the bottom of the line\n   */\n  public void draw(Canvas c, float x, int top, int y, int bottom) {\n    if (!mHasTabs) {\n      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {\n        drawRun(c, 0, mLen, false, x, top, y, bottom, false);\n        return;\n      }\n      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {\n        drawRun(c, 0, mLen, true, x, top, y, bottom, false);\n        return;\n      }\n    }\n\n    float h = 0;\n    int[] runs = mDirections.mDirections;\n\n    int lastRunIndex = runs.length - 2;\n    for (int i = 0; i < runs.length; i += 2) {\n      int runStart = runs[i];\n      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);\n      if (runLimit > mLen) {\n        runLimit = mLen;\n      }\n      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;\n\n      int segstart = runStart;\n      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {\n        int codept = 0;\n        if (mHasTabs && j < runLimit) {\n          codept = mChars[j];\n          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {\n            codept = Character.codePointAt(mChars, j);\n            if (codept > 0xFFFF) {\n              ++j;\n              continue;\n            }\n          }\n        }\n\n        if (j == runLimit || codept == '\\t') {\n          h += drawRun(c, segstart, j, runIsRtl, x + h, top, y, bottom,\n              i != lastRunIndex || j != mLen);\n\n          if (codept == '\\t') {\n            h = mDir * nextTab(h * mDir);\n          }\n          segstart = j + 1;\n        }\n      }\n    }\n  }\n\n  /**\n   * Returns metrics information for the entire line.\n   *\n   * @param fmi receives font metrics information, can be null\n   * @return the signed width of the line\n   */\n  public float metrics(FontMetricsInt fmi) {\n    return measure(mLen, false, fmi);\n  }\n\n  /**\n   * Returns information about a position on the line.\n   *\n   * @param offset   the line-relative character offset, between 0 and the\n   *                 line length, inclusive\n   * @param trailing true to measure the trailing edge of the character\n   *                 before offset, false to measure the leading edge of the character\n   *                 at offset.\n   * @param fmi      receives metrics information about the requested\n   *                 character, can be null.\n   * @return the signed offset from the leading margin to the requested\n   * character edge.\n   */\n  float measure(int offset, boolean trailing, FontMetricsInt fmi) {\n    int target = trailing ? offset - 1 : offset;\n    if (target < 0) {\n      return 0;\n    }\n\n    float h = 0;\n\n    if (!mHasTabs) {\n      if (mDirections == Directions.DIRS_ALL_LEFT_TO_RIGHT) {\n        return measureRun(0, offset, mLen, false, fmi);\n      }\n      if (mDirections == Directions.DIRS_ALL_RIGHT_TO_LEFT) {\n        return measureRun(0, offset, mLen, true, fmi);\n      }\n    }\n\n    char[] chars = mChars;\n    int[] runs = mDirections.mDirections;\n    for (int i = 0; i < runs.length; i += 2) {\n      int runStart = runs[i];\n      int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);\n      if (runLimit > mLen) {\n        runLimit = mLen;\n      }\n      boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;\n\n      int segstart = runStart;\n      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {\n        int codept = 0;\n        if (mHasTabs && j < runLimit) {\n          codept = chars[j];\n          if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {\n            codept = Character.codePointAt(chars, j);\n            if (codept > 0xFFFF) {\n              ++j;\n              continue;\n            }\n          }\n        }\n\n        if (j == runLimit || codept == '\\t') {\n          boolean inSegment = target >= segstart && target < j;\n\n          boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;\n          if (inSegment && advance) {\n            return h += measureRun(segstart, offset, j, runIsRtl, fmi);\n          }\n\n          float w = measureRun(segstart, j, j, runIsRtl, fmi);\n          h += advance ? w : -w;\n\n          if (inSegment) {\n            return h += measureRun(segstart, offset, j, runIsRtl, null);\n          }\n\n          if (codept == '\\t') {\n            if (offset == j) {\n              return h;\n            }\n            h = mDir * nextTab(h * mDir);\n            if (target == j) {\n              return h;\n            }\n          }\n\n          segstart = j + 1;\n        }\n      }\n    }\n\n    return h;\n  }\n\n  /**\n   * Draws a unidirectional (but possibly multi-styled) run of text.\n   *\n   * @param c         the canvas to draw on\n   * @param start     the line-relative start\n   * @param limit     the line-relative limit\n   * @param runIsRtl  true if the run is right-to-left\n   * @param x         the position of the run that is closest to the leading margin\n   * @param top       the top of the line\n   * @param y         the baseline\n   * @param bottom    the bottom of the line\n   * @param needWidth true if the width value is required.\n   * @return the signed width of the run, based on the paragraph direction.\n   * Only valid if needWidth is true.\n   */\n  private float drawRun(Canvas c, int start,\n                        int limit, boolean runIsRtl, float x, int top, int y, int bottom,\n                        boolean needWidth) {\n\n    if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {\n      float w = -measureRun(start, limit, limit, runIsRtl, null);\n      handleRun(start, limit, limit, runIsRtl, c, x + w, top,\n          y, bottom, null, false);\n      return w;\n    }\n\n    return handleRun(start, limit, limit, runIsRtl, c, x, top,\n        y, bottom, null, needWidth);\n  }\n\n  /**\n   * Measures a unidirectional (but possibly multi-styled) run of text.\n   *\n   * @param start    the line-relative start of the run\n   * @param offset   the offset to measure to, between start and limit inclusive\n   * @param limit    the line-relative limit of the run\n   * @param runIsRtl true if the run is right-to-left\n   * @param fmi      receives metrics information about the requested\n   *                 run, can be null.\n   * @return the signed width from the start of the run to the leading edge\n   * of the character at offset, based on the run (not paragraph) direction\n   */\n  private float measureRun(int start, int offset, int limit, boolean runIsRtl,\n                           FontMetricsInt fmi) {\n    return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);\n  }\n\n  /**\n   * Walk the cursor through this line, skipping conjuncts and\n   * zero-width characters.\n   * <p>\n   * <p>This function cannot properly walk the cursor off the ends of the line\n   * since it does not know about any shaping on the previous/following line\n   * that might affect the cursor position. Callers must either avoid these\n   * situations or handle the result specially.\n   *\n   * @param cursor the starting position of the cursor, between 0 and the\n   *               length of the line, inclusive\n   * @param toLeft true if the caret is moving to the left.\n   * @return the new offset.  If it is less than 0 or greater than the length\n   * of the line, the previous/following line should be examined to get the\n   * actual offset.\n   */\n  int getOffsetToLeftRightOf(int cursor, boolean toLeft) {\n    // 1) The caret marks the leading edge of a character. The character\n    // logically before it might be on a different level, and the active caret\n    // position is on the character at the lower level. If that character\n    // was the previous character, the caret is on its trailing edge.\n    // 2) Take this character/edge and move it in the indicated direction.\n    // This gives you a new character and a new edge.\n    // 3) This position is between two visually adjacent characters.  One of\n    // these might be at a lower level.  The active position is on the\n    // character at the lower level.\n    // 4) If the active position is on the trailing edge of the character,\n    // the new caret position is the following logical character, else it\n    // is the character.\n\n    int lineStart = 0;\n    int lineEnd = mLen;\n    boolean paraIsRtl = mDir == -1;\n    int[] runs = mDirections.mDirections;\n\n    int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;\n    boolean trailing = false;\n\n    if (cursor == lineStart) {\n      runIndex = -2;\n    } else if (cursor == lineEnd) {\n      runIndex = runs.length;\n    } else {\n      // First, get information about the run containing the character with\n      // the active caret.\n      for (runIndex = 0; runIndex < runs.length; runIndex += 2) {\n        runStart = lineStart + runs[runIndex];\n        if (cursor >= runStart) {\n          runLimit = runStart + (runs[runIndex + 1] & Layout.RUN_LENGTH_MASK);\n          if (runLimit > lineEnd) {\n            runLimit = lineEnd;\n          }\n          if (cursor < runLimit) {\n            runLevel = (runs[runIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &\n                Layout.RUN_LEVEL_MASK;\n            if (cursor == runStart) {\n              // The caret is on a run boundary, see if we should\n              // use the position on the trailing edge of the previous\n              // logical character instead.\n              int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;\n              int pos = cursor - 1;\n              for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {\n                prevRunStart = lineStart + runs[prevRunIndex];\n                if (pos >= prevRunStart) {\n                  prevRunLimit = prevRunStart +\n                      (runs[prevRunIndex + 1] & Layout.RUN_LENGTH_MASK);\n                  if (prevRunLimit > lineEnd) {\n                    prevRunLimit = lineEnd;\n                  }\n                  if (pos < prevRunLimit) {\n                    prevRunLevel = (runs[prevRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT)\n                        & Layout.RUN_LEVEL_MASK;\n                    if (prevRunLevel < runLevel) {\n                      // Start from logically previous character.\n                      runIndex = prevRunIndex;\n                      runLevel = prevRunLevel;\n                      runStart = prevRunStart;\n                      runLimit = prevRunLimit;\n                      trailing = true;\n                      break;\n                    }\n                  }\n                }\n              }\n            }\n            break;\n          }\n        }\n      }\n\n      // caret might be == lineEnd.  This is generally a space or paragraph\n      // separator and has an associated run, but might be the end of\n      // text, in which case it doesn't.  If that happens, we ran off the\n      // end of the run list, and runIndex == runs.length.  In this case,\n      // we are at a run boundary so we skip the below test.\n      if (runIndex != runs.length) {\n        boolean runIsRtl = (runLevel & 0x1) != 0;\n        boolean advance = toLeft == runIsRtl;\n        if (cursor != (advance ? runLimit : runStart) || advance != trailing) {\n          // Moving within or into the run, so we can move logically.\n          newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,\n              runIsRtl, cursor, advance);\n          // If the new position is internal to the run, we're at the strong\n          // position already so we're finished.\n          if (newCaret != (advance ? runLimit : runStart)) {\n            return newCaret;\n          }\n        }\n      }\n    }\n\n    // If newCaret is -1, we're starting at a run boundary and crossing\n    // into another run. Otherwise we've arrived at a run boundary, and\n    // need to figure out which character to attach to.  Note we might\n    // need to run this twice, if we cross a run boundary and end up at\n    // another run boundary.\n    while (true) {\n      boolean advance = toLeft == paraIsRtl;\n      int otherRunIndex = runIndex + (advance ? 2 : -2);\n      if (otherRunIndex >= 0 && otherRunIndex < runs.length) {\n        int otherRunStart = lineStart + runs[otherRunIndex];\n        int otherRunLimit = otherRunStart +\n            (runs[otherRunIndex + 1] & Layout.RUN_LENGTH_MASK);\n        if (otherRunLimit > lineEnd) {\n          otherRunLimit = lineEnd;\n        }\n        int otherRunLevel = (runs[otherRunIndex + 1] >>> Layout.RUN_LEVEL_SHIFT) &\n            Layout.RUN_LEVEL_MASK;\n        boolean otherRunIsRtl = (otherRunLevel & 1) != 0;\n\n        advance = toLeft == otherRunIsRtl;\n        if (newCaret == -1) {\n          newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,\n              otherRunLimit, otherRunIsRtl,\n              advance ? otherRunStart : otherRunLimit, advance);\n          if (newCaret == (advance ? otherRunLimit : otherRunStart)) {\n            // Crossed and ended up at a new boundary,\n            // repeat a second and final time.\n            runIndex = otherRunIndex;\n            runLevel = otherRunLevel;\n            continue;\n          }\n          break;\n        }\n\n        // The new caret is at a boundary.\n        if (otherRunLevel < runLevel) {\n          // The strong character is in the other run.\n          newCaret = advance ? otherRunStart : otherRunLimit;\n        }\n        break;\n      }\n\n      if (newCaret == -1) {\n        // We're walking off the end of the line.  The paragraph\n        // level is always equal to or lower than any internal level, so\n        // the boundaries get the strong caret.\n        newCaret = advance ? mLen + 1 : -1;\n        break;\n      }\n\n      // Else we've arrived at the end of the line.  That's a strong position.\n      // We might have arrived here by crossing over a run with no internal\n      // breaks and dropping out of the above loop before advancing one final\n      // time, so reset the caret.\n      // Note, we use '<=' below to handle a situation where the only run\n      // on the line is a counter-directional run.  If we're not advancing,\n      // we can end up at the 'lineEnd' position but the caret we want is at\n      // the lineStart.\n      if (newCaret <= lineEnd) {\n        newCaret = advance ? lineEnd : lineStart;\n      }\n      break;\n    }\n\n    return newCaret;\n  }\n\n  /**\n   * Returns the next valid offset within this directional run, skipping\n   * conjuncts and zero-width characters.  This should not be called to walk\n   * off the end of the line, since the returned values might not be valid\n   * on neighboring lines.  If the returned offset is less than zero or\n   * greater than the line length, the offset should be recomputed on the\n   * preceding or following line, respectively.\n   *\n   * @param runIndex the run index\n   * @param runStart the start of the run\n   * @param runLimit the limit of the run\n   * @param runIsRtl true if the run is right-to-left\n   * @param offset   the offset\n   * @param after    true if the new offset should logically follow the provided\n   *                 offset\n   * @return the new offset\n   */\n  private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,\n                                   boolean runIsRtl, int offset, boolean after) {\n\n    if (runIndex < 0 || offset == (after ? mLen : 0)) {\n      // Walking off end of line.  Since we don't know\n      // what cursor positions are available on other lines, we can't\n      // return accurate values.  These are a guess.\n      if (after) {\n        return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;\n      }\n      return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;\n    }\n\n    TextPaint wp = mWorkPaint;\n    wp.set(mPaint);\n\n    int spanStart = runStart;\n    int spanLimit;\n    if (mSpanned == null) {\n      spanLimit = runLimit;\n    } else {\n      int target = after ? offset + 1 : offset;\n      int limit = mStart + runLimit;\n      while (true) {\n        spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,\n            MetricAffectingSpan.class) - mStart;\n        if (spanLimit >= target) {\n          break;\n        }\n        spanStart = spanLimit;\n      }\n\n      MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,\n          mStart + spanLimit, MetricAffectingSpan.class);\n      spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);\n\n      if (spans.length > 0) {\n        ReplacementSpan replacement = null;\n        for (int j = 0; j < spans.length; j++) {\n          MetricAffectingSpan span = spans[j];\n          if (span instanceof ReplacementSpan) {\n            replacement = (ReplacementSpan) span;\n          } else {\n            span.updateMeasureState(wp);\n          }\n        }\n\n        if (replacement != null) {\n          // If we have a replacement span, we're moving either to\n          // the start or end of this span.\n          return after ? spanLimit : spanStart;\n        }\n      }\n    }\n\n    int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;\n    int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;\n    if (mCharsValid) {\n      return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,\n          dir, offset, cursorOpt);\n    } else {\n      return wp.getTextRunCursor(mText, mStart + spanStart,\n          mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;\n    }\n  }\n\n  /**\n   * @param wp\n   */\n  private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {\n    final int previousTop = fmi.top;\n    final int previousAscent = fmi.ascent;\n    final int previousDescent = fmi.descent;\n    final int previousBottom = fmi.bottom;\n    final int previousLeading = fmi.leading;\n\n    wp.getFontMetricsInt(fmi);\n\n    updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,\n        previousLeading);\n  }\n\n  static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,\n                            int previousDescent, int previousBottom, int previousLeading) {\n    fmi.top = Math.min(fmi.top, previousTop);\n    fmi.ascent = Math.min(fmi.ascent, previousAscent);\n    fmi.descent = Math.max(fmi.descent, previousDescent);\n    fmi.bottom = Math.max(fmi.bottom, previousBottom);\n    fmi.leading = Math.max(fmi.leading, previousLeading);\n  }\n\n  /**\n   * Utility function for measuring and rendering text.  The text must\n   * not include a tab.\n   *\n   * @param wp        the working paint\n   * @param start     the start of the text\n   * @param end       the end of the text\n   * @param runIsRtl  true if the run is right-to-left\n   * @param c         the canvas, can be null if rendering is not needed\n   * @param x         the edge of the run closest to the leading margin\n   * @param top       the top of the line\n   * @param y         the baseline\n   * @param bottom    the bottom of the line\n   * @param fmi       receives metrics information, can be null\n   * @param needWidth true if the width of the run is needed\n   * @param offset    the offset for the purpose of measuring\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleText(TextPaint wp, int start, int end,\n                           int contextStart, int contextEnd, boolean runIsRtl,\n                           Canvas c, float x, int top, int y, int bottom,\n                           FontMetricsInt fmi, boolean needWidth, int offset) {\n\n    // Get metrics first (even for empty strings or \"0\" width runs)\n    if (fmi != null) {\n      expandMetricsFromPaint(fmi, wp);\n    }\n\n    int runLen = end - start;\n    // No need to do anything if the run width is \"0\"\n    if (runLen == 0) {\n      return 0f;\n    }\n\n    float ret = 0;\n\n    if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {\n      if (mCharsValid) {\n        ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd,\n            runIsRtl, offset);\n      } else {\n        int delta = mStart;\n        ret = wp.getRunAdvance(mText, delta + start, delta + end,\n            delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);\n      }\n    }\n\n    if (c != null) {\n      if (runIsRtl) {\n        x -= ret;\n      }\n\n      if (wp.bgColor != 0) {\n        int previousColor = wp.getColor();\n        Paint.Style previousStyle = wp.getStyle();\n\n        wp.setColor(wp.bgColor);\n        wp.setStyle(Paint.Style.FILL);\n        c.drawRect(x, top, x + ret, bottom, wp);\n\n        wp.setStyle(previousStyle);\n        wp.setColor(previousColor);\n      }\n\n      if (wp.underlineColor != 0) {\n        // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h\n        float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();\n\n        int previousColor = wp.getColor();\n        Paint.Style previousStyle = wp.getStyle();\n        boolean previousAntiAlias = wp.isAntiAlias();\n\n        wp.setStyle(Paint.Style.FILL);\n        wp.setAntiAlias(true);\n\n        wp.setColor(wp.underlineColor);\n        c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);\n\n        wp.setStyle(previousStyle);\n        wp.setColor(previousColor);\n        wp.setAntiAlias(previousAntiAlias);\n      }\n\n      drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,\n          x, y + wp.baselineShift);\n    }\n\n    return runIsRtl ? -ret : ret;\n  }\n\n  /**\n   * Utility function for measuring and rendering a replacement.\n   *\n   * @param replacement the replacement\n   * @param wp          the work paint\n   * @param start       the start of the run\n   * @param limit       the limit of the run\n   * @param runIsRtl    true if the run is right-to-left\n   * @param c           the canvas, can be null if not rendering\n   * @param x           the edge of the replacement closest to the leading margin\n   * @param top         the top of the line\n   * @param y           the baseline\n   * @param bottom      the bottom of the line\n   * @param fmi         receives metrics information, can be null\n   * @param needWidth   true if the width of the replacement is needed\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleReplacement(ReplacementSpan replacement, TextPaint wp,\n                                  int start, int limit, boolean runIsRtl, Canvas c,\n                                  float x, int top, int y, int bottom, FontMetricsInt fmi,\n                                  boolean needWidth) {\n\n    float ret = 0;\n\n    int textStart = mStart + start;\n    int textLimit = mStart + limit;\n\n    if (needWidth || (c != null && runIsRtl)) {\n      int previousTop = 0;\n      int previousAscent = 0;\n      int previousDescent = 0;\n      int previousBottom = 0;\n      int previousLeading = 0;\n\n      boolean needUpdateMetrics = (fmi != null);\n\n      if (needUpdateMetrics) {\n        previousTop = fmi.top;\n        previousAscent = fmi.ascent;\n        previousDescent = fmi.descent;\n        previousBottom = fmi.bottom;\n        previousLeading = fmi.leading;\n      }\n\n      ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);\n\n      if (needUpdateMetrics) {\n        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,\n            previousLeading);\n      }\n    }\n\n    if (c != null) {\n      if (runIsRtl) {\n        x -= ret;\n      }\n      replacement.draw(c, mText, textStart, textLimit,\n          x, top, y, bottom, wp);\n    }\n\n    return runIsRtl ? -ret : ret;\n  }\n\n  /**\n   * Utility function for handling a unidirectional run.  The run must not\n   * contain tabs but can contain styles.\n   *\n   * @param start        the line-relative start of the run\n   * @param measureLimit the offset to measure to, between start and limit inclusive\n   * @param limit        the limit of the run\n   * @param runIsRtl     true if the run is right-to-left\n   * @param c            the canvas, can be null\n   * @param x            the end of the run closest to the leading margin\n   * @param top          the top of the line\n   * @param y            the baseline\n   * @param bottom       the bottom of the line\n   * @param fmi          receives metrics information, can be null\n   * @param needWidth    true if the width is required\n   * @return the signed width of the run based on the run direction; only\n   * valid if needWidth is true\n   */\n  private float handleRun(int start, int measureLimit,\n                          int limit, boolean runIsRtl, Canvas c, float x, int top, int y,\n                          int bottom, FontMetricsInt fmi, boolean needWidth) {\n\n    // Case of an empty line, make sure we update fmi according to mPaint\n    if (start == measureLimit) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n      if (fmi != null) {\n        expandMetricsFromPaint(fmi, wp);\n      }\n      return 0f;\n    }\n\n    if (mSpanned == null) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n      final int mlimit = measureLimit;\n      return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,\n          y, bottom, fmi, needWidth || mlimit < measureLimit, mlimit);\n    }\n\n    mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);\n    mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);\n\n    // Shaping needs to take into account context up to metric boundaries,\n    // but rendering needs to take into account character style boundaries.\n    // So we iterate through metric runs to get metric bounds,\n    // then within each metric run iterate through character style runs\n    // for the run bounds.\n    final float originalX = x;\n    for (int i = start, inext; i < measureLimit; i = inext) {\n      TextPaint wp = mWorkPaint;\n      wp.set(mPaint);\n\n      inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -\n          mStart;\n      int mlimit = Math.min(inext, measureLimit);\n\n      ReplacementSpan replacement = null;\n\n      for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {\n        // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT\n        // empty by construction. This special case in getSpans() explains the >= & <= tests\n        if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||\n            (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;\n        MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];\n        if (span instanceof ReplacementSpan) {\n          replacement = (ReplacementSpan) span;\n        } else {\n          // We might have a replacement that uses the draw\n          // state, otherwise measure state would suffice.\n          span.updateDrawState(wp);\n        }\n      }\n\n      if (replacement != null) {\n        x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,\n            bottom, fmi, needWidth || mlimit < measureLimit);\n        continue;\n      }\n\n      for (int j = i, jnext; j < mlimit; j = jnext) {\n        jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -\n            mStart;\n        int offset = Math.min(jnext, mlimit);\n\n        wp.set(mPaint);\n        for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {\n          // Intentionally using >= and <= as explained above\n          if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||\n              (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;\n\n          CharacterStyle span = mCharacterStyleSpanSet.spans[k];\n          span.updateDrawState(wp);\n        }\n\n        // Only draw hyphen on last run in line\n        if (jnext < mLen) {\n          wp.setHyphenEdit(0);\n        }\n        x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,\n            top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);\n      }\n    }\n\n    return x - originalX;\n  }\n\n  /**\n   * Render a text run with the set-up paint.\n   *\n   * @param c            the canvas\n   * @param wp           the paint used to render the text\n   * @param start        the start of the run\n   * @param end          the end of the run\n   * @param contextStart the start of context for the run\n   * @param contextEnd   the end of the context for the run\n   * @param runIsRtl     true if the run is right-to-left\n   * @param x            the x position of the left edge of the run\n   * @param y            the baseline of the run\n   */\n  private void drawTextRun(Canvas c, TextPaint wp, int start, int end,\n                           int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {\n    if (mCharsValid) {\n      int count = end - start;\n      int contextCount = contextEnd - contextStart;\n      c.drawTextRun(mChars, start, count, contextStart, contextCount, x, y, runIsRtl, wp);\n    } else {\n      int delta = mStart;\n      c.drawTextRun(mText, delta + start, delta + end, delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);\n    }\n  }\n\n  /**\n   * Returns the next tab position.\n   *\n   * @param h the (unsigned) offset from the leading margin\n   * @return the (unsigned) tab position after this offset\n   */\n  float nextTab(float h) {\n    if (mTabs != null) {\n      return mTabs.nextTab(h);\n    }\n    return TabStops.nextDefaultStop(h, TAB_INCREMENT);\n  }\n\n  private static final int TAB_INCREMENT = 20;\n}\n"
  },
  {
    "path": "widget.FastTextView/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "widget.FastTextView/build.gradle",
    "content": "apply plugin: 'com.android.library'\n//apply plugin: 'com.github.dcendents.android-maven'\napply plugin: 'com.jfrog.bintray'\napply plugin: \"maven-publish\"\n\n\nProperties properties = new Properties()\nproperties.load(project.rootProject.file('local.properties').newDataInputStream())\n\next {\n    siteUrl = properties.getProperty(\"siteUrl\")\n    gitUrl = properties.getProperty(\"gitUrl\")\n    libVersion = '1.2.20'\n}\n\n\ngroup = 'com.lsjwzh'\nversion = libVersion\n\nandroid {\n    lintOptions {\n        abortOnError false\n    }\n\n    compileSdkVersion 28\n    buildToolsVersion \"28.0.3\"\n\n    defaultConfig {\n        minSdkVersion 15\n        targetSdkVersion 28\n        versionCode 4\n        versionName libVersion\n\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'com.android.support:support-v4:28.0.0'\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n}\n\ntask cleanAndroidMock(type: Delete) {\n    description = 'Deletes the mockable Android jar'\n\n    delete fileTree(\"${project.buildDir}/generated\") {\n        include 'mockable-android*.jar'\n    }\n}\n\nproject.afterEvaluate {\n    tasks['createMockableJar'].dependsOn cleanAndroidMock\n}\n//\nbintray {\n    user = properties.getProperty('BINTRAY_USER')\n    key = properties.getProperty('BINTRAY_KEY')\n//    configurations = ['archives']\n    publications = ['MyPublication']\n\n    pkg {\n        repo = 'maven'\n        name = properties.getProperty('maven.name')\n        licenses = ['Apache-2.0']\n        vcsUrl = gitUrl\n        publish = true\n        publicDownloadNumbers = true\n        version {\n            name = libVersion\n            desc = properties.getProperty('project.name')\n            released = new Date()\n            version libVersion\n        }\n    }\n}\n\n//\n//install {\n//    repositories.mavenInstaller {\n//        // This generates POM.xml with proper parameters\n//        pom {\n//            project {\n//                packaging 'aar'\n//                name properties.getProperty('project.name')\n//                url siteUrl\n//                // Set your license\n//                licenses {\n//                    license {\n//                        name 'The Apache Software License, Version 2.0'\n//                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'\n//                    }\n//                }\n//                developers {\n//                    developer { //填写的一些基本信息\n//                        id properties.getProperty(\"developer.name\")\n//                        name properties.getProperty(\"developer.name\")\n//                        email properties.getProperty(\"developer.email\")\n//                    }\n//                }\n//                scm {\n//                    connection gitUrl\n//                    developerConnection gitUrl\n//                    url siteUrl\n//                }\n//            }\n//        }\n//    }\n//}\n\n\n//android.libraryVariants.all { variant ->\n//    println variant.javaCompile.classpath.files\n//    if (variant.name == 'release') { //我们只需 release 的 javadoc\n//        task(\"generate${variant.name.capitalize()}Javadoc\", type: Javadoc) {\n//            // title = ''\n//            // description = ''\n//            source = variant.javaCompile.source\n//            doFirst {\n//                classpath = files(variant.javaCompile.classpath.files, project.android.getBootClasspath())\n//            }\n//            options {\n//                encoding \"utf-8\"\n//                links \"http://docs.oracle.com/javase/7/docs/api/\"\n//                linksOffline \"http://d.android.com/reference\", \"${android.sdkDirectory}/docs/reference\"\n//            }\n//            exclude '**/BuildConfig.java'\n//            exclude '**/R.java'\n//        }\n//        task(\"javadoc${variant.name.capitalize()}Jar\", type: Jar, dependsOn: \"generate${variant.name.capitalize()}Javadoc\") {\n//            classifier = 'javadoc'\n//            from tasks.getByName(\"generate${variant.name.capitalize()}Javadoc\").destinationDir\n//        }\n//        artifacts {\n//            archives tasks.getByName(\"javadoc${variant.name.capitalize()}Jar\")\n//        }\n//    }\n//}\n//\n//artifacts {\n//    archives javadocJar\n//    archives sourcesJar\n//    archives file(\"$buildDir/outputs/aar/$name-release.aar\")\n//}\n\n// Create the pom configuration:\ndef pomConfig = {\n    licenses {\n        license {\n            name \"The Apache Software License, Version 2.0\"\n            url \"http://www.apache.org/licenses/LICENSE-2.0.txt\"\n            distribution \"repo\"\n        }\n    }\n    developers {\n        developer { //填写的一些基本信息\n            id properties.getProperty(\"developer.name\")\n            name properties.getProperty(\"developer.name\")\n            email properties.getProperty(\"developer.email\")\n        }\n    }\n\n    scm {\n        url siteUrl\n    }\n}\n\nproject.afterEvaluate {\n    publishing {\n        publications {\n            MyPublication(MavenPublication) {\n                task androidJavadocs(type: Javadoc) {\n                    source = android.sourceSets.main.java.srcDirs\n                    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))\n                    android.libraryVariants.all { variant ->\n                        if (variant.name == 'release') {\n                            owner.classpath += variant.javaCompile.classpath\n                        }\n                    }\n                    failOnError false\n                    exclude '**/R.html', '**/R.*.html', '**/index.html'\n                }\n\n                task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {\n                    classifier = 'javadoc'\n                    from androidJavadocs.destinationDir\n                }\n\n                task androidSourcesJar(type: Jar) {\n                    classifier = 'sources'\n                    from android.sourceSets.main.java.srcDirs\n                }\n\n                artifact bundleReleaseAar\n                artifact androidSourcesJar\n                artifact androidJavadocsJar\n\n                groupId properties.getProperty('project.groudId')\n                artifactId properties.getProperty('project.name')\n                version libVersion\n                pom.withXml {\n                    def root = asNode()\n                    root.appendNode('description', 'Your description of the lib')\n                    root.appendNode('name', 'Your name of the lib')\n                    root.appendNode('url', 'https://site_for_lib.tld')\n                    root.children().last() + pomConfig\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "widget.FastTextView/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/wenye/android_sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "widget.FastTextView/src/main/AndroidManifest.xml",
    "content": "<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",
    "content": "package android.text;\n\nimport android.text.style.ReplacementSpan;\n\nimport java.lang.reflect.Array;\nimport java.util.Arrays;\n\n/**\n * Fix Spanned Ellipsis bug.\n */\npublic class EllipsisSpannedContainer implements Spanned, GetChars{\n  public static final char[] ELLIPSIS_NORMAL = { '\\u2026' }; // this is \"...\"\n  private final Spanned mSourceSpanned;\n  private Layout mLayout;\n  private int mEllipsisStart = -1;\n  private int mEllipsisEnd = -1;\n  private ReplacementSpan mCustomEllipsisSpan;\n\n  public EllipsisSpannedContainer(Spanned spanned) {\n    mSourceSpanned = spanned;\n  }\n\n  public void setCustomEllipsisSpan(ReplacementSpan customEllipsisSpan) {\n    mCustomEllipsisSpan = customEllipsisSpan;\n  }\n\n  public int getEllipsisStart() {\n    return mEllipsisStart;\n  }\n\n  public int getEllipsisEnd() {\n    return mEllipsisEnd;\n  }\n\n  public Spanned getSourceSpanned() {\n    return mSourceSpanned;\n  }\n\n  @Override\n  public int getSpanEnd(Object tag) {\n    if (mCustomEllipsisSpan != null && mCustomEllipsisSpan == tag) {\n      return mEllipsisEnd;\n    }\n    return mSourceSpanned.getSpanEnd(tag);\n  }\n\n  @Override\n  public int getSpanFlags(Object tag) {\n    if (mCustomEllipsisSpan != null && mCustomEllipsisSpan == tag) {\n      return Spanned.SPAN_INCLUSIVE_EXCLUSIVE;\n    }\n    return mSourceSpanned.getSpanFlags(tag);\n  }\n\n  @Override\n  public int getSpanStart(Object tag) {\n    if (mCustomEllipsisSpan != null && mCustomEllipsisSpan == tag) {\n      return mEllipsisStart;\n    }\n    return mSourceSpanned.getSpanStart(tag);\n  }\n\n  @Override\n  public <T> T[] getSpans(int start, int end, Class<T> type) {\n    if (mEllipsisEnd >= end && mEllipsisStart <= end) {\n      T[] spans1 = mSourceSpanned.getSpans(start, Math.max(mEllipsisStart, start), type);\n      T[] spans2 = mSourceSpanned.getSpans(Math.min(end, mEllipsisEnd), end, type);\n      int offset = mCustomEllipsisSpan != null\n          && (type.isAssignableFrom(ReplacementSpan.class) || type == mCustomEllipsisSpan.getClass()) ?\n          1 : 0;\n      int minLen = spans1.length + spans2.length + offset;\n      T[] spans = (T[]) Array.newInstance(type, minLen);\n      if (spans.length > minLen) {\n        spans = Arrays.copyOf(spans, minLen);\n      }\n      System.arraycopy(spans1, 0, spans, 0, spans1.length);\n      if (offset > 0) {\n        spans[spans1.length] =  (T) mCustomEllipsisSpan;\n      }\n      System.arraycopy(spans2, 0, spans, spans1.length + offset, spans2.length);\n      return spans;\n    }\n    return mSourceSpanned.getSpans(start, end, type);\n  }\n\n  @Override\n  public int nextSpanTransition(int start, int limit, Class type) {\n    return mSourceSpanned.nextSpanTransition(start, limit, type);\n  }\n\n  @Override\n  public int length() {\n    return mSourceSpanned.length();\n  }\n\n  @Override\n  public char charAt(int index) {\n    return mSourceSpanned.charAt(index);\n  }\n\n  @Override\n  public CharSequence subSequence(int start, int end) {\n    return mSourceSpanned.subSequence(start, end);\n  }\n\n  @Override\n  public void getChars(int start, int end, char[] dest, int destoff) {\n    TextUtils.getChars(mSourceSpanned, start, end, dest, destoff);\n    if (mLayout != null) {\n      int line1 = mLayout.getLineForOffset(start);\n      int line2 = mLayout.getLineForOffset(end);\n\n      for (int i = line1; i <= line2; i++) {\n        ellipsize(start, end, i, dest, destoff);\n      }\n    }\n  }\n\n  void ellipsize(int start, int end, int line, char[] dest, int destoff) {\n    int ellipsisCount = mLayout.getEllipsisCount(line);\n\n    if (ellipsisCount == 0) {\n      return;\n    }\n\n    int ellipsisStart = mLayout.getEllipsisStart(line);\n    int linestart = mLayout.getLineStart(line);\n\n    for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {\n      char c;\n\n      if (i == ellipsisStart) {\n        c = ELLIPSIS_NORMAL[0]; // ellipsis\n        mEllipsisStart = i + linestart;\n        mEllipsisEnd = mEllipsisStart + ellipsisCount;\n      } else {\n        c = '\\uFEFF'; // 0-width space\n      }\n\n      int a = i + linestart;\n\n      if (a >= start && a < end) {\n        dest[destoff + a - start] = c;\n      }\n    }\n  }\n\n  public void setLayout(StaticLayout layout) {\n    mLayout = layout;\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/LayoutUtils.java",
    "content": "package android.text;\n\nimport android.util.Log;\n\npublic class LayoutUtils {\n\n  static Class sEllipsizerClazz;\n\n  static {\n    Log.e(\"LayoutUtils\", \" static\");\n    try {\n      sEllipsizerClazz = Class.forName(\"android.text.Layout$Ellipsizer\");\n    } catch (ClassNotFoundException e) {\n      e.printStackTrace();\n    }\n  }\n\n  public static boolean isEllipsizer(CharSequence charSequence) {\n    return sEllipsizerClazz != null && sEllipsizerClazz.isInstance(charSequence);\n\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/SpanSetCompat.java",
    "content": "/*\n * Copyright (C) 2012 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage android.text;\n\nimport java.lang.reflect.Array;\nimport java.util.Arrays;\n\n/**\n * A cached set of spans. Caches the result of {@link Spanned#getSpans(int, int, Class)} and then\n * provides faster access to {@link Spanned#nextSpanTransition(int, int, Class)}.\n *\n * Fields are left public for a convenient direct access.\n *\n * Note that empty spans are ignored by this class.\n */\npublic class SpanSetCompat<E> {\n    private final Class<? extends E> classType;\n\n    int numberOfSpans;\n    E[] spans;\n    int[] spanStarts;\n    int[] spanEnds;\n    int[] spanFlags;\n\n    SpanSetCompat(Class<? extends E> type) {\n        classType = type;\n        numberOfSpans = 0;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void init(Spanned spanned, int start, int limit) {\n        final E[] allSpans = spanned.getSpans(start, limit, classType);\n        final int length = allSpans.length;\n\n        if (length > 0 && (spans == null || spans.length < length)) {\n            // These arrays may end up being too large because of the discarded empty spans\n            spans = (E[]) Array.newInstance(classType, length);\n            spanStarts = new int[length];\n            spanEnds = new int[length];\n            spanFlags = new int[length];\n        }\n\n        int prevNumberOfSpans = numberOfSpans;\n        numberOfSpans = 0;\n        for (int i = 0; i < length; i++) {\n            final E span = allSpans[i];\n\n            final int spanStart = spanned.getSpanStart(span);\n            final int spanEnd = spanned.getSpanEnd(span);\n            if (spanStart == spanEnd) continue;\n\n            final int spanFlag = spanned.getSpanFlags(span);\n\n            spans[numberOfSpans] = span;\n            spanStarts[numberOfSpans] = spanStart;\n            spanEnds[numberOfSpans] = spanEnd;\n            spanFlags[numberOfSpans] = spanFlag;\n\n            numberOfSpans++;\n        }\n\n        // cleanup extra spans left over from previous init() call\n        if (numberOfSpans < prevNumberOfSpans) {\n            // prevNumberofSpans was > 0, therefore spans != null\n            Arrays.fill(spans, numberOfSpans, prevNumberOfSpans, null);\n        }\n    }\n\n    /**\n     * Returns true if there are spans intersecting the given interval.\n     * @param end must be strictly greater than start\n     */\n    public boolean hasSpansIntersecting(int start, int end) {\n        for (int i = 0; i < numberOfSpans; i++) {\n            // equal test is valid since both intervals are not empty by construction\n            if (spanStarts[i] >= end || spanEnds[i] <= start) continue;\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Similar to {@link Spanned#nextSpanTransition(int, int, Class)}\n     */\n    int getNextTransition(int start, int limit) {\n        for (int i = 0; i < numberOfSpans; i++) {\n            final int spanStart = spanStarts[i];\n            final int spanEnd = spanEnds[i];\n            if (spanStart > start && spanStart < limit) limit = spanStart;\n            if (spanEnd > start && spanEnd < limit) limit = spanEnd;\n        }\n        return limit;\n    }\n\n    /**\n     * Removes all internal references to the spans to avoid memory leaks.\n     */\n    public void recycle() {\n        if (spans != null) {\n            Arrays.fill(spans, 0, numberOfSpans, null);\n        }\n    }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/StaticLayoutBuilderCompat.java",
    "content": "package android.text;\n\nimport android.annotation.Nullable;\nimport android.os.Build;\nimport android.support.v4.util.Pools;\n\n/**\n * Builder for static layouts. The builder is a newer pattern for constructing\n * StaticLayout objects and should be preferred over the constructors,\n * particularly to access newer features. To build a static layout, first\n * call {@link #obtain} with the required arguments (text, paint, and width),\n * then call setters for optional parameters, and finally {@link #build}\n * to build the StaticLayout object. Parameters not explicitly set will get\n * default values.\n */\npublic class StaticLayoutBuilderCompat {\n  private StaticLayoutBuilderCompat() {\n  }\n\n  /**\n   * Obtain a builder for constructing StaticLayout objects\n   *\n   * @param source The text to be laid out, optionally with spans\n   * @param start  The index of the start of the text\n   * @param end    The index + 1 of the end of the text\n   * @param paint  The base paint used for layout\n   * @param width  The width in pixels\n   * @return a builder object used for constructing the StaticLayout\n   */\n  public static StaticLayoutBuilderCompat obtain(CharSequence source, int start, int end, TextPaint paint,\n                                                 int width) {\n    StaticLayoutBuilderCompat b = sPool.acquire();\n    if (b == null) {\n      b = new StaticLayoutBuilderCompat();\n    }\n\n    // set default initial values\n    b.mText = source;\n    b.mStart = start;\n    b.mEnd = end;\n    b.mPaint = paint;\n    b.mWidth = width;\n    b.mAlignment = Layout.Alignment.ALIGN_NORMAL;\n    // it has exist in 4.0.1\n    // see http://www.grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/text/TextDirectionHeuristics.java#TextDirectionHeuristics.FirstStrong.0INSTANCE\n    b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;\n\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n      b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;\n      b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;\n    }\n    b.mSpacingMult = 1.0f;\n    b.mSpacingAdd = 0.0f;\n    b.mIncludePad = true;\n    b.mEllipsizedWidth = width;\n    b.mEllipsize = null;\n    b.mMaxLines = Integer.MAX_VALUE;\n\n    return b;\n  }\n\n  // release any expensive state\n        /* package */ void finish() {\n    mText = null;\n    mPaint = null;\n    mRightIndents = null;\n  }\n\n  public StaticLayoutBuilderCompat setText(CharSequence source) {\n    return setText(source, 0, source.length());\n  }\n\n  /**\n   * Set the text. Only useful when re-using the builder, which is done for\n   * the internal implementation of {@link DynamicLayout} but not as part\n   * of normal {@link StaticLayout} usage.\n   *\n   * @param source The text to be laid out, optionally with spans\n   * @param start  The index of the start of the text\n   * @param end    The index + 1 of the end of the text\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setText(CharSequence source, int start, int end) {\n    mText = source;\n    mStart = start;\n    mEnd = end;\n    return this;\n  }\n\n  /**\n   * Set the paint. Internal for reuse cases only.\n   *\n   * @param paint The base paint used for layout\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setPaint(TextPaint paint) {\n    mPaint = paint;\n    return this;\n  }\n\n  /**\n   * Set the width. Internal for reuse cases only.\n   *\n   * @param width The width in pixels\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setWidth(int width) {\n    mWidth = width;\n    if (mEllipsize == null) {\n      mEllipsizedWidth = width;\n    }\n    return this;\n  }\n\n  /**\n   * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.\n   *\n   * @param alignment Alignment for the resulting {@link StaticLayout}\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setAlignment(Layout.Alignment alignment) {\n    mAlignment = alignment;\n    return this;\n  }\n\n  /**\n   * Set the text direction heuristic. The text direction heuristic is used to\n   * resolve text direction based per-paragraph based on the input text. The default is\n   * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.\n   *\n   * @param textDir text direction heuristic for resolving BiDi behavior.\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setTextDirection(TextDirectionHeuristic textDir) {\n    mTextDir = textDir;\n    return this;\n  }\n\n  /**\n   * Set line spacing parameters. The default is 0.0 for {@code spacingAdd}\n   * and 1.0 for {@code spacingMult}.\n   *\n   * @param spacingAdd  line spacing add\n   * @param spacingMult line spacing multiplier\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setLineSpacing\n   */\n  public StaticLayoutBuilderCompat setLineSpacing(float spacingAdd, float spacingMult) {\n    mSpacingAdd = spacingAdd;\n    mSpacingMult = spacingMult;\n    return this;\n  }\n\n  /**\n   * Set whether to include extra space beyond font ascent and descent (which is\n   * needed to avoid clipping in some languages, such as Arabic and Kannada). The\n   * default is {@code true}.\n   *\n   * @param includePad whether to include padding\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setIncludeFontPadding\n   */\n  public StaticLayoutBuilderCompat setIncludePad(boolean includePad) {\n    mIncludePad = includePad;\n    return this;\n  }\n\n  /**\n   * Set the width as used for ellipsizing purposes, if it differs from the\n   * normal layout width. The default is the {@code width}\n   * passed to {@link #obtain}.\n   *\n   * @param ellipsizedWidth width used for ellipsizing, in pixels\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setEllipsize\n   */\n  public StaticLayoutBuilderCompat setEllipsizedWidth(int ellipsizedWidth) {\n    mEllipsizedWidth = ellipsizedWidth;\n    return this;\n  }\n\n  /**\n   * Set ellipsizing on the layout. Causes words that are longer than the view\n   * is wide, or exceeding the number of lines (see #setMaxLines) in the case\n   * of {@link android.text.TextUtils.TruncateAt#END} or\n   * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead\n   * of broken. The default is\n   * {@code null}, indicating no ellipsis is to be applied.\n   *\n   * @param ellipsize type of ellipsis behavior\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setEllipsize\n   */\n  public StaticLayoutBuilderCompat setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {\n    mEllipsize = ellipsize;\n    return this;\n  }\n\n  /**\n   * Set maximum number of lines. This is particularly useful in the case of\n   * ellipsizing, where it changes the layout of the last line. The default is\n   * unlimited.\n   *\n   * @param maxLines maximum number of lines in the layout\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setMaxLines\n   */\n  public StaticLayoutBuilderCompat setMaxLines(int maxLines) {\n    mMaxLines = maxLines;\n    return this;\n  }\n\n  /**\n   * Set break strategy, useful for selecting high quality or balanced paragraph\n   * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.\n   *\n   * @param breakStrategy break strategy for paragraph layout\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setBreakStrategy\n   */\n  public StaticLayoutBuilderCompat setBreakStrategy(int breakStrategy) {\n    mBreakStrategy = breakStrategy;\n    return this;\n  }\n\n  /**\n   * Set hyphenation frequency, to control the amount of automatic hyphenation used. The\n   * default is {@link Layout#HYPHENATION_FREQUENCY_NONE}.\n   *\n   * @param hyphenationFrequency hyphenation frequency for the paragraph\n   * @return this builder, useful for chaining\n   * @see android.widget.TextView#setHyphenationFrequency\n   */\n  public StaticLayoutBuilderCompat setHyphenationFrequency(int hyphenationFrequency) {\n    mHyphenationFrequency = hyphenationFrequency;\n    return this;\n  }\n\n  /**\n   * Set indents. Arguments are arrays holding an indent amount, one per line, measured in\n   * pixels. For lines past the last element in the array, the last element repeats.\n   *\n   * @param leftIndents  array of indent values for left margin, in pixels\n   * @param rightIndents array of indent values for right margin, in pixels\n   * @return this builder, useful for chaining\n   */\n  public StaticLayoutBuilderCompat setIndents(int[] leftIndents, int[] rightIndents) {\n    mLeftIndents = leftIndents;\n    mRightIndents = rightIndents;\n    return this;\n  }\n\n  /**\n   * Build the {@link StaticLayout} after options have been set.\n   * <p>\n   * <p>Note: the builder object must not be reused in any way after calling this\n   * method. Setting parameters after calling this method, or calling it a second\n   * time on the same builder object, will likely lead to unexpected results.\n   *\n   * @return the newly constructed {@link StaticLayout} object\n   */\n  public StaticLayout build() {\n    StaticLayout result;\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n      StaticLayout.Builder builder = StaticLayout.Builder.obtain(mText, mStart, mEnd, mPaint, mWidth);\n      builder.setAlignment(mAlignment)\n          .setBreakStrategy(mBreakStrategy)\n          .setIndents(mLeftIndents, mRightIndents)\n          .setHyphenationFrequency(mHyphenationFrequency)\n          .setTextDirection(mTextDir)\n          .setLineSpacing(mSpacingAdd, mSpacingMult)\n          .setIncludePad(mIncludePad)\n          .setEllipsizedWidth(mEllipsizedWidth)\n          .setEllipsize(mEllipsize)\n          .setMaxLines(mMaxLines);\n      result = builder.build();\n    } else {\n      result = new StaticLayout(mText, mStart, mEnd, mPaint, mWidth, mAlignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mEllipsize, mEllipsizedWidth, mMaxLines);\n    }\n    sPool.release(this);\n    return result;\n  }\n\n  CharSequence mText;\n  int mStart;\n  int mEnd;\n  TextPaint mPaint;\n  int mWidth;\n  Layout.Alignment mAlignment;\n  TextDirectionHeuristic mTextDir;\n  float mSpacingMult;\n  float mSpacingAdd;\n  boolean mIncludePad;\n  int mEllipsizedWidth;\n  TextUtils.TruncateAt mEllipsize;\n  int mMaxLines = Integer.MAX_VALUE;\n  int mBreakStrategy;\n  int mHyphenationFrequency;\n  int[] mLeftIndents;\n  int[] mRightIndents;\n\n  private static final Pools.SynchronizedPool<StaticLayoutBuilderCompat> sPool = new Pools.SynchronizedPool<>(3);\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/TextLayoutCache.java",
    "content": "package android.text;\n\nimport java.util.Map;\nimport java.util.WeakHashMap;\n\npublic class TextLayoutCache<T extends Layout> {\n  public static TextLayoutCache<StaticLayout> STATIC_LAYOUT_CACHE = new TextLayoutCache<>();\n\n  private Map<CharSequence, T> mLayoutPool = new WeakHashMap<>();\n\n  public synchronized void put(CharSequence key, T value) {\n    mLayoutPool.put(key, value);\n  }\n\n  public synchronized T get(CharSequence key) {\n    return mLayoutPool.get(key);\n  }\n\n  public synchronized T remove(CharSequence key) {\n    return mLayoutPool.remove(key);\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/android/text/TextLayoutWarmer.java",
    "content": "package android.text;\n\nimport android.graphics.Canvas;\n\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.WeakHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\n/**\n * Warm up Text Layout.\n */\npublic class TextLayoutWarmer<T extends Layout> {\n  private Canvas mCanvas = new Canvas();\n  private LayoutFactory<T> mLayoutFactory;\n  private Map<CharSequence, WarmerTask<T>> mLayoutPool = Collections.synchronizedMap(new\n      WeakHashMap<CharSequence, WarmerTask<T>>());\n  private LayoutWarmerExecutor mWarmerExecutor = new LayoutWarmerExecutor() {\n    ExecutorService executor = Executors.newSingleThreadExecutor();\n\n    @Override\n    public void queue(WarmerTask warmerTask) {\n      executor.submit(warmerTask);\n    }\n  };\n  private final Set<WarmListener<T>> mWarmListeners = Collections.synchronizedSet(new\n      LinkedHashSet<WarmListener<T>>());\n\n  public void setLayoutFactory(LayoutFactory<T> layoutFactory) {\n    mLayoutFactory = layoutFactory;\n  }\n\n  public LayoutFactory<T> getLayoutFactory() {\n    return mLayoutFactory;\n  }\n\n  public void setWarmerExecutor(LayoutWarmerExecutor warmerExecutor) {\n    mWarmerExecutor = warmerExecutor;\n  }\n\n  public LayoutWarmerExecutor getWarmerExecutor() {\n    return mWarmerExecutor;\n  }\n\n  public boolean contains(CharSequence text) {\n    return mLayoutPool.containsKey(text);\n  }\n\n  public T getLayout(CharSequence text) {\n    WarmerTask<T> tWarmerTask = mLayoutPool.get(text);\n    return tWarmerTask != null ? tWarmerTask.mLayout : null;\n  }\n\n  public void addListener(WarmListener<T> listener) {\n    mWarmListeners.add(listener);\n  }\n\n  public void removeListener(WarmListener<T> listener) {\n    mWarmListeners.remove(listener);\n  }\n\n  public Set<WarmListener<T>> getAllListeners() {\n    return mWarmListeners;\n  }\n\n  public void addText(CharSequence text) {\n    if (mLayoutFactory == null) {\n      throw new IllegalStateException(\"LayoutFactory can not be null\");\n    }\n    if (mWarmerExecutor == null) {\n      throw new IllegalStateException(\"WarmerExecutor can not be null\");\n    }\n    if (contains(text)) {\n      return;\n    }\n    WarmerTask<T> warmerTask = new WarmerTask<>(text, this);\n    mLayoutPool.put(text, warmerTask);\n    mWarmerExecutor.queue(warmerTask);\n  }\n\n  public void removeCache(CharSequence text) {\n    WarmerTask<T> tWarmerTask = mLayoutPool.get(text);\n    if (tWarmerTask != null) {\n      tWarmerTask.mIsCancelled = true;\n    }\n    mLayoutPool.remove(text);\n  }\n\n  public interface LayoutWarmerExecutor {\n    void queue(WarmerTask warmerTask);\n  }\n\n  public static class WarmerTask<T extends Layout> implements Runnable {\n    public CharSequence mText;\n    public TextLayoutWarmer<T> mTextLayoutWarmer;\n    public T mLayout;\n    public boolean mIsCancelled;\n\n    WarmerTask(CharSequence text, TextLayoutWarmer<T> warmer) {\n      mText = text;\n      mTextLayoutWarmer = warmer;\n    }\n\n    @Override\n    public void run() {\n      if (mIsCancelled) {\n        return;\n      }\n      mLayout = mTextLayoutWarmer.getLayoutFactory().makeLayout(mText);\n      if (mLayout != null) {\n        mLayout.draw(mTextLayoutWarmer.mCanvas);\n        Set<WarmListener<T>> allListeners = mTextLayoutWarmer.getAllListeners();\n        for (WarmListener<T> listener : allListeners) {\n          listener.onWarmComplete(mText, this);\n        }\n      }\n    }\n  }\n\n  public interface WarmListener<T extends Layout> {\n    void onWarmComplete(CharSequence text, WarmerTask<T> warmerTask);\n  }\n\n  public interface LayoutFactory<T extends Layout> {\n    T makeLayout(CharSequence text);\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanLayoutView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.Layout;\nimport android.text.Spannable;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\n/**\n * FastTextLayoutView with ClickSpan support.\n */\npublic class ClickableSpanLayoutView extends FastTextLayoutView {\n\n  public ClickableSpanLayoutView(Context context) {\n    super(context);\n  }\n\n  public ClickableSpanLayoutView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public ClickableSpanLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public ClickableSpanLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  @Override\n  public boolean onTouchEvent(MotionEvent event) {\n    Layout textLayout = getTextLayout();\n    if (textLayout != null) {\n      CharSequence textSource = textLayout.getText();\n      if (textSource instanceof Spannable) {\n        if (ClickableSpanUtil.handleClickableSpan(this, textLayout, (Spannable) textSource, event)) {\n          return true;\n        }\n      }\n    }\n    return super.onTouchEvent(event);\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ClickableSpanUtil.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.text.Layout;\nimport android.text.Selection;\nimport android.text.Spannable;\nimport android.text.Spanned;\nimport android.text.style.ClickableSpan;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.View;\n\npublic class ClickableSpanUtil {\n  public static boolean handleClickableSpan(View view, Layout layout, Spanned buffer, Class<?\n      extends Clickable> spanType, MotionEvent event) {\n    int action = event.getAction();\n\n    if (action == MotionEvent.ACTION_UP ||\n        action == MotionEvent.ACTION_DOWN) {\n      int x = (int) event.getX();\n      int y = (int) event.getY();\n\n      x -= view.getPaddingLeft();\n      y -= view.getPaddingTop();\n\n      x += view.getScrollX();\n      y += view.getScrollY();\n\n      int line = layout.getLineForVertical(y);\n      int off = getOffsetForHorizontal(view, layout, x, line);\n\n      Clickable[] link = buffer.getSpans(off, off, spanType);\n\n      if (link.length != 0) {\n        if (action == MotionEvent.ACTION_UP) {\n          link[0].onClick(view);\n        } else if (buffer instanceof Spannable) {\n          Selection.setSelection((Spannable) buffer,\n              buffer.getSpanStart(link[0]),\n              buffer.getSpanEnd(link[0]));\n        }\n        return true;\n      } else if (buffer instanceof Spannable) {\n        Selection.removeSelection((Spannable) buffer);\n      }\n    }\n\n    return false;\n  }\n\n  public static boolean handleClickableSpan(View view, Layout layout, Spannable buffer,\n                                            MotionEvent event) {\n    int action = event.getAction();\n\n    if (action == MotionEvent.ACTION_UP ||\n        action == MotionEvent.ACTION_DOWN) {\n      int x = (int) event.getX();\n      int y = (int) event.getY();\n\n      x -= view.getPaddingLeft();\n      y -= view.getPaddingTop();\n\n      x += view.getScrollX();\n      y += view.getScrollY();\n\n      int line = layout.getLineForVertical(y);\n      line = Math.min(layout.getLineCount() - 1, line); // 避免line超出line count\n      int off = getOffsetForHorizontal(view, layout, x, line);\n\n      ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);\n\n      if (link.length != 0) {\n        if (action == MotionEvent.ACTION_UP) {\n          link[0].onClick(view);\n        } else {\n          Selection.setSelection(buffer,\n              buffer.getSpanStart(link[0]),\n              buffer.getSpanEnd(link[0]));\n        }\n        return true;\n      } else {\n        Selection.removeSelection(buffer);\n      }\n    }\n\n    return false;\n  }\n\n  private static int getOffsetForHorizontal(View view, Layout layout, int x, int line) {\n    if (view.getWidth() > layout.getWidth()) {\n      if (view instanceof FastTextView) {\n        int gravity = ((FastTextView) view).getGravity();\n        int translateX;\n        int horizontalGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;\n        switch (horizontalGravity) {\n          default:\n          case Gravity.LEFT:\n            translateX = view.getPaddingLeft();\n            break;\n          case Gravity.CENTER_HORIZONTAL:\n            translateX = view.getPaddingLeft() + (((FastTextView) view).getInnerWidth() - layout\n                .getWidth()) / 2;\n            break;\n          case Gravity.RIGHT:\n            translateX = view.getPaddingLeft() + ((FastTextView) view).getInnerWidth() - layout\n                .getWidth();\n            break;\n        }\n        x -= translateX;\n      }\n    }\n    try {\n      return layout.getOffsetForHorizontal(line, x);\n    } catch (Throwable e) {\n      e.printStackTrace();\n    }\n    return layout.getLineEnd(line);\n  }\n\n  public interface Clickable {\n    /**\n     * Performs the click action associated with this span.\n     */\n    void onClick(View widget);\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextLayoutView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.Layout;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\n\n/**\n * A custom view for rendering layout directly.\n */\npublic class FastTextLayoutView extends View {\n  private static final String TAG = FastTextLayoutView.class.getSimpleName();\n  protected Layout mLayout;\n\n  public FastTextLayoutView(Context context) {\n    super(context);\n  }\n\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public FastTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    long start = System.currentTimeMillis();\n    canvas.save();\n    if (mLayout != null) {\n      canvas.translate(getPaddingLeft(), getPaddingTop());\n      mLayout.draw(canvas);\n    }\n    canvas.restore();\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onDraw cost:\" + (end - start));\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    long start = System.currentTimeMillis();\n    if (mLayout != null) {\n      setMeasuredDimension(getMeasuredWidth(getPaddingLeft() + getPaddingRight() + mLayout.getWidth(), widthMeasureSpec),\n          getMeasuredHeight(getPaddingTop() + getPaddingBottom() + mLayout.getHeight(), heightMeasureSpec));\n    } else {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onMeasure cost:\" + (end - start));\n    }\n  }\n\n  protected int getMeasuredWidth(int size, int measureSpec) {\n    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {\n      return size;\n    }\n    return getDefaultSize(size, measureSpec);\n  }\n\n  protected int getMeasuredHeight(int size, int measureSpec) {\n    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {\n      return size;\n    }\n    return getDefaultSize(size, measureSpec);\n  }\n\n  public void setTextLayout(Layout layout) {\n    mLayout = layout;\n  }\n\n  public Layout getTextLayout() {\n    return mLayout;\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/FastTextView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.annotation.ColorInt;\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.EllipsisSpannedContainer;\nimport android.text.Layout;\nimport android.text.LayoutUtils;\nimport android.text.Spannable;\nimport android.text.SpannableString;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.StaticLayout;\nimport android.text.StaticLayoutBuilderCompat;\nimport android.text.TextLayoutCache;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.text.style.ReplacementSpan;\nimport android.text.util.Linkify;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport java.util.regex.Pattern;\n\n/**\n * Simple and Fast TextView.\n */\npublic class FastTextView extends FastTextLayoutView {\n  private static final String TAG = FastTextView.class.getSimpleName();\n  TextViewAttrsHelper mAttrsHelper = new TextViewAttrsHelper();\n  private CharSequence mText;\n  private TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);\n  private ReplacementSpan mCustomEllipsisSpan;\n  private boolean mEnableLayoutCache = false; // experiment\n  private EllipsisSpannedContainer mEllipsisSpanned;\n  private int mCurTextColor;\n  protected boolean mCompressText;\n  protected int mLinkifyMask;\n  private int mLinkColor = Color.parseColor(\"#109DD0\");\n\n  public FastTextView(Context context) {\n    this(context, null);\n  }\n\n  public FastTextView(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, -1);\n  }\n\n  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    init(context, attrs, defStyleAttr, -1);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public FastTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int\n      defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n    init(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int\n      defStyleRes) {\n    mAttrsHelper.init(context, attrs, defStyleAttr, defStyleRes);\n    setText(mAttrsHelper.mText);\n    TextPaint textPaint = getPaint();\n    mCurTextColor = mAttrsHelper.mTextColor.getDefaultColor();\n    textPaint.setColor(mCurTextColor);\n    textPaint.setTextSize(mAttrsHelper.mTextSize);\n    final Resources.Theme theme = context.getTheme();\n    TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.FastTextView, defStyleAttr,\n        defStyleRes);\n    mEnableLayoutCache = a.getBoolean(R.styleable.FastTextView_enableLayoutCache, false);\n    a.recycle();\n  }\n\n  private void updateTextColors() {\n    boolean inval = false;\n    final int[] drawableState = getDrawableState();\n    int color = mAttrsHelper.mTextColor.getColorForState(drawableState, mCurTextColor);\n    if (color != mCurTextColor) {\n      mCurTextColor = color;\n      getPaint().setColor(mCurTextColor);\n      inval = true;\n    }\n    if (inval) {\n      // Text needs to be redrawn with the new color\n      invalidate();\n    }\n  }\n\n  @Override\n  protected void drawableStateChanged() {\n    super.drawableStateChanged();\n    if (mAttrsHelper.mTextColor != null && mAttrsHelper.mTextColor.isStateful()) {\n      updateTextColors();\n    }\n  }\n\n  public void setLineSpacing(int spacingAdd, float spacingMultiplier) {\n    mAttrsHelper.mSpacingAdd = spacingAdd;\n    mAttrsHelper.mSpacingMultiplier = spacingMultiplier;\n    clearTextLayout();\n  }\n\n  public void setTextColor(ColorStateList colorStateList) {\n    mAttrsHelper.mTextColor = colorStateList;\n    updateTextColors();\n  }\n\n  public void setTextColor(@ColorInt int color) {\n    mAttrsHelper.mTextColor = ColorStateList.valueOf(color);\n    updateTextColors();\n  }\n\n  /**\n   * Gets the text colors for the different states (normal, selected, focused) of the TextView.\n   *\n   * @see #setTextColor(ColorStateList)\n   * @see #setTextColor(int)\n   *\n   * @attr ref android.R.styleable#TextView_textColor\n   */\n  public final ColorStateList getTextColors() {\n    return mAttrsHelper.mTextColor;\n  }\n\n  /**\n   * Return the current color selected for normal text.\n   *\n   * @return Returns the current text color.\n   */\n  @ColorInt\n  public final int getCurrentTextColor() {\n    return mCurTextColor;\n  }\n\n  @Override\n  public boolean onTouchEvent(MotionEvent event) {\n    Layout textLayout = getTextLayout();\n    if (textLayout != null) {\n      CharSequence textSource = textLayout.getText();\n      EllipsisSpannedContainer ellipsisSpannedContainer = null;\n      if (LayoutUtils.isEllipsizer(textSource) && mEllipsisSpanned != null) {\n        ellipsisSpannedContainer = mEllipsisSpanned;\n        textSource = ellipsisSpannedContainer.getSourceSpanned();\n      }\n      if (textSource instanceof Spannable) {\n        if (ClickableSpanUtil.handleClickableSpan(this, textLayout, (Spannable) textSource, event)\n            || ClickableSpanUtil.handleClickableSpan(this, textLayout, (Spannable) textSource,\n            ClickableSpanUtil.Clickable.class, event)\n            || (mCustomEllipsisSpan != null\n            && mCustomEllipsisSpan instanceof ClickableSpanUtil.Clickable\n            && ellipsisSpannedContainer != null\n            && ClickableSpanUtil.handleClickableSpan(this, textLayout, ellipsisSpannedContainer,\n            ((ClickableSpanUtil.Clickable) mCustomEllipsisSpan).getClass(), event))) {\n          return true;\n        }\n      }\n    }\n    return super.onTouchEvent(event);\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    int width = MeasureSpec.getSize(widthMeasureSpec);\n    boolean exactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;\n    if (!exactly) {\n      if (mAttrsHelper.mMaxWidth != Integer.MAX_VALUE && width > mAttrsHelper.mMaxWidth) {\n        width = mAttrsHelper.mMaxWidth;\n      }\n    }\n    if (width > 0) {\n      width = width - getPaddingLeft() - getPaddingRight();\n    }\n    if (shouldResetStaticLayout(width, mText, mLayout)) {\n      if (mEnableLayoutCache) {\n        mLayout = TextLayoutCache.STATIC_LAYOUT_CACHE.get(mText);\n        if (mLayout == null) {\n          mLayout = makeLayout(mText, width, exactly);\n          TextLayoutCache.STATIC_LAYOUT_CACHE.put(mText, (StaticLayout) mLayout);\n        }\n      } else {\n        mLayout = makeLayout(mText, width, exactly);\n      }\n    }\n    if (Build.VERSION.SDK_INT <= 19 && mLayout != null) {\n      // when <= api 19, maxLines can not be supported well.\n      int height = mAttrsHelper.mMaxLines < mLayout.getLineCount() ? mLayout.getLineTop\n          (mAttrsHelper.mMaxLines) : mLayout.getHeight();\n      setMeasuredDimension(getMeasuredWidth(getPaddingLeft() + getPaddingRight() + mLayout\n              .getWidth(), widthMeasureSpec),\n          getMeasuredHeight(getPaddingTop() + getPaddingBottom() + height, heightMeasureSpec));\n    } else {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n  }\n\n  protected boolean shouldResetStaticLayout(int width, CharSequence text, Layout layout) {\n    return !TextUtils.isEmpty(text) && width > 0 &&\n        (layout == null || width < layout.getWidth()\n            || (width > layout.getWidth() && layout.getLineCount() > 1)\n            || (width > layout.getWidth() && getContentWidth(text) > layout.getWidth()));\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    long start = System.currentTimeMillis();\n    canvas.save();\n    if (mLayout != null) {\n      int translateX, translateY;\n      int horizontalGravity = getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;\n      switch (horizontalGravity) {\n        default:\n        case Gravity.LEFT:\n          translateX = getPaddingLeft();\n          break;\n        case Gravity.CENTER_HORIZONTAL:\n          translateX = getPaddingLeft() + (getInnerWidth() - mLayout.getWidth()) / 2;\n          break;\n        case Gravity.RIGHT:\n          translateX = getPaddingLeft() + getInnerWidth() - mLayout.getWidth();\n          break;\n      }\n      int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;\n      switch (verticalGravity) {\n        default:\n        case Gravity.TOP:\n          translateY = getPaddingTop();\n          break;\n        case Gravity.CENTER_VERTICAL:\n          translateY = getPaddingTop() + (getInnerHeight() - mLayout.getHeight()) / 2;\n          break;\n        case Gravity.BOTTOM:\n          translateY = getPaddingTop() + getInnerHeight() - mLayout.getHeight();\n          break;\n      }\n      canvas.translate(translateX, translateY);\n      mLayout.draw(canvas);\n    }\n    canvas.restore();\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onDraw cost:\" + (end - start));\n    }\n  }\n\n  public int getInnerWidth() {\n    return getWidth() - getPaddingLeft() - getPaddingRight();\n  }\n\n  private int getInnerHeight() {\n    return getHeight() - getPaddingTop() - getPaddingBottom();\n  }\n\n  public TextPaint getPaint() {\n    return mTextPaint;\n  }\n\n  @Deprecated\n  public TextPaint getTextPaint() {\n    return mTextPaint;\n  }\n\n  public CharSequence getText() {\n    return mText;\n  }\n\n  public void setText(@android.annotation.NonNull CharSequence text) {\n    if (mText != text) {\n      clearTextLayout(false);\n    }\n    mText = text;\n  }\n\n  private void clearTextLayout() {\n    clearTextLayout(false);\n  }\n\n  private void clearTextLayout(boolean cleanCache) {\n    if (mEnableLayoutCache && cleanCache) {\n      TextLayoutCache.STATIC_LAYOUT_CACHE.remove(mText);\n    }\n    mEllipsisSpanned = null;\n    setTextLayout(null);\n    requestLayout();\n    invalidate();\n  }\n\n  /**\n   * Returns the horizontal and vertical alignment of this TextView.\n   *\n   * @see android.view.Gravity\n   */\n  public int getGravity() {\n    return mAttrsHelper.getGravity();\n  }\n\n  /**\n   * Sets the horizontal alignment of the text and the\n   * vertical gravity that will be used when there is extra space\n   * in the TextView beyond what is required for the text itself.\n   *\n   * @see android.view.Gravity\n   */\n  public void setGravity(int gravity) {\n    if (mAttrsHelper.setGravity(gravity)) {\n      clearTextLayout();\n    }\n  }\n\n  public int getMaxWidth() {\n    return mAttrsHelper.mMaxWidth;\n  }\n\n  public void setMaxWidth(int width) {\n    if (mAttrsHelper.mMaxWidth != width) {\n      mAttrsHelper.mMaxWidth = width;\n      clearTextLayout();\n    }\n  }\n\n  public int getMaxLines() {\n    return mAttrsHelper.mMaxLines;\n  }\n\n  public void setMaxLines(int maxLines) {\n    if (mAttrsHelper.mMaxLines != maxLines) {\n      mAttrsHelper.mMaxLines = maxLines;\n      clearTextLayout();\n    }\n  }\n\n  /**\n   * Set the default text size to a given unit and value.  See {@link\n   * TypedValue} for the possible dimension units.\n   *\n   * @param textSize The desired size in the given units.\n   * @param unit The desired dimension unit.\n   */\n  public void setTextSize(float textSize, int unit) {\n    float rawTextSize = TypedValue.applyDimension(\n        unit, textSize, getResources().getDisplayMetrics());\n    if (rawTextSize != mTextPaint.getTextSize()) {\n      mTextPaint.setTextSize(rawTextSize);\n      clearTextLayout();\n    }\n  }\n\n  public float getTextSize() {\n    return mTextPaint.getTextSize();\n  }\n\n  public void setTextSize(float textSize) {\n    setTextSize(textSize, TypedValue.COMPLEX_UNIT_SP);\n  }\n\n  public int getEllipsize() {\n    return mAttrsHelper.mEllipsize;\n  }\n\n  public void setEllipsize(int ellipsize) {\n    if (mAttrsHelper.mEllipsize != ellipsize) {\n      mAttrsHelper.mEllipsize = ellipsize;\n      clearTextLayout();\n    }\n  }\n\n  public ReplacementSpan getCustomEllipsisSpan() {\n    return mCustomEllipsisSpan;\n  }\n\n  public void setCustomEllipsisSpan(ReplacementSpan customEllipsisSpan) {\n    mCustomEllipsisSpan = customEllipsisSpan;\n  }\n\n  public void compressText(boolean enable) {\n    mCompressText = enable;\n  }\n\n  public void addLinks(int mask) {\n    mLinkifyMask = mask;\n    mTextPaint.linkColor = mLinkColor;\n  }\n\n  @NonNull\n  protected StaticLayout makeLayout(CharSequence text, int maxWidth, boolean exactly) {\n    if (mCompressText) {\n      SpannableStringBuilder ssb = new SpannableStringBuilder();\n      String[] patterns = Pattern.compile(\"\\n\").split(text);\n      int patternSize = patterns.length;\n      int realCount = 0;\n      for (int i = 0; i < patternSize; i++) {\n        realCount++;\n        if (patterns[i].isEmpty()) continue;\n        ssb.append(patterns[i]);\n        if (i < patternSize - 1) {\n          ssb.append(\"\\n\");\n        }\n      }\n      if (realCount >= mAttrsHelper.mMaxLines) {\n        ssb.append(\"\\n\").append(\"\\n\"); // extra line to fix ellipse symbol display\n      }\n      text = ssb;\n    } else {\n      text = new SpannableString(text); // also make text into SpannableString.\n    }\n\n    if (mLinkifyMask > 0) {\n      Linkify.addLinks((Spannable) text, mLinkifyMask);\n    }\n\n    TextUtils.TruncateAt truncateAt = getTruncateAt();\n    int layoutTargetWidth = maxWidth;\n    int contentWidth = maxWidth;\n    if (!exactly || truncateAt != null) {\n      contentWidth = getContentWidth(text);\n    }\n    if (!exactly) {\n      layoutTargetWidth = maxWidth > 0 ? Math.min(maxWidth, contentWidth) : contentWidth;\n    }\n\n    StaticLayoutBuilderCompat layoutBuilder = createStaticLayoutBuilder(text, 0, text\n        .length(), mTextPaint, layoutTargetWidth);\n    layoutBuilder.setLineSpacing(mAttrsHelper.mSpacingAdd, mAttrsHelper.mSpacingMultiplier)\n        .setMaxLines(mAttrsHelper.mMaxLines)\n        .setAlignment(TextViewAttrsHelper.getLayoutAlignment(this, getGravity()))\n        .setIncludePad(true);\n    if (truncateAt != null) {\n      layoutBuilder.setEllipsize(truncateAt);\n      EllipsisSpannedContainer ellipsisSpanned =\n          new EllipsisSpannedContainer(text instanceof Spanned ? (Spanned) text : new\n              SpannableString(text));\n      ellipsisSpanned.setCustomEllipsisSpan(mCustomEllipsisSpan);\n      layoutBuilder.setText(ellipsisSpanned);\n      if (contentWidth > layoutTargetWidth * mAttrsHelper.mMaxLines) {\n        int ellipsisWithOffset = (int) mTextPaint.measureText(ReadMoreTextView.ELLIPSIS_NORMAL) - 2;\n\n        // when StaticLayout call calculateEllipsis,\n        // textWidth <= avail will cause do nothing so we should make it different with textWidth\n        if (mCustomEllipsisSpan != null) {\n          layoutBuilder.setEllipsizedWidth(layoutTargetWidth - mCustomEllipsisSpan.getSize\n              (getPaint(), mText, 0, mText.length(), null) + ellipsisWithOffset);\n        } else if (Build.VERSION.SDK_INT <= 19) {\n          ReadMoreTextView.EllipsisSpan ellipsisSpan = new ReadMoreTextView.EllipsisSpan\n              (ReadMoreTextView.ELLIPSIS_NORMAL);\n          ellipsisSpanned.setCustomEllipsisSpan(ellipsisSpan);\n          layoutBuilder.setEllipsizedWidth(layoutTargetWidth - ellipsisSpan.getSize(getPaint(),\n              mText, 0, mText.length(), null) + ellipsisWithOffset);\n        } else {\n          layoutBuilder.setEllipsizedWidth(layoutTargetWidth);\n        }\n      } else {\n        layoutBuilder.setEllipsizedWidth(contentWidth);\n      }\n      beforeStaticLayoutBuild(layoutBuilder);\n      StaticLayout layout = layoutBuilder.build();\n      ellipsisSpanned.setLayout(layout);\n      mEllipsisSpanned = ellipsisSpanned;\n      return layout;\n    }\n    beforeStaticLayoutBuild(layoutBuilder);\n    return layoutBuilder.build();\n  }\n\n  protected StaticLayoutBuilderCompat createStaticLayoutBuilder(CharSequence source,\n      int start, int end,\n      TextPaint paint, int width) {\n    return StaticLayoutBuilderCompat.obtain(source, start, end, paint, width);\n  }\n\n  protected void beforeStaticLayoutBuild(StaticLayoutBuilderCompat layoutBuilder) {\n    // do noting\n  }\n\n  protected int getContentWidth(CharSequence text) {\n    int contentWidth;\n    if (text instanceof Spanned) {\n      contentWidth = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint));\n    } else {\n      contentWidth = (int) Math.ceil(mTextPaint.measureText(text, 0, text.length()));\n    }\n    return contentWidth;\n  }\n\n  protected TextUtils.TruncateAt getTruncateAt() {\n    switch (mAttrsHelper.mEllipsize) {\n      // do not support marque\n      case 1:\n        return TextUtils.TruncateAt.START;\n      case 2:\n        return TextUtils.TruncateAt.MIDDLE;\n      case 3:\n        return TextUtils.TruncateAt.END;\n      default:\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ItalicReplacementSpan.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.text.style.ReplacementSpan;\n\n/**\n * Use getTextBounds to measure Italic text.\n * Since that TextPaint.measureText can not measure Italic text correctly.\n * Note:ItalicReplacementSpan can not include other ReplacementSpan, If you do this you will get incorrect with.\n */\npublic class ItalicReplacementSpan extends ReplacementSpan {\n  public static final float DEFAULT_ITALIC_VALUE = -0.25f;\n  private Rect mRect = new Rect();\n  private float mTextSkewX = DEFAULT_ITALIC_VALUE; // -0.25f is default value for Italic style.\n\n  public ItalicReplacementSpan() {\n    this(DEFAULT_ITALIC_VALUE);\n  }\n\n\n  public ItalicReplacementSpan(float textSkewX) {\n    mTextSkewX = textSkewX;\n  }\n\n  @Override\n  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) {\n    canvas.save();\n    float preTextSkewX = paint.getTextSkewX();\n    paint.setTextSkewX(mTextSkewX);\n    canvas.drawText(text, start, end, x, y, paint);\n    paint.setTextSkewX(preTextSkewX);\n    canvas.restore();\n  }\n\n  /**\n   * Returns the width of the span. Extending classes can set the height of the span by updating\n   * attributes of {@link android.graphics.Paint.FontMetricsInt}. If the span covers the whole\n   * text, and the height is not set,\n   * {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} will not be\n   * called for the span.\n   *\n   * @param paint Paint instance.\n   * @param text  Current text.\n   * @param start Start character index for span.\n   * @param end   End character index for span.\n   * @param fm    Font metrics, can be null.\n   * @return Width of the span.\n   */\n  @Override\n  public int getSize(@NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) {\n    float preTextSkewX = paint.getTextSkewX();\n    paint.setTextSkewX(mTextSkewX);\n    TextMeasureUtil.getTextBounds(paint, text, start, end, mRect);\n    paint.setTextSkewX(preTextSkewX);\n    setHeightIfNeed(text, start, end, fm);\n    return mRect.width();\n  }\n\n  private void setHeightIfNeed(CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) {\n    if (fm != null && text.length() == end - start) {\n      // Extending classes can set the height of the span by updating\n      // attributes of {@link android.graphics.Paint.FontMetricsInt}. If the span covers the whole\n      // text, and the height is not set,\n      // {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} will not be\n      // called for the span.\n      fm.top = mRect.top;\n      fm.bottom = mRect.bottom;\n    }\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/LineUtil.java",
    "content": "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",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.text.style.ReplacementSpan;\n\n// TODO\npublic abstract class NestReplacementSpan extends ReplacementSpan {\n  private boolean mNestSpanEnabled = false;\n\n\n  public boolean isNestSpanEnabled() {\n    return mNestSpanEnabled;\n  }\n\n  public void setNestSpanEnabled(boolean nestSpanEnabled) {\n    this.mNestSpanEnabled = nestSpanEnabled;\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/ReadMoreTextView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.Spannable;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.StaticLayout;\nimport android.text.StaticLayoutBuilderCompat;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.text.style.ReplacementSpan;\nimport android.text.util.Linkify;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.View;\n\npublic class ReadMoreTextView extends FastTextView {\n  public static final String ELLIPSIS_NORMAL = \"\\u2026\"; // this is \"...\"\n  public static final String COLLAPSE_NORMAL = \"\\u25b2\"; // this is \"▲\"\n  static final String TAG = ReadMoreTextView.class.getSimpleName();\n  protected boolean mIsShowAll;\n  protected StaticLayout mAllTextLayout;\n  protected StaticLayout mWithEllipsisLayout;\n  protected ReplacementSpan mCollapseSpan = new EllipsisSpan(COLLAPSE_NORMAL);\n\n  public ReadMoreTextView(Context context) {\n    this(context, null);\n  }\n\n  public ReadMoreTextView(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, -1);\n  }\n\n  public ReadMoreTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    setCustomEllipsisSpan(new EllipsisSpan(ELLIPSIS_NORMAL));\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public ReadMoreTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,\n                          int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n    setCustomEllipsisSpan(new EllipsisSpan(ELLIPSIS_NORMAL));\n  }\n\n  @Override\n  public void setText(CharSequence text) {\n    if (text != getText()) {\n      mIsShowAll = false;\n      mWithEllipsisLayout = null;\n      mAllTextLayout = null;\n    }\n    super.setText(text);\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    long start = System.currentTimeMillis();\n    int width = MeasureSpec.getSize(widthMeasureSpec);\n    boolean exactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;\n    if (!exactly) {\n      if (mAttrsHelper.mMaxWidth != Integer.MAX_VALUE && width > mAttrsHelper.mMaxWidth) {\n        width = mAttrsHelper.mMaxWidth;\n      }\n    }\n    if (width > 0) {\n      width = width - getPaddingLeft() - getPaddingRight();\n    }\n    if (!TextUtils.isEmpty(getText()) && width > 0 &&\n        (mLayout == null || width < mLayout.getWidth()\n            || (width > mLayout.getWidth() && mLayout.getLineCount() > 1))) {\n      mLayout = makeLayout(getText(), width, exactly);\n    }\n    if (mWithEllipsisLayout != null && !mIsShowAll) {\n      mLayout = mWithEllipsisLayout;\n      setMeasuredDimension(getMeasuredWidth(getPaddingLeft() + getPaddingRight() + mWithEllipsisLayout.getWidth(), widthMeasureSpec),\n          getMeasuredHeight(getPaddingTop() + getPaddingBottom() + mWithEllipsisLayout.getHeight(), heightMeasureSpec));\n    } else if (mAllTextLayout != null && mIsShowAll) {\n      mLayout = mAllTextLayout;\n      setMeasuredDimension(getMeasuredWidth(getPaddingLeft() + getPaddingRight() + mAllTextLayout.getWidth(), widthMeasureSpec),\n          getMeasuredHeight(getPaddingTop() + getPaddingBottom() + mAllTextLayout.getHeight(),\n              heightMeasureSpec));\n    } else {\n      super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onMeasure cost:\" + (end - start));\n    }\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    long start = System.currentTimeMillis();\n    canvas.save();\n    if (mWithEllipsisLayout != null && !mIsShowAll) {\n      canvas.translate(getPaddingLeft(), getPaddingTop());\n      mWithEllipsisLayout.draw(canvas);\n    } else if (mAllTextLayout != null && mIsShowAll) {\n      canvas.translate(getPaddingLeft(), getPaddingTop());\n      mAllTextLayout.draw(canvas);\n    }\n    canvas.restore();\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onDraw cost:\" + (end - start));\n    }\n  }\n\n  @NonNull\n  @Override\n  protected StaticLayout makeLayout(CharSequence text, int maxWidth, boolean exactly) {\n    mWithEllipsisLayout = super.makeLayout(text, maxWidth, exactly);\n    SpannableStringBuilder textWithExtraEnd = new SpannableStringBuilder(text);\n    if (mLinkifyMask > 0) {\n      Linkify.addLinks(textWithExtraEnd, mLinkifyMask);\n    }\n    if (mCollapseSpan != null) {\n      textWithExtraEnd.append(COLLAPSE_NORMAL);\n      textWithExtraEnd.setSpan(mCollapseSpan, textWithExtraEnd.length() - 1,\n          textWithExtraEnd.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);\n    }\n    StaticLayoutBuilderCompat layoutBuilder =\n        createAllStaticLayoutBuilder(textWithExtraEnd, 0, textWithExtraEnd.length(), getPaint(),\n            maxWidth > 0 ? Math.min(maxWidth, mWithEllipsisLayout.getWidth()) :\n                mWithEllipsisLayout.getWidth());\n    layoutBuilder.setLineSpacing(mAttrsHelper.mSpacingAdd, mAttrsHelper.mSpacingMultiplier)\n        .setAlignment(TextViewAttrsHelper.getLayoutAlignment(this, getGravity()))\n        .setIncludePad(true);\n    beforeAllStaticLayoutBuild(layoutBuilder);\n    mAllTextLayout = layoutBuilder.build();\n    return mWithEllipsisLayout;\n  }\n\n  @Override\n  final protected StaticLayoutBuilderCompat createStaticLayoutBuilder(CharSequence source,\n                                                                      int start, int end,\n                                                                      TextPaint paint, int width) {\n    return createEllipsisStaticLayoutBuilder(source, start, end, paint, width);\n  }\n\n  @Override\n  final protected void beforeStaticLayoutBuild(StaticLayoutBuilderCompat layoutBuilder) {\n    beforeEllipsisStaticLayoutBuild(layoutBuilder);\n  }\n\n  protected StaticLayoutBuilderCompat createAllStaticLayoutBuilder(CharSequence source,\n                                                                   int start, int end,\n                                                                   TextPaint paint, int width) {\n    return StaticLayoutBuilderCompat.obtain(source, start, end, paint, width);\n  }\n\n  protected StaticLayoutBuilderCompat createEllipsisStaticLayoutBuilder(CharSequence source,\n                                                                        int start, int end,\n                                                                        TextPaint paint,\n                                                                        int width) {\n    return StaticLayoutBuilderCompat.obtain(source, start, end, paint, width);\n  }\n\n  protected void beforeAllStaticLayoutBuild(StaticLayoutBuilderCompat layoutBuilder) {\n    // do noting\n  }\n\n  protected void beforeEllipsisStaticLayoutBuild(StaticLayoutBuilderCompat layoutBuilder) {\n    // do noting\n  }\n\n\n  public void showAll() {\n    mIsShowAll = true;\n    mLayout = mAllTextLayout;\n    requestLayout();\n  }\n\n  public void showEllipsis() {\n    mIsShowAll = false;\n    mLayout = mWithEllipsisLayout;\n    requestLayout();\n  }\n\n  public void setCustomCollapseSpan(ReplacementSpan collapseSpan) {\n    mCollapseSpan = collapseSpan;\n  }\n\n  public boolean isShowAll() {\n    return mIsShowAll;\n  }\n\n  public static class EllipsisSpan extends ReplacementSpan implements ClickableSpanUtil.Clickable {\n    String mText;\n\n    public EllipsisSpan(String text) {\n      mText = text;\n    }\n\n    @Override\n    public void draw(@NonNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start,\n                     @IntRange(from = 0) int end, float x, int top, int y, int bottom,\n                     @NonNull Paint paint) {\n      canvas.drawText(mText, 0, mText.length(), x, y, paint);\n    }\n\n    @Override\n    public int getSize(@NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start,\n                       @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) {\n      return (int) Math.ceil(paint.measureText(mText, 0, mText.length()));\n    }\n\n    @Override\n    public void onClick(View widget) {\n      ReadMoreTextView readMoreTextView = (ReadMoreTextView) widget;\n      if (readMoreTextView.isShowAll()) {\n        readMoreTextView.showEllipsis();\n      } else {\n        readMoreTextView.showAll();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/SingleLineTextView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.BoringLayout;\nimport android.text.Layout;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.Gravity;\n\n/**\n * Special for Single line text.\n * 1.Plain text single line rendering\n * 2.Spanned text single line rendering\n * 3.Ellipsis\n */\npublic class SingleLineTextView extends FastTextLayoutView {\n  private CharSequence mText;\n  private TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);\n  TextViewAttrsHelper mAttrsHelper = new TextViewAttrsHelper();\n\n  public SingleLineTextView(Context context) {\n    this(context, null);\n  }\n\n  public SingleLineTextView(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, -1);\n  }\n\n  public SingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    init(context, attrs, defStyleAttr, -1);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public SingleLineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n    init(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n  private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    mAttrsHelper.init(context, attrs, defStyleAttr, defStyleRes);\n    setText(mAttrsHelper.mText);\n    TextPaint textPaint = getPaint();\n    textPaint.setColor(mAttrsHelper.mTextColor.getDefaultColor());\n    textPaint.setTextSize(mAttrsHelper.mTextSize);\n  }\n\n  public TextPaint getPaint() {\n    return mTextPaint;\n  }\n\n  /**\n   * Sets the horizontal alignment of the text and the\n   * vertical gravity that will be used when there is extra space\n   * in the TextView beyond what is required for the text itself.\n   *\n   * @see android.view.Gravity\n   */\n  public void setGravity(int gravity) {\n    if (mAttrsHelper.setGravity(gravity)) {\n      setTextLayout(null);\n    }\n  }\n\n  /**\n   * Returns the horizontal and vertical alignment of this TextView.\n   *\n   * @see android.view.Gravity\n   */\n  public int getGravity() {\n    return mAttrsHelper.getGravity();\n  }\n\n\n\n  public void setMaxWidth(int width) {\n    if (mAttrsHelper.mMaxWidth != width) {\n      mAttrsHelper.mMaxWidth = width;\n      setTextLayout(null);\n    }\n  }\n\n  public int getMaxWidth() {\n    return mAttrsHelper.mMaxWidth;\n  }\n\n  public void setMaxLines(int maxLines) {\n    if (mAttrsHelper.mMaxLines != maxLines) {\n      mAttrsHelper.mMaxLines = maxLines;\n      setTextLayout(null);\n    }\n  }\n\n  public int getMaxLines() {\n    return mAttrsHelper.mMaxLines;\n  }\n\n  public void setTextSize(float textSize) {\n    setTextSize(textSize, TypedValue.COMPLEX_UNIT_SP);\n  }\n\n  /**\n   * Set the default text size to a given unit and value.  See {@link\n   * TypedValue} for the possible dimension units.\n   *\n   * @param textSize The desired size in the given units.\n   * @param unit     The desired dimension unit.\n   */\n  public void setTextSize(float textSize, int unit) {\n    float rawTextSize = TypedValue.applyDimension(\n        unit, textSize, getResources().getDisplayMetrics());\n    mTextPaint.setTextSize(rawTextSize);\n  }\n\n  public float getTextSize() {\n    return mTextPaint.getTextSize();\n  }\n\n  public int getEllipsize() {\n    return mAttrsHelper.mEllipsize;\n  }\n\n  public void setEllipsize(int ellipsize) {\n    if (mAttrsHelper.mEllipsize != ellipsize) {\n      mAttrsHelper.mEllipsize = ellipsize;\n      setTextLayout(null);\n    }\n  }\n\n  public void setText(CharSequence text) {\n    mText = text;\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    super.onDraw(canvas);\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    if (mLayout == null && !TextUtils.isEmpty(mText)) {\n      BoringLayout.Metrics fm = new BoringLayout.Metrics();\n      fm.width = (int) Layout.getDesiredWidth(mText, mTextPaint);\n      mLayout = BoringLayout.make(mText, mTextPaint, MeasureSpec.getSize(widthMeasureSpec),\n          TextViewAttrsHelper.getLayoutAlignment(this, getGravity()), mAttrsHelper.mSpacingMultiplier, mAttrsHelper.mSpacingAdd, fm, true);\n    }\n    super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n  }\n\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokableTextView.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.annotation.RequiresApi;\nimport android.text.Spanned;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\npublic class StrokableTextView extends FastTextView {\n  private static final String TAG = \"StrokableTextView\";\n\n  public StrokableTextView(Context context) {\n    super(context);\n  }\n\n  public StrokableTextView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public StrokableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n  public StrokableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    super(context, attrs, defStyleAttr, defStyleRes);\n  }\n\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    long start = System.currentTimeMillis();\n    CharSequence text = getText();\n    if (text instanceof Spanned) {\n      StrokeSpan[] strokeSpans = StrokeSpanUtil.getStrokeSpans((Spanned) text);\n      if (strokeSpans != null && strokeSpans.length > 0) {\n        StrokeSpanUtil.startStroke(strokeSpans);\n        super.onDraw(canvas);\n        StrokeSpanUtil.endStroke(strokeSpans);\n      }\n    }\n    super.onDraw(canvas);\n    long end = System.currentTimeMillis();\n    if (BuildConfig.DEBUG) {\n      Log.d(TAG, \"onDraw cost:\" + (end - start));\n    }\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeReplacementSpan.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.text.style.ReplacementSpan;\n\npublic class StrokeReplacementSpan extends ReplacementSpan {\n  private final int mStrokeColor;\n  private final int mTextColor;\n  private final int mStrokeWidth;\n  private Rect mRect = new Rect();\n\n  public StrokeReplacementSpan(int textColor, int strokeColor, int strokeWidth) {\n    mTextColor = textColor;\n    mStrokeColor = strokeColor;\n    mStrokeWidth = strokeWidth;\n  }\n\n  @Override\n  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) {\n    canvas.save();\n    canvas.translate(mStrokeWidth, 0);\n    paint.setColor(mStrokeColor);\n    paint.setStrokeCap(Paint.Cap.ROUND);\n    paint.setStrokeWidth(mStrokeWidth);\n    paint.setStyle(Paint.Style.STROKE);\n    canvas.drawText(text, start, end, x, y, paint);\n    paint.setColor(mTextColor);\n    paint.setStyle(Paint.Style.FILL);\n    canvas.drawText(text, start, end, x, y, paint);\n    canvas.restore();\n  }\n\n  @Override\n  public int getSize(@NonNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm) {\n    TextMeasureUtil.getTextBounds(paint, text, start, end, mRect);\n    if (fm != null && text.length() == end - start) {\n      // Extending classes can set the height of the span by updating\n      // attributes of {@link android.graphics.Paint.FontMetricsInt}. If the span covers the whole\n      // text, and the height is not set,\n      // {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} will not be\n      // called for the span.\n      fm.top = mRect.top - mStrokeWidth / 2;\n      fm.bottom = mRect.bottom + mStrokeWidth / 2;\n    }\n    return mRect.width() + mStrokeWidth * 2;\n  }\n\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpan.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Paint;\nimport android.text.TextPaint;\nimport android.text.style.CharacterStyle;\n\n/**\n * 描边Span,需要配合override textview的onDraw方法使用.\n */\npublic class StrokeSpan extends CharacterStyle {\n  private final int mStrokeColor;\n  private final int mTextColor;\n  private final int mStrokeWidth;\n  private boolean mDrawStroke = true;\n\n  @Override\n  public void updateDrawState(TextPaint tp) {\n    if (mDrawStroke) {\n      tp.setColor(mStrokeColor);\n      tp.setStrokeCap(Paint.Cap.ROUND);\n      tp.setStrokeWidth(mStrokeWidth);\n      tp.setStyle(Paint.Style.STROKE);\n    } else {\n      tp.setColor(mTextColor);\n      tp.setStyle(Paint.Style.FILL);\n    }\n  }\n\n  public StrokeSpan(int textColor, int strokeColor, int strokeWidth) {\n    mTextColor = textColor;\n    mStrokeColor = strokeColor;\n    mStrokeWidth = strokeWidth;\n  }\n\n  public void startStroke() {\n    mDrawStroke = true;\n  }\n\n  public void endStroke() {\n    mDrawStroke = false;\n  }\n\n  public boolean isStrokeDrawing() {\n    return mDrawStroke;\n  }\n}\n\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/StrokeSpanUtil.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.text.Spanned;\n\npublic class StrokeSpanUtil {\n\n  public static StrokeSpan[] getStrokeSpans(Spanned spanned) {\n    return spanned.getSpans(0, spanned.length(), StrokeSpan.class);\n  }\n\n  public static void startStroke(StrokeSpan[] strokeSpans) {\n    if (strokeSpans != null && strokeSpans.length > 0) {\n      for (StrokeSpan span : strokeSpans) {\n        span.startStroke();\n      }\n    }\n  }\n\n  public static void endStroke(StrokeSpan[] strokeSpans) {\n    if (strokeSpans != null && strokeSpans.length > 0) {\n      for (StrokeSpan span : strokeSpans) {\n        span.endStroke();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextMeasureUtil.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.support.annotation.IntRange;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport android.text.style.ReplacementSpan;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.LinkedList;\nimport java.util.List;\n\n/**\n * Util for TextMeasurement.\n */\npublic class TextMeasureUtil {\n\n  /**\n   * Do not support cross Span.\n   *\n   * @param text       text\n   * @param parentSpan parentSpan\n   * @param start      start index of parentSpan\n   * @param end        end index of parentSpan\n   * @param paint      TextPaint\n   * @return recursive calculated width\n   */\n  public int recursiveGetSizeWithReplacementSpan(CharSequence text, ReplacementSpan parentSpan, @IntRange(from = 0) int start, @IntRange(from = 0) int end, Paint paint) {\n    if (text instanceof Spanned) {\n      Spanned spannedText = (Spanned) text;\n      List<ReplacementSpan> spans = getSortedReplacementSpans(spannedText, start, end);\n      if (!spans.isEmpty()) {\n        int lastIndexCursor = 0;\n        int width = 0;\n        for (ReplacementSpan span : spans) {\n          if (span == parentSpan) {\n            continue;\n          }\n          int spanStart = spannedText.getSpanStart(span);\n          int spanEnd = spannedText.getSpanEnd(span);\n          width += parentSpan.getSize(paint, text, lastIndexCursor, spanStart, null);\n          width += span.getSize(paint, text, spanStart, spanEnd, null);\n          lastIndexCursor = spanEnd;\n        }\n        if (lastIndexCursor < end) {\n          width += parentSpan.getSize(paint, text, lastIndexCursor, end, null);\n        }\n        return width;\n      }\n    }\n    return parentSpan.getSize(paint, text, start, end, null);\n  }\n\n  public static void getTextBounds(Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, Rect rect) {\n    char[] currentText = new char[end - start];\n    TextUtils.getChars(text, start, end, currentText, 0);\n    paint.getTextBounds(currentText, 0, end - start, rect);\n  }\n\n  public static List<ReplacementSpan> getSortedReplacementSpans(final Spanned spanned, int start, int end) {\n    List<ReplacementSpan> sortedSpans = new LinkedList<>();\n    ReplacementSpan[] spans = spanned.getSpans(start, end, ReplacementSpan.class);\n    if (spans.length > 0) {\n      sortedSpans.addAll(Arrays.asList(spans));\n    }\n    Collections.sort(sortedSpans, new Comparator<ReplacementSpan>() {\n      @Override\n      public int compare(ReplacementSpan span1, ReplacementSpan span2) {\n        return spanned.getSpanStart(span1) - spanned.getSpanStart(span2);\n      }\n    });\n    return sortedSpans;\n  }\n\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/java/com/lsjwzh/widget/text/TextViewAttrsHelper.java",
    "content": "package com.lsjwzh.widget.text;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.support.annotation.Nullable;\nimport android.support.v4.view.ViewCompat;\nimport android.text.Layout;\nimport android.util.AttributeSet;\nimport android.view.Gravity;\nimport android.view.View;\n\nimport static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL;\n\npublic class TextViewAttrsHelper {\n  public int mSpacingAdd;\n  public float mSpacingMultiplier = 1f;\n  public int mMaxWidth = Integer.MAX_VALUE;\n  public int mMaxLines = Integer.MAX_VALUE;\n  public int mEllipsize = -1;\n  public ColorStateList mTextColor = ColorStateList.valueOf(Color.BLACK);\n  public int mTextSize = 15;\n  public CharSequence mText;\n  private int mGravity;\n\n  public void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n    final Resources.Theme theme = context.getTheme();\n    TypedArray a = theme.obtainStyledAttributes(attrs,\n        com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);\n    int n = a.getIndexCount();\n    for (int i = 0; i < n; i++) {\n      int attr = a.getIndex(i);\n      switch (attr) {\n        case com.android.internal.R.styleable.TextView_gravity:\n          mGravity = a.getInt(attr, Gravity.TOP | Gravity.LEFT);\n          break;\n        case com.android.internal.R.styleable.TextView_text:\n          mText = a.getText(attr);\n          break;\n        case com.android.internal.R.styleable.TextView_ellipsize:\n          mEllipsize = a.getInt(attr, mEllipsize);\n          break;\n        case com.android.internal.R.styleable.TextView_maxLines:\n          mMaxLines = a.getInt(attr, Integer.MAX_VALUE);\n          break;\n        case com.android.internal.R.styleable.TextView_textColor:\n          // Do not support ColorState\n          mTextColor = a.getColorStateList(attr);\n          break;\n        case com.android.internal.R.styleable.TextView_textSize:\n          mTextSize = a.getDimensionPixelSize(attr, 15);\n          break;\n        case com.android.internal.R.styleable.TextView_lineSpacingExtra:\n          mSpacingAdd = a.getDimensionPixelSize(attr, mSpacingAdd);\n          break;\n        case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:\n          mSpacingMultiplier = a.getFloat(attr, mSpacingMultiplier);\n          break;\n      }\n    }\n  }\n\n  \n\n  /**\n   * Sets the horizontal alignment of the text and the\n   * vertical gravity that will be used when there is extra space\n   * in the TextView beyond what is required for the text itself.\n   *\n   * @see android.view.Gravity\n   */\n  public boolean setGravity(int gravity) {\n    if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {\n      gravity |= Gravity.START;\n    }\n    if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {\n      gravity |= Gravity.TOP;\n    }\n\n    boolean newLayout = false;\n\n    if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=\n        (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {\n      newLayout = true;\n    }\n\n    if (gravity != mGravity) {\n      newLayout = true;\n    }\n\n    mGravity = gravity;\n    return newLayout;\n  }\n\n  /**\n   * Returns the horizontal and vertical alignment of this TextView.\n   *\n   * @see android.view.Gravity\n   */\n  public int getGravity() {\n    return mGravity;\n  }\n\n  public static Layout.Alignment getLayoutAlignment(View view, int gravity) {\n    Layout.Alignment alignment;\n    if (Build.VERSION.SDK_INT >= 17) {\n      switch (view.getTextAlignment()) {\n        case android.view.View.TEXT_ALIGNMENT_GRAVITY:\n          alignment = getAlignmentByGravity(gravity);\n          break;\n        case android.view.View.TEXT_ALIGNMENT_TEXT_START:\n          alignment = Layout.Alignment.ALIGN_NORMAL;\n          break;\n        case android.view.View.TEXT_ALIGNMENT_TEXT_END:\n          alignment = Layout.Alignment.ALIGN_OPPOSITE;\n          break;\n        case android.view.View.TEXT_ALIGNMENT_CENTER:\n          alignment = Layout.Alignment.ALIGN_CENTER;\n          break;\n        case android.view.View.TEXT_ALIGNMENT_VIEW_START:\n          alignment = (ViewCompat.getLayoutDirection(view) == LAYOUT_DIRECTION_RTL) ?\n              Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;\n          break;\n        case android.view.View.TEXT_ALIGNMENT_VIEW_END:\n          alignment = (ViewCompat.getLayoutDirection(view) == LAYOUT_DIRECTION_RTL) ?\n              Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;\n          break;\n        case android.view.View.TEXT_ALIGNMENT_INHERIT:\n          // This should never happen as we have already resolved the text alignment\n          // but better safe than sorry so we just fall through\n        default:\n          alignment = Layout.Alignment.ALIGN_NORMAL;\n          break;\n      }\n      return alignment;\n    } else {\n      return getAlignmentByGravity(gravity);\n    }\n  }\n\n  private static Layout.Alignment getAlignmentByGravity(int gravity) {\n    switch (gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {\n      case Gravity.START:\n        return Layout.Alignment.ALIGN_NORMAL;\n      case Gravity.END:\n        return Layout.Alignment.ALIGN_OPPOSITE;\n      case Gravity.LEFT:\n        return Layout.Alignment.ALIGN_LEFT;\n      case Gravity.RIGHT:\n        return Layout.Alignment.ALIGN_RIGHT;\n      case Gravity.CENTER_HORIZONTAL:\n        return Layout.Alignment.ALIGN_CENTER;\n      default:\n        return Layout.Alignment.ALIGN_NORMAL;\n    }\n  }\n}\n"
  },
  {
    "path": "widget.FastTextView/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <declare-styleable name=\"FastTextView\">\n    <attr name=\"enableLayoutCache\" format=\"boolean\"/>\n  </declare-styleable>\n</resources>"
  }
]