Initial commit
35
.editorconfig
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# noinspection EditorConfigKeyCorrectness
|
||||||
|
[{*.kt,*.kts}]
|
||||||
|
ktlint_standard_annotation = disabled
|
||||||
|
ktlint_standard_filename = disabled
|
||||||
|
ktlint_standard_wrapping = disabled
|
||||||
|
ktlint_standard_import-ordering = enabled
|
||||||
|
ktlint_standard_max-line-length = disabled
|
||||||
|
ktlint_standard_multiline-if-else = disabled
|
||||||
|
ktlint_standard_argument-list-wrapping = disabled
|
||||||
|
ktlint_standard_parameter-list-wrapping = disabled
|
||||||
|
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||||
|
ktlint_function_signature_body_expression_wrapping = multiline
|
||||||
|
ktlint_standard_string-template-indent = disabled
|
||||||
|
ktlint_standard_function-signature = disabled
|
||||||
|
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||||
|
ktlint_standard_multiline-expression-wrapping = disabled
|
||||||
|
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
||||||
|
ktlint_standard_if-else-wrapping = disabled
|
||||||
|
ktlint_standard_if-else-bracing = disabled
|
||||||
|
ktlint_standard_statement-wrapping = disabled
|
||||||
|
ktlint_standard_blank-line-before-declaration = disabled
|
||||||
|
ktlint_standard_no-empty-file = disabled
|
||||||
|
ktlint_standard_property-naming = disabled
|
||||||
|
ktlint_standard_function-naming = disabled
|
||||||
|
ktlint_standard_chain-method-continuation = disabled
|
||||||
|
ktlint_standard_class-signature = disabled
|
||||||
|
ktlint_standard_condition-wrapping = disabled
|
||||||
|
ktlint_standard_class-signature = disabled
|
||||||
|
ktlint_standard_no-trailing-spaces = disabled
|
||||||
|
ktlint_standard_multiline-loop = disabled
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = false
|
||||||
|
max_line_length = 150
|
57
.github/workflows/docs-deploy.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Deploy to GitHub pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'pangutext-android/src/**'
|
||||||
|
- 'docs-source/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
if: ${{ success() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- name: Prepare Java 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
java-package: jdk
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: 'gradle'
|
||||||
|
- name: Cache Gradle Dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
!~/.gradle/caches/build-cache-*
|
||||||
|
key: gradle-deps-core-${{ hashFiles('**/build.gradle.kts') }}
|
||||||
|
restore-keys: |
|
||||||
|
gradle-deps
|
||||||
|
- name: Build VuePress site
|
||||||
|
run: |
|
||||||
|
cd docs-source
|
||||||
|
yarn -i
|
||||||
|
yarn docs:build-gh-pages
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: crazy-max/ghaction-github-pages@v4
|
||||||
|
with:
|
||||||
|
target_branch: gh-pages
|
||||||
|
build_dir: docs-source/dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
107
.gitignore
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
## Fully .gtignore for IntelliJ, Android Studio and Gradle based Java projects
|
||||||
|
## References:
|
||||||
|
## - https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
## - https://github.com/android/platform-samples/blob/main/.gitignore
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
.idea/.name
|
||||||
|
.idea/artifacts
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/*.iml
|
||||||
|
.idea/modules
|
||||||
|
.idea/caches
|
||||||
|
.idea/material_theme**
|
||||||
|
.idea/other.xml
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.idea/misc.xml
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# Android studio 3.1+ additional
|
||||||
|
.idea/deployment*.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/androidTestResultsUserPreferences.xml
|
||||||
|
|
||||||
|
# Android projects
|
||||||
|
**/local.properties
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
|
||||||
|
# Gradle projects
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Mkdocs temporary serving folder
|
||||||
|
docs-gen
|
||||||
|
site
|
||||||
|
*.bak
|
||||||
|
.idea/appInsightsSettings.xml
|
||||||
|
|
||||||
|
# Mac OS
|
||||||
|
.DS_Store
|
BIN
.idea/icon.png
generated
Normal file
After Width: | Height: | Size: 31 KiB |
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="2.0.21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/ktlint-plugin.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="com.nbadal.ktlint.KtlintProjectSettings">
|
||||||
|
<ktlintMode>MANUAL</ktlintMode>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
92
README-zh-CN.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Pangu Text
|
||||||
|
|
||||||
|
[](https://github.com/BetterAndroid/android-app-template/blob/main/LICENSE)
|
||||||
|
[](https://t.me/BetterAndroid)
|
||||||
|
[](https://t.me/HighCapable_Dev)
|
||||||
|
[](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf)
|
||||||
|
|
||||||
|
<img src="img-src/icon.png" width = "100" height = "100" alt="LOGO"/>
|
||||||
|
|
||||||
|
一个中日韩 (CJK) 与英文单词、半角数字排版的解决方案。
|
||||||
|
|
||||||
|
[English](README.md) | 简体中文
|
||||||
|
|
||||||
|
| <img src="https://github.com/BetterAndroid/.github/blob/main/img-src/logo.png?raw=true" width = "30" height = "30" alt="LOGO"/> | [BetterAndroid](https://github.com/BetterAndroid) |
|
||||||
|
|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
|
||||||
|
|
||||||
|
这个项目属于上述组织,**点击上方链接关注这个组织**,发现更多好项目。
|
||||||
|
|
||||||
|
## 项目缘由
|
||||||
|
|
||||||
|
这个项目的起因是因为直到目前为止还没有一套公开的方案能够完美解决中文、日文、韩文与英文之间的排版问题,
|
||||||
|
正常情况下我们将 CJK (即中日韩) 与英文混排的时候,都会涉及到美观性问题,这算是一个历史遗留问题,全角文字与半角文字之间的书写规范不一样。虽然现在 W3C 规定了
|
||||||
|
CJK 排版规范,
|
||||||
|
但是还是仅有部分愿意遵守排版要求的个人或企业选择了这种方案。
|
||||||
|
|
||||||
|
目前已知的厂商解决方案如下
|
||||||
|
|
||||||
|
- Apple 全系 (iOS、iPadOS、macOS、tvOS、watchOS) 文本排版解决方案
|
||||||
|
- 小米 (HyperOS) 文本排版优化
|
||||||
|
- OrginOS 基于字体的文本排版优化
|
||||||
|
|
||||||
|
但是这些方案都是封闭的,无法在其他平台上使用,因此我们希望能够提供一套开源的解决方案,能够适应各种场景、侵入性低且更容易集成,让更多的开发者能够使用这个方案来解决文本排版问题。
|
||||||
|
|
||||||
|
本项目得以进行的主要来源为 [pangu.js](https://github.com/vinta/pangu.js),它提供了一套 CJK 排版的正则,我们对其加以优化,实现各个平台不需要插入空格字符即可格式化文本排版的效果,
|
||||||
|
衷心感谢这个项目的开发者提供的方案,我们在这个方案上加以扩展,提供了更多解决方案的可能性。
|
||||||
|
|
||||||
|
## 效果
|
||||||
|
|
||||||
|
如你所见,`PanguText` 的排版方案并不是向 CJK 与英文单词之间插入空格来完成,而是使用每个平台对应的处理方案自动在这些字符之间添加空白间距来达到排版效果以达到最低的侵入性。
|
||||||
|
|
||||||
|
> 应用前 (上)、应用后 (下)
|
||||||
|
|
||||||
|
<img src="docs-source/src/.vuepress/public/images/demo_01.png" width="300" />
|
||||||
|
|
||||||
|
> 动态应用
|
||||||
|
|
||||||
|
<img src="docs-source/src/.vuepress/public/images/demo_02.gif" width="480" />
|
||||||
|
|
||||||
|
`PanguText` 支持动态应用,它允许你在输入文本的同时动态为每个字符添加空白间距。
|
||||||
|
|
||||||
|
## 开始使用
|
||||||
|
|
||||||
|
[点击这里](https://betterandroid.github.io/PanguText/zh-cn) 前往文档页面查看更多详细教程和内容。
|
||||||
|
|
||||||
|
## 项目推广
|
||||||
|
|
||||||
|
<!--suppress HtmlDeprecatedAttribute -->
|
||||||
|
<div align="center">
|
||||||
|
<h2>嘿,还请君留步!👋</h2>
|
||||||
|
<h3>这里有 Android 开发工具、UI 设计、Gradle 插件、Xposed 模块和实用软件等相关项目。</h3>
|
||||||
|
<h3>如果下方的项目能为你提供帮助,不妨为我点个 star 吧!</h3>
|
||||||
|
<h3>所有项目免费、开源,遵循对应开源许可协议。</h3>
|
||||||
|
<h1><a href="https://github.com/fankes/fankes/blob/main/project-promote/README-zh-CN.md">→ 查看更多关于我的项目,请点击这里 ←</a></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
- [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Copyright (C) 2019 HighCapable
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
```
|
||||||
|
|
||||||
|
版权所有 © 2019 HighCapable
|
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Pangu Text
|
||||||
|
|
||||||
|
[](https://github.com/BetterAndroid/android-app-template/blob/main/LICENSE)
|
||||||
|
[](https://t.me/BetterAndroid)
|
||||||
|
[](https://t.me/HighCapable_Dev)
|
||||||
|
[](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf)
|
||||||
|
|
||||||
|
<img src="img-src/icon.png" width = "100" height = "100" alt="LOGO"/>
|
||||||
|
|
||||||
|
A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
|
||||||
|
English | [简体中文](README-zh-CN.md)
|
||||||
|
|
||||||
|
| <img src="https://github.com/BetterAndroid/.github/blob/main/img-src/logo.png?raw=true" width = "30" height = "30" alt="LOGO"/> | [BetterAndroid](https://github.com/BetterAndroid) |
|
||||||
|
|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
|
||||||
|
|
||||||
|
This project belongs to the above-mentioned organization, **click the link above to follow this organization** and discover more good projects.
|
||||||
|
|
||||||
|
## Project Reason
|
||||||
|
|
||||||
|
This project was created because, until now, there hasn’t been a public solution to perfectly address the typography issues between Chinese, Japanese,
|
||||||
|
Korean, and English.
|
||||||
|
Typically, when mixing CJK (i.e. Chinese, Japanese, Korean) with English, aesthetic issues can arise—a historical legacy stemming from the differences
|
||||||
|
in writing conventions between full-width and half-width characters. Although the W3C has now established CJK typography guidelines, only a few
|
||||||
|
individuals or companies willing to adhere to these standards have adopted this approach.
|
||||||
|
|
||||||
|
Currently, the known vendor solutions are as follows:
|
||||||
|
|
||||||
|
- Apple platforms (iOS, iPadOS, macOS, tvOS, watchOS) text typography solutions
|
||||||
|
- Xiaomi’s (HyperOS) text typography optimization
|
||||||
|
- OrginOS’s font-based text typography optimization
|
||||||
|
|
||||||
|
However, these solutions are closed and cannot be implemented on other platforms.
|
||||||
|
We aim to provide an open-source solution adaptable to various scenarios, featuring low intrusiveness and easy integration, allowing more developers
|
||||||
|
to effectively address text typography issues.
|
||||||
|
|
||||||
|
The primary inspiration for this project comes from [pangu.js](https://github.com/vinta/pangu.js), which offers a set of regular expressions for CJK
|
||||||
|
typography.
|
||||||
|
We have optimized these solutions to format text across platforms without inserting extra space characters. We extend this approach further to explore
|
||||||
|
additional possibilities.
|
||||||
|
|
||||||
|
Heartfelt thanks to the original developer of **pangu.js** for providing the foundational solution.
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
As you can see, the typography scheme of `PanguText` does not work by simply inserting spaces between CJK characters and English words.
|
||||||
|
Instead, it leverages each platform's native handling to automatically add whitespace between these characters, ensuring minimal intrusion.
|
||||||
|
|
||||||
|
> Before Applying (Top) vs. After Applying (Bottom)
|
||||||
|
|
||||||
|
<img src="docs-source/src/.vuepress/public/images/demo_01.png" width="300" />
|
||||||
|
|
||||||
|
> Dynamic Application
|
||||||
|
|
||||||
|
<img src="docs-source/src/.vuepress/public/images/demo_02.gif" width="480" />
|
||||||
|
|
||||||
|
`PanguText` supports dynamic application, which means it can add whitespace gaps to each character on-the-fly as you input text.
|
||||||
|
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
[Click here](https://betterandroid.github.io/PanguText/en) go to the documentation page for more detailed tutorials and content.
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
<!--suppress HtmlDeprecatedAttribute -->
|
||||||
|
<div align="center">
|
||||||
|
<h2>Hey, please stay! 👋</h2>
|
||||||
|
<h3>Here are related projects such as Android development tools, UI design, Gradle plugins, Xposed Modules and practical software. </h3>
|
||||||
|
<h3>If the project below can help you, please give me a star! </h3>
|
||||||
|
<h3>All projects are free, open source, and follow the corresponding open source license agreement. </h3>
|
||||||
|
<h1><a href="https://github.com/fankes/fankes/blob/main/project-promote/README.md">→ To see more about my projects, please click here ←</a></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
- [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Copyright (C) 2019 HighCapable
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
```
|
||||||
|
|
||||||
|
Copyright © 2019 HighCapable
|
64
build.gradle.kts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
|
||||||
|
import com.vanniktech.maven.publish.MavenPublishBaseExtension
|
||||||
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
autowire(libs.plugins.android.application) apply false
|
||||||
|
autowire(libs.plugins.android.library) apply false
|
||||||
|
autowire(libs.plugins.kotlin.android) apply false
|
||||||
|
autowire(libs.plugins.kotlin.dokka) apply false
|
||||||
|
autowire(libs.plugins.maven.publish) apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryProjects {
|
||||||
|
afterEvaluate {
|
||||||
|
configure<PublishingExtension> {
|
||||||
|
repositories {
|
||||||
|
val repositoryDir = gradle.gradleUserHomeDir
|
||||||
|
.resolve("highcapable-maven-repository")
|
||||||
|
.resolve("repository")
|
||||||
|
maven {
|
||||||
|
name = "HighCapableMavenReleases"
|
||||||
|
url = repositoryDir.resolve("releases").toURI()
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "HighCapableMavenSnapShots"
|
||||||
|
url = repositoryDir.resolve("snapshots").toURI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configure<MavenPublishBaseExtension> {
|
||||||
|
configure(AndroidSingleVariantLibrary(publishJavadocJar = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
val configuration = """{ "footerMessage": "PanguText | Apache-2.0 License | Copyright (C) 2019 HighCapable" }"""
|
||||||
|
pluginsMapConfiguration.set(mapOf("org.jetbrains.dokka.base.DokkaBase" to configuration))
|
||||||
|
}
|
||||||
|
tasks.register("publishKDoc") {
|
||||||
|
group = "documentation"
|
||||||
|
dependsOn("dokkaHtml")
|
||||||
|
doLast {
|
||||||
|
val docsDir = rootProject.projectDir
|
||||||
|
.resolve("docs-source")
|
||||||
|
.resolve("dist")
|
||||||
|
.resolve("KDoc")
|
||||||
|
.resolve(project.name)
|
||||||
|
if (docsDir.exists()) docsDir.deleteRecursively() else docsDir.mkdirs()
|
||||||
|
layout.buildDirectory.dir("dokka/html").get().asFile.copyRecursively(docsDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun libraryProjects(action: Action<in Project>) {
|
||||||
|
val libraries = listOf(
|
||||||
|
Libraries.PANGUTEXT_ANDROID,
|
||||||
|
Libraries.PANGUTEXT_COMPOSE
|
||||||
|
)
|
||||||
|
allprojects { if (libraries.contains(name)) action.execute(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
object Libraries {
|
||||||
|
const val PANGUTEXT_ANDROID = "pangutext-android"
|
||||||
|
const val PANGUTEXT_COMPOSE = "pangutext-compose"
|
||||||
|
}
|
54
demo-android/build.gradle.kts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
plugins {
|
||||||
|
autowire(libs.plugins.android.application)
|
||||||
|
autowire(libs.plugins.kotlin.android)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = property.project.app.packageName
|
||||||
|
compileSdk = property.project.android.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = property.project.app.packageName
|
||||||
|
minSdk = property.project.android.minSdk
|
||||||
|
targetSdk = property.project.android.targetSdk
|
||||||
|
versionName = property.project.app.versionName
|
||||||
|
versionCode = property.project.app.versionCode
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
"-Xno-param-assertions",
|
||||||
|
"-Xno-call-assertions",
|
||||||
|
"-Xno-receiver-assertions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.pangutextAndroid)
|
||||||
|
implementation(com.highcapable.betterandroid.ui.component)
|
||||||
|
implementation(com.highcapable.betterandroid.ui.extension)
|
||||||
|
implementation(com.highcapable.betterandroid.system.extension)
|
||||||
|
implementation(androidx.core.core.ktx)
|
||||||
|
implementation(androidx.appcompat.appcompat)
|
||||||
|
implementation(com.google.android.material.material)
|
||||||
|
implementation(androidx.constraintlayout.constraintlayout)
|
||||||
|
testImplementation(junit.junit)
|
||||||
|
androidTestImplementation(androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(androidx.test.espresso.espresso.core)
|
||||||
|
}
|
32
demo-android/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.kts.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
|
public static *** throwUninitializedProperty(...);
|
||||||
|
public static *** throwUninitializedPropertyAccessException(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep class * extends android.app.Activity
|
||||||
|
-keep class * implements androidx.viewbinding.ViewBinding {
|
||||||
|
<init>();
|
||||||
|
*** inflate(android.view.LayoutInflater);
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.highcapable.pangutext.demo
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.highcapable.pangutext", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
29
demo-android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.DefaultAppTheme"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ListActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/2/9.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.demo.ui
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.highcapable.betterandroid.ui.component.adapter.factory.bindAdapter
|
||||||
|
import com.highcapable.betterandroid.ui.extension.view.textColor
|
||||||
|
import com.highcapable.pangutext.demo.databinding.ActivityListBinding
|
||||||
|
import com.highcapable.pangutext.demo.databinding.AdapterListBinding
|
||||||
|
import com.highcapable.pangutext.demo.ui.base.BaseActivity
|
||||||
|
|
||||||
|
class ListActivity : BaseActivity<ActivityListBinding>() {
|
||||||
|
|
||||||
|
private val listData = List(100) { "这是第${it}条Data演示" }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding.recyclerView.bindAdapter<String> {
|
||||||
|
onBindData { listData }
|
||||||
|
onBindViews<AdapterListBinding> { binding, text, _ ->
|
||||||
|
binding.text.text = text
|
||||||
|
binding.text.textColor = Color.rgb(
|
||||||
|
(0..255).random(),
|
||||||
|
(0..255).random(),
|
||||||
|
(0..255).random()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/12.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.demo.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import com.highcapable.betterandroid.ui.component.insets.factory.handleOnWindowInsetsChanged
|
||||||
|
import com.highcapable.betterandroid.ui.component.insets.factory.setInsetsPadding
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.startActivity
|
||||||
|
import com.highcapable.pangutext.demo.databinding.ActivityMainBinding
|
||||||
|
import com.highcapable.pangutext.demo.ui.base.BaseActivity
|
||||||
|
|
||||||
|
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||||
|
|
||||||
|
private val demoText = HtmlCompat.fromHtml(
|
||||||
|
"今天下午,我去了一家新开的咖啡店,店里环境非常舒适,感觉很cozy。" +
|
||||||
|
"我点了一杯latte,坐在窗边,透过玻璃看着街上的人来人往。店员还<b>特别热情</b>," +
|
||||||
|
"给我推荐了一款很<font color='#639F70'><b>特别的</b></font>chocolate cake,味道真是不错!<br/>" +
|
||||||
|
"我发现现在很多人都<b>喜欢在咖啡店里工作</b>,几乎每桌都有laptop。我的旁边有一位女士,正在忙着处理emails。" +
|
||||||
|
"我想,这样的环境真是适合集中精力工作。<br/>" +
|
||||||
|
"总的来说,今天的体验很不错,下次还想再来尝试其他的<font color='#5C80BC'>drinks</font>和<font color='#9C528B'>desserts</font>。<br/>" +
|
||||||
|
"<span style='background-color: #E9EDDE'>混<b>合wo</b>rd样式</span>测试。<br/>" +
|
||||||
|
"You can<a href='https://github.com/BetterAndroid/PanguText'>点击这里访问项目地址</a>。",
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding.root.handleOnWindowInsetsChanged(animated = true) { linearLayout, insetsWrapper ->
|
||||||
|
linearLayout.setInsetsPadding(insetsWrapper.safeDrawing)
|
||||||
|
}
|
||||||
|
listOf(
|
||||||
|
binding.textViewPanguText,
|
||||||
|
binding.textViewPanguTextCjkSpacingRatio,
|
||||||
|
binding.textViewNoPanguText
|
||||||
|
).forEach {
|
||||||
|
it.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
it.text = demoText
|
||||||
|
}
|
||||||
|
binding.buttonJumpList.setOnClickListener {
|
||||||
|
startActivity<ListActivity>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/2/10.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.demo.ui.base
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.highcapable.betterandroid.ui.component.activity.AppBindingActivity
|
||||||
|
import com.highcapable.pangutext.android.factory.PanguTextFactory2
|
||||||
|
|
||||||
|
open class BaseActivity<VB : ViewBinding> : AppBindingActivity<VB>() {
|
||||||
|
|
||||||
|
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||||
|
val inflater = super.onPrepareContentView(savedInstanceState)
|
||||||
|
PanguTextFactory2.inject(inflater)
|
||||||
|
return inflater
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.4686"
|
||||||
|
android:scaleY="0.4686"
|
||||||
|
android:translateX="12.7536"
|
||||||
|
android:translateY="12.4412">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M43.901,36H4.099C5.102,25.893 13.629,18 24,18C34.371,18 42.898,25.893 43.901,36Z"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
<path
|
||||||
|
android:pathData="M14,20L10,13"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
<path
|
||||||
|
android:pathData="M33,20L37,13"
|
||||||
|
android:strokeWidth="4"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
21
demo-android/src/main/res/layout/activity_list.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?colorBackgroundPrimary"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".ui.MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</LinearLayout>
|
168
demo-android/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?colorBackgroundPrimary"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".ui.MainActivity"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fadingEdgeLength="10dp"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:requiresFadingEdge="vertical"
|
||||||
|
android:scrollbars="none">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:paddingBottom="15dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Xiaoming今年16岁"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:panguText_enabled="false" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="Xiaoming今年16岁"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="第1个Radio Button" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:text="第2个Radio Button" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="这是一个Check Box组件" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="这是一个Switch组件" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Input something"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_jump_list"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="启动List演示" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:text="中英混排演示"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:text="Mixed Chinese and English Demo"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:text="包含 PanguText (With PanguText)"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_pangu_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:lineSpacingExtra="5dp"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:text="3.5f Spacing Ratio PanguText"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_pangu_text_cjk_spacing_ratio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:lineSpacingExtra="5dp"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:panguText_cjkSpacingRatio="3.5" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:text="不包含 PanguText (Without PanguText)"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_no_pangu_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:lineSpacingExtra="5dp"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:panguText_enabled="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
</LinearLayout>
|
12
demo-android/src/main/res/layout/adapter_list.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="20dp" />
|
||||||
|
</LinearLayout>
|
BIN
demo-android/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 31 KiB |
11
demo-android/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.DefaultAppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your dark theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||||
|
<item name="colorPrimary">@color/theme</item>
|
||||||
|
<item name="colorAccent">@color/theme</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<item name="colorBackgroundPrimary">@color/background_night</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
4
demo-android/src/main/res/values-zh-rCN/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Pangu Text 演示</string>
|
||||||
|
</resources>
|
8
demo-android/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="theme">#FF639F70</color>
|
||||||
|
<color name="background_day">#FFF5F5F5</color>
|
||||||
|
<color name="background_night">#FF2D2D2D</color>
|
||||||
|
</resources>
|
3
demo-android/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Pangu Text Demo</string>
|
||||||
|
</resources>
|
6
demo-android/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="Colors">
|
||||||
|
<attr name="colorBackgroundPrimary" format="color" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
13
demo-android/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.DefaultAppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your light theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||||
|
<item name="colorPrimary">@color/theme</item>
|
||||||
|
<item name="colorAccent">@color/theme</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<item name="colorBackgroundPrimary">@color/background_day</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.DefaultAppTheme" parent="Base.Theme.DefaultAppTheme" />
|
||||||
|
</resources>
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.highcapable.pangutext.demo
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
4
docs-source/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/node_modules
|
||||||
|
/src/.vuepress/.cache
|
||||||
|
/src/.vuepress/.temp
|
||||||
|
/dist
|
3
docs-source/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"git.ignoreLimitWarning": true
|
||||||
|
}
|
4
docs-source/build-dokka.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
cd ..
|
||||||
|
./gradlew :pangutext-android:publishKDoc
|
||||||
|
# TODO: When the pangutext-compose library is done.
|
||||||
|
# :pangutext-compose:publishKDoc
|
17
docs-source/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "pangutext_docs",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@mr-hope/vuepress-plugin-copy-code": "^1.30.0",
|
||||||
|
"@vuepress/plugin-prismjs": "2.0.0-rc.0",
|
||||||
|
"@vuepress/plugin-search": "2.0.0-rc.0",
|
||||||
|
"@vuepress/plugin-shiki": "2.0.0-rc.0",
|
||||||
|
"vuepress": "2.0.0-rc.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"docs:dev": "vuepress dev src",
|
||||||
|
"docs:build": "vuepress build src",
|
||||||
|
"docs:build-gh-pages": "vuepress build src && touch dist/.nojekyll && sh build-dokka.sh"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
64
docs-source/src/.vuepress/config.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { defaultTheme } from 'vuepress';
|
||||||
|
import { shikiPlugin } from '@vuepress/plugin-shiki';
|
||||||
|
import { searchPlugin } from '@vuepress/plugin-search';
|
||||||
|
import { navBarItems, sideBarItems, configs, pageLinkRefs } from './configs/template';
|
||||||
|
import { env, markdown } from './configs/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dest: configs.dev.dest,
|
||||||
|
port: configs.dev.port,
|
||||||
|
base: configs.website.base,
|
||||||
|
head: [['link', { rel: 'icon', href: configs.website.icon }]],
|
||||||
|
title: configs.website.title,
|
||||||
|
description: configs.website.locales['/en/'].description,
|
||||||
|
locales: configs.website.locales,
|
||||||
|
theme: defaultTheme({
|
||||||
|
logo: configs.website.logo,
|
||||||
|
repo: configs.github.repo,
|
||||||
|
docsRepo: configs.github.repo,
|
||||||
|
docsBranch: configs.github.branch,
|
||||||
|
docsDir: configs.github.dir,
|
||||||
|
editLinkPattern: ':repo/edit/:branch/:path',
|
||||||
|
sidebar: sideBarItems,
|
||||||
|
sidebarDepth: 2,
|
||||||
|
locales: {
|
||||||
|
'/en/': {
|
||||||
|
navbar: navBarItems['/en/'],
|
||||||
|
selectLanguageText: 'English (US)',
|
||||||
|
selectLanguageName: 'English',
|
||||||
|
editLinkText: 'Edit this page on GitHub',
|
||||||
|
tip: 'Tips',
|
||||||
|
warning: 'Notice',
|
||||||
|
danger: 'Pay Attention',
|
||||||
|
},
|
||||||
|
'/zh-cn/': {
|
||||||
|
navbar: navBarItems['/zh-cn/'],
|
||||||
|
selectLanguageText: '简体中文 (CN)',
|
||||||
|
selectLanguageName: '简体中文',
|
||||||
|
editLinkText: '在 GitHub 上编辑此页',
|
||||||
|
notFound: ['这里什么都没有', '我们怎么到这来了?', '这是一个 404 页面', '看起来我们进入了错误的链接'],
|
||||||
|
backToHome: '回到首页',
|
||||||
|
contributorsText: '贡献者',
|
||||||
|
lastUpdatedText: '上次更新',
|
||||||
|
tip: '小提示',
|
||||||
|
warning: '注意',
|
||||||
|
danger: '特别注意',
|
||||||
|
openInNewWindow: '在新窗口中打开',
|
||||||
|
toggleColorMode: '切换颜色模式'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
extendsMarkdown: (md: markdownit) => {
|
||||||
|
markdown.injectLinks(md, env.dev ? pageLinkRefs.dev : pageLinkRefs.prod);
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
shikiPlugin({ theme: 'github-dark-dimmed' }),
|
||||||
|
searchPlugin({
|
||||||
|
isSearchable: (page) => page.path !== '/',
|
||||||
|
locales: {
|
||||||
|
'/en/': { placeholder: 'Search' },
|
||||||
|
'/zh-cn/': { placeholder: '搜索' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
146
docs-source/src/.vuepress/configs/template.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { i18n } from './utils';
|
||||||
|
|
||||||
|
interface PageLinkRefs {
|
||||||
|
dev: Record<string, string>[];
|
||||||
|
prod: Record<string, string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationLinks = {
|
||||||
|
start: [
|
||||||
|
'/guide/home',
|
||||||
|
'/guide/quick-start'
|
||||||
|
],
|
||||||
|
library: [
|
||||||
|
'/library/android',
|
||||||
|
'/library/compose'
|
||||||
|
],
|
||||||
|
config: [
|
||||||
|
'/config/r8-proguard'
|
||||||
|
],
|
||||||
|
about: [
|
||||||
|
'/about/changelog',
|
||||||
|
'/about/future',
|
||||||
|
'/about/contacts',
|
||||||
|
'/about/about'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const configs = {
|
||||||
|
dev: {
|
||||||
|
dest: 'dist',
|
||||||
|
port: 9000
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
base: '/PanguText/',
|
||||||
|
icon: '/images/logo.png',
|
||||||
|
logo: '/images/logo.png',
|
||||||
|
title: 'Pangu Text',
|
||||||
|
locales: {
|
||||||
|
'/en/': {
|
||||||
|
lang: 'en-US',
|
||||||
|
description: 'A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits'
|
||||||
|
},
|
||||||
|
'/zh-cn/': {
|
||||||
|
lang: 'zh-CN',
|
||||||
|
description: '一个中日韩 (CJK) 与英文单词、半角数字排版的解决方案'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
repo: 'https://github.com/BetterAndroid/PanguText',
|
||||||
|
page: 'https://betterandroid.github.io/PanguText',
|
||||||
|
branch: 'main',
|
||||||
|
dir: 'docs-source/src'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pageLinkRefs: PageLinkRefs = {
|
||||||
|
dev: [
|
||||||
|
{ 'repo://': `${configs.github.repo}/` },
|
||||||
|
// KDoc URL for local debugging, non-fixed value, adjust according to your own needs.
|
||||||
|
// You can run ./build-dokka.sh and start the local server in dist/KDoc.
|
||||||
|
{ 'kdoc://': 'http://localhost:9001/' }
|
||||||
|
],
|
||||||
|
prod: [
|
||||||
|
{ 'repo://': `${configs.github.repo}/` },
|
||||||
|
{ 'kdoc://': `${configs.github.page}/KDoc/` }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navBarItems = {
|
||||||
|
'/en/': [{
|
||||||
|
text: 'Navigation',
|
||||||
|
children: [{
|
||||||
|
text: 'Get Started',
|
||||||
|
children: i18n.array(navigationLinks.start, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'Libraries',
|
||||||
|
children: i18n.array(navigationLinks.library, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'Configs',
|
||||||
|
children: i18n.array(navigationLinks.config, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'About',
|
||||||
|
children: i18n.array(navigationLinks.about, 'en')
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
text: 'Contact Us',
|
||||||
|
link: i18n.string(navigationLinks.about[2], 'en')
|
||||||
|
}],
|
||||||
|
'/zh-cn/': [{
|
||||||
|
text: '导航',
|
||||||
|
children: [{
|
||||||
|
text: '入门',
|
||||||
|
children: i18n.array(navigationLinks.start, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '依赖',
|
||||||
|
children: i18n.array(navigationLinks.library, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '配置',
|
||||||
|
children: i18n.array(navigationLinks.config, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '关于',
|
||||||
|
children: i18n.array(navigationLinks.about, 'zh-cn')
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
text: '联系我们',
|
||||||
|
link: i18n.string(navigationLinks.about[2], 'zh-cn')
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sideBarItems = {
|
||||||
|
'/en/': [{
|
||||||
|
text: 'Get Started',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.start, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'Libraries',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.library, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'Configs',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.config, 'en')
|
||||||
|
}, {
|
||||||
|
text: 'About',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.about, 'en')
|
||||||
|
}],
|
||||||
|
'/zh-cn/': [{
|
||||||
|
text: '入门',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.start, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '依赖',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.library, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '配置',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.config, 'zh-cn')
|
||||||
|
}, {
|
||||||
|
text: '关于',
|
||||||
|
collapsible: true,
|
||||||
|
children: i18n.array(navigationLinks.about, 'zh-cn')
|
||||||
|
}]
|
||||||
|
};
|
39
docs-source/src/.vuepress/configs/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export const env = {
|
||||||
|
dev: process.env.NODE_ENV === 'development'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const i18n = {
|
||||||
|
space: ' ',
|
||||||
|
string: (content: string, locale: string) => {
|
||||||
|
return '/' + locale + content;
|
||||||
|
},
|
||||||
|
array: (contents: string[], locale: string) => {
|
||||||
|
const newContents: string[] = [];
|
||||||
|
contents.forEach((content) => {
|
||||||
|
newContents.push(i18n.string(content, locale));
|
||||||
|
});
|
||||||
|
return newContents;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markdown = {
|
||||||
|
injectLinks: (md: markdownit, maps: Record<string, string>[]) => {
|
||||||
|
const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, _env, self) {
|
||||||
|
return self.renderToken(tokens, idx, options);
|
||||||
|
};
|
||||||
|
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||||
|
const hrefIndex = tokens[idx].attrIndex('href');
|
||||||
|
let current = tokens[idx].attrs!![hrefIndex][1];
|
||||||
|
for (const map of maps) {
|
||||||
|
for (const [search, replace] of Object.entries(map)) {
|
||||||
|
if (current.startsWith(search)) {
|
||||||
|
current = current.replace(search, replace);
|
||||||
|
tokens[idx].attrs!![hrefIndex][1] = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultRender(tokens, idx, options, env, self);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
BIN
docs-source/src/.vuepress/public/images/demo_01.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs-source/src/.vuepress/public/images/demo_02.gif
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
docs-source/src/.vuepress/public/images/demo_02.mov
Normal file
BIN
docs-source/src/.vuepress/public/images/logo.png
Normal file
After Width: | Height: | Size: 31 KiB |
179
docs-source/src/.vuepress/styles/index.scss
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
$primary-color: rgb(99, 159, 112);
|
||||||
|
$accent-color: rgb(130, 180, 140);
|
||||||
|
$content-width: 965px;
|
||||||
|
$scroll-bar-width: 8px;
|
||||||
|
$scroll-bar-height: 6.5px;
|
||||||
|
$scroll-bar-border-radius: 50px;
|
||||||
|
$scroll-bar-track-color-code: rgb(86, 96, 110);
|
||||||
|
$scroll-bar-thumb-hover-color-code: rgb(121, 135, 155);
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--c-brand: #{$primary-color};
|
||||||
|
--c-brand-light: #{$accent-color};
|
||||||
|
--content-width: #{$content-width};
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 3px 5px 3px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-container {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-text {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #{$scroll-bar-track-color-code};
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #{$scroll-bar-thumb-hover-color-code};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-kotlin {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #{$scroll-bar-track-color-code};
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #{$scroll-bar-thumb-hover-color-code};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-java {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #{$scroll-bar-track-color-code};
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #{$scroll-bar-thumb-hover-color-code};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-groovy {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #{$scroll-bar-track-color-code};
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #{$scroll-bar-thumb-hover-color-code};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-xml {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #{$scroll-bar-track-color-code};
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #{$scroll-bar-thumb-hover-color-code};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-anchor-page {
|
||||||
|
h6 {
|
||||||
|
color: transparent;
|
||||||
|
margin-bottom: -35px;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-page {
|
||||||
|
h1 {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 18pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 15pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 9.6pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 8.4pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
color: rgb(142, 155, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deprecated {
|
||||||
|
color: rgb(142, 155, 168);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: #{$scroll-bar-width};
|
||||||
|
height: #{$scroll-bar-height};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgb(234, 236, 239);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(189, 189, 189);
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(133, 133, 133);
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--c-brand: #{$primary-color};
|
||||||
|
--c-brand-light: #{$accent-color};
|
||||||
|
--content-width: #{$content-width};
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: #{$scroll-bar-width};
|
||||||
|
height: #{$scroll-bar-height};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgb(41, 46, 53);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(65, 72, 83);
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(56, 62, 72);
|
||||||
|
border-radius: #{$scroll-bar-border-radius};
|
||||||
|
}
|
||||||
|
}
|
27
docs-source/src/en/about/about.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# About This Document
|
||||||
|
|
||||||
|
> This document is powered by [VuePress](https://v2.vuepress.vuejs.org/en).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Apache-2.0](https://github.com/HighCapable/YukiReflection/blob/master/LICENSE)
|
||||||
|
|
||||||
|
```:no-line-numbers
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Copyright (C) 2019 HighCapable
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
```
|
||||||
|
|
||||||
|
Copyright © 2019 HighCapable
|
27
docs-source/src/en/about/changelog.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
> The version update history of `PanguText` is recorded here.
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
|
||||||
|
We will only maintain the latest API version, if you are using an outdate API version, you voluntarily renounce any possibility of maintenance.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
To avoid translation time consumption, Changelog will use **Google Translation** from **Chinese** to **English**, please refer to the original text for actual reference.
|
||||||
|
|
||||||
|
Time zone of version release date: **UTC+8**
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## pangutext-android
|
||||||
|
|
||||||
|
### 1.0.0 | 2025.02.10  <Badge type="tip" text="latest" vertical="middle" />
|
||||||
|
|
||||||
|
- The first version is submitted to Maven
|
||||||
|
|
||||||
|
## pangutext-compose
|
||||||
|
|
||||||
|
Not yet released.
|
16
docs-source/src/en/about/contacts.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Contact Us
|
||||||
|
|
||||||
|
> If you have any questions in use, or have any constructive suggestions, you can contact us.
|
||||||
|
|
||||||
|
Join our developers group.
|
||||||
|
|
||||||
|
- [Click to join Telegram group](https://t.me/BetterAndroid)
|
||||||
|
- [Click to join Telegram group (Developer)](https://t.me/HighCapable_Dev)
|
||||||
|
|
||||||
|
Find me on **Twitter** [@fankesyooni](https://twitter.com/fankesyooni).
|
||||||
|
|
||||||
|
## Help with Maintenance
|
||||||
|
|
||||||
|
Thank you for choosing and using `PanguText`.
|
||||||
|
|
||||||
|
If you have code-related suggestions and requests, you can submit a Pull Request on GitHub.
|
15
docs-source/src/en/about/future.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Looking for Future
|
||||||
|
|
||||||
|
> The future is bright and uncertain, let us look forward to the future development space of `PanguText`.
|
||||||
|
|
||||||
|
## Future Plans
|
||||||
|
|
||||||
|
> Features that `PanguText` may add later are included here.
|
||||||
|
|
||||||
|
### Limitations of SpannableString
|
||||||
|
|
||||||
|
`PanguText`'s main functionality on the Android platform currently comes from `SpannableString`, which has not yet fully resolved the issues of handling complex text styles and performance overhead.
|
||||||
|
|
||||||
|
### Jetpack Compose Plan
|
||||||
|
|
||||||
|
`PanguText` will support Jetpack Compose in the future and plans to use `AnnotatedString` as the main text processing method to minimize intrusion into the underlying layer.
|
6
docs-source/src/en/config/r8-proguard.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# R8 & Proguard Obfuscate
|
||||||
|
|
||||||
|
> In most scenarios, the app packages can be compressed through obfuscation,
|
||||||
|
> here is an introduction to how to configure obfuscation rules.
|
||||||
|
|
||||||
|
`PanguText` does not require any additional obfuscation rules.
|
70
docs-source/src/en/guide/home.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Introduce
|
||||||
|
|
||||||
|
> `PanguText` is a solution for CJK (Chinese, Japanese, Korean) and English word, half-width number spacing.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
This project was created because, until now, there hasn’t been a public solution to perfectly address the typography issues between Chinese, Japanese, Korean, and English.
|
||||||
|
Typically, when mixing CJK (i.e. Chinese, Japanese, Korean) with English, aesthetic issues can arise—a historical legacy stemming from the differences in writing conventions between full-width and half-width characters. Although the W3C has now established CJK typography guidelines, only a few individuals or companies willing to adhere to these standards have adopted this approach.
|
||||||
|
|
||||||
|
Currently, the known vendor solutions are as follows:
|
||||||
|
|
||||||
|
- Apple platforms (iOS, iPadOS, macOS, tvOS, watchOS) text typography solutions
|
||||||
|
- Xiaomi’s (HyperOS) text typography optimization
|
||||||
|
- OrginOS’s font-based text typography optimization
|
||||||
|
|
||||||
|
However, these solutions are closed and cannot be implemented on other platforms.
|
||||||
|
We aim to provide an open-source solution adaptable to various scenarios, featuring low intrusiveness and easy integration, allowing more developers to effectively address text typography issues.
|
||||||
|
|
||||||
|
The primary inspiration for this project comes from [pangu.js](https://github.com/vinta/pangu.js), which offers a set of regular expressions for CJK typography.
|
||||||
|
We have optimized these solutions to format text across platforms without inserting extra space characters. We extend this approach further to explore additional possibilities.
|
||||||
|
|
||||||
|
Heartfelt thanks to the original developer of **pangu.js** for providing the foundational solution.
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
As you can see, the typography scheme of `PanguText` does not work by simply inserting spaces between CJK characters and English words.
|
||||||
|
Instead, it leverages each platform's native handling to automatically add whitespace between these characters, ensuring minimal intrusion.
|
||||||
|
|
||||||
|
> Before Applying (Top) vs. After Applying (Bottom)
|
||||||
|
|
||||||
|
<img src="/images/demo_01.png" width="300" />
|
||||||
|
|
||||||
|
> Dynamic Application
|
||||||
|
|
||||||
|
<img src="/images/demo_02.gif" width="480" />
|
||||||
|
|
||||||
|
`PanguText` supports dynamic application, which means it can add whitespace gaps to each character on-the-fly as you input text.
|
||||||
|
|
||||||
|
::: tip Developer's Perspective
|
||||||
|
|
||||||
|
I personally do not recommend manually inserting spaces between CJK and English characters for typographic refinement if your software or system natively supports enhanced typographic formatting.
|
||||||
|
|
||||||
|
The spacing can vary across fonts, which may lead to formatting issues and the insertion of undesired space characters.
|
||||||
|
|
||||||
|
In certain contexts, such as URLs, filenames, or hashtags containing “#”, these spaces are not acceptable.
|
||||||
|
|
||||||
|
However, in special scenarios—for example, within code comments or documentation—it can be beneficial to add spaces, as these areas typically do not employ automated formatting tools.
|
||||||
|
|
||||||
|
Another point to consider is the use of different punctuation marks in different languages.
|
||||||
|
Avoid mixing full-width and half-width punctuation marks.
|
||||||
|
If you must use half-width punctuation marks to annotate full-width text, ensure that the half-width marks are followed by a space to complete the character space (the same applies to English).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Language Requirement
|
||||||
|
|
||||||
|
It is recommended to use Kotlin as the preferred development language.
|
||||||
|
|
||||||
|
This project is entirely written in Kotlin and is compatible with Java in some parts, but it may not be fully compatible.
|
||||||
|
|
||||||
|
All demo & sample codes in the document will be described using Kotlin, if you don’t know how to use Kotlin at all, you may not get the best experience.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
The maintenance of this project is inseparable from the support and contributions of all developers.
|
||||||
|
|
||||||
|
This project is currently in its early stages, and there may still be some problems or lack of functions you need.
|
||||||
|
|
||||||
|
If possible, feel free to submit a PR to contribute features you think are needed to this project or goto [GitHub Issues](repo://issues)
|
||||||
|
to make suggestions to us.
|
86
docs-source/src/en/guide/quick-start.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
> Integrate `PanguText` into your project.
|
||||||
|
|
||||||
|
## Project Requirements
|
||||||
|
|
||||||
|
The project needs to be created using `Android Studio` or `IntelliJ IDEA` and be of type Android or Kotlin Multiplatform
|
||||||
|
project and have integrated Kotlin environment dependencies.
|
||||||
|
|
||||||
|
- Android Studio (It is recommended to get the latest version [from here](https://developer.android.com/studio))
|
||||||
|
|
||||||
|
- IntelliJ IDEA (It is recommended to get the latest version [from here](https://www.jetbrains.com/idea))
|
||||||
|
|
||||||
|
- Kotlin 1.9.0+, Gradle 8+, Java 17+, Android Gradle Plugin 8+
|
||||||
|
|
||||||
|
### Configure Repositories
|
||||||
|
|
||||||
|
The dependencies of `PanguText` are published in **Maven Central** and our public repository,
|
||||||
|
you can use the following method to configure repositories.
|
||||||
|
|
||||||
|
We recommend using Kotlin DSL as the Gradle build script language and [SweetDependency](https://github.com/HighCapable/SweetDependency)
|
||||||
|
to manage dependencies.
|
||||||
|
|
||||||
|
#### SweetDependency (Recommended)
|
||||||
|
|
||||||
|
Configure repositories in your project's `SweetDependency` configuration file.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
repositories:
|
||||||
|
google:
|
||||||
|
maven-central:
|
||||||
|
# (Optional) You can add this URL to use our public repository
|
||||||
|
# When Sonatype-OSS fails and cannot publish dependencies, this repository is added as a backup
|
||||||
|
# For details, please visit: https://github.com/HighCapable/maven-repository
|
||||||
|
highcapable-maven-releases:
|
||||||
|
url: https://raw.githubusercontent.com/HighCapable/maven-repository/main/repository/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Traditional Method
|
||||||
|
|
||||||
|
Configure repositories in your project `build.gradle.kts`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
// (Optional) You can add this URL to use our public repository
|
||||||
|
// When Sonatype-OSS fails and cannot publish dependencies, this repository is added as a backup
|
||||||
|
// For details, please visit: https://github.com/HighCapable/maven-repository
|
||||||
|
maven("https://raw.githubusercontent.com/HighCapable/maven-repository/main/repository/releases")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure Java Version
|
||||||
|
|
||||||
|
Modify the Java version of Kotlin in your project `build.gradle.kts` to 17 or above.
|
||||||
|
|
||||||
|
> Kotlin DSL
|
||||||
|
|
||||||
|
```kt
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Functional Overview
|
||||||
|
|
||||||
|
The project is divided into multiple modules: Android platform and Jetpack Compose (multiplatform). You can choose the module you wish to include as a dependency in your project.
|
||||||
|
|
||||||
|
Click the corresponding module below to view detailed feature descriptions.
|
||||||
|
|
||||||
|
- [Android](../library/android.md)
|
||||||
|
- [Jetpack Compose](../library/compose.md)
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
You can find some examples below. Check out the corresponding demo projects to get a better understanding of how these features work and quickly select the functionality you need.
|
||||||
|
|
||||||
|
- [Android](repo://tree/main/demo-android)
|
||||||
|
- [Jetpack Compose (Coming soon)](repo://tree/main/demo-compose)
|
13
docs-source/src/en/index.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
title: Home
|
||||||
|
heroImage: /images/logo.png
|
||||||
|
actions:
|
||||||
|
- text: Get Started
|
||||||
|
link: /en/guide/home
|
||||||
|
type: primary
|
||||||
|
- text: Changelog
|
||||||
|
link: /en/about/changelog
|
||||||
|
type: secondary
|
||||||
|
footer: Apache-2.0 License | Copyright (C) 2019 HighCapable
|
||||||
|
---
|
367
docs-source/src/en/library/android.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# Android
|
||||||
|
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
|
||||||
|
This is the core dependency for the Android platform. When using `PanguText` on Android, you need to include this module.
|
||||||
|
|
||||||
|
## Configure Dependency
|
||||||
|
|
||||||
|
You can add this module to your project using the following method.
|
||||||
|
|
||||||
|
### SweetDependency (Recommended)
|
||||||
|
|
||||||
|
Add dependency in your project's `SweetDependency` configuration file.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
libraries:
|
||||||
|
com.highcapable.pangutext:
|
||||||
|
pangutext-android:
|
||||||
|
version: +
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure dependency in your project `build.gradle.kts`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
implementation(com.highcapable.pangutext.pangutext.android)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traditional Method
|
||||||
|
|
||||||
|
Configure dependency in your project `build.gradle.kts`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
implementation("com.highcapable.pangutext:pangutext-android:<version>")
|
||||||
|
```
|
||||||
|
|
||||||
|
Please change `<version>` to the version displayed at the top of this document.
|
||||||
|
|
||||||
|
## Function Introduction
|
||||||
|
|
||||||
|
You can view the KDoc [click here](kdoc://pangutext-android).
|
||||||
|
|
||||||
|
### Implementation Principle
|
||||||
|
|
||||||
|
`PanguText` provides two methods for text formatting on the Android platform: `SpannableString` (does not alter the original text length) and direct insertion of whitespace characters (alters the original text length).
|
||||||
|
|
||||||
|
The first method, `SpannableString`, adds a `Span` with spacing to the character before the one that needs spacing, changing the text style without altering the string content. The rendering is done by the `TextView` layer (or manually using `TextPaint` based on `Spanned` for layout styling), achieving non-intrusive text styling.
|
||||||
|
|
||||||
|
This method also supports processing already styled text (`Spanned`), such as text created via `Html.fromHtml`.
|
||||||
|
|
||||||
|
**However, it is currently experimental and may still have unexpected style errors**. You can refer to the [Personalized Configuration](#personalized-configuration) section below to disable it.
|
||||||
|
|
||||||
|
The dynamic application (injection) feature mainly targets the input state of `EditText`. It sets a custom `TextWatcher` for `EditText` to monitor input changes and formats the text from `afterTextChanged`.
|
||||||
|
|
||||||
|
The second method directly inserts whitespace characters after the characters that need spacing. This method alters the original text length and content but does not rely on the `TextView` layer for rendering. It uses `TextPaint` to draw the text directly, suitable for all scenarios, **but does not support dynamic application (injection)**.
|
||||||
|
|
||||||
|
::: warning Unresolved Issues
|
||||||
|
|
||||||
|
`PanguText` may conflict with Material components like `TextInputEditText`, `MaterialAutoCompleteTextView`, and `TextInputLayout` when using `setHint`, as `TextView` does not account for `Span` during measurement. This issue is particularly noticeable in single-line text, and there is no solution yet. Use these components cautiously.
|
||||||
|
|
||||||
|
Due to the above issue, calculating the width of a `TextView` with `PanguText` style using the `View.measure` method may also result in errors.
|
||||||
|
|
||||||
|
`PanguText` currently cannot handle continuous characters like underlines or strikethroughs in `Spanned` text, as the lines will break after adding spacing. It may also cause style errors or fail to apply styles correctly to some special characters. For stability, avoid enabling `PanguText` for very complex rich text or refer to the [Personalized Configuration](#personalized-configuration) section to set `excludePatterns`.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Integrate into Existing Projects
|
||||||
|
|
||||||
|
Integrating `PanguText` into your current project is very easy. You don't need to change much code. Choose your preferred method below to complete the integration.
|
||||||
|
|
||||||
|
#### Inject to LayoutInflater
|
||||||
|
|
||||||
|
`PanguText` supports direct injection of `LayoutInflater.Factory2` or creating a `LayoutInflater.Factory2` instance for the current `Activity` to take over the entire view. This is the recommended integration method, as it allows for non-intrusive and quick integration without modifying any existing layouts.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// Inject here.
|
||||||
|
PanguTextFactory2.inject(this)
|
||||||
|
setContentView(binding.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
Since `LayoutInflater.Factory2` is taken over, recycled layouts like `ListView` and `RecyclerView` can also be correctly taken over.
|
||||||
|
|
||||||
|
After injecting the `LayoutInflater` instance in the `Activity`, the following instances attached to the current `Context` will automatically take effect:
|
||||||
|
|
||||||
|
- `Fragment`
|
||||||
|
- `Dialog`
|
||||||
|
- `PopupWindow`
|
||||||
|
- `Toast` (foreground only in higher system versions)
|
||||||
|
|
||||||
|
Layouts based on `RemoteView` will not take effect because they are remote objects and do not use the current `Context`'s `LayoutInflater` for layout loading.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
If you are using [ui-component → AppBindingActivity](https://betterandroid.github.io/BetterAndroid/KDoc/ui-component/ui-component/com.highcapable.betterandroid.ui.component.activity/-app-binding-activity) in `BetterAndroid`, you need to slightly modify the current code.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : AppBindingActivity<ActivityMainBinding>() {
|
||||||
|
|
||||||
|
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||||
|
val inflater = super.onPrepareContentView(savedInstanceState)
|
||||||
|
// Inject here.
|
||||||
|
PanguTextFactory2.inject(inflater)
|
||||||
|
return inflater
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// Your code here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If your application does not use `AppCompatActivity` or `ViewBinding`, don't worry, you can still use the original method.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : Activity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// Inject here.
|
||||||
|
PanguTextFactory2.inject(this)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
`PanguTextFactory2` can be used not only with `Activity` but also injected into any existing `LayoutInflater` instance.
|
||||||
|
However, please inject before the `LayoutInflater` instance is used to load the layout, otherwise it will not take effect.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Manual Injection or Text Formatting
|
||||||
|
|
||||||
|
`PanguText` also supports manual injection, allowing you to inject it into the desired `TextView` or `EditText`.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Assume this is your TextView.
|
||||||
|
val textView: TextView
|
||||||
|
// Assume this is your EditText.
|
||||||
|
val editText: EditText
|
||||||
|
// Inject into existing text.
|
||||||
|
textView.injectPanguText()
|
||||||
|
editText.injectPanguText()
|
||||||
|
// Optionally choose whether to inject Hint (default is true).
|
||||||
|
textView.injectPanguText(injectHint = false)
|
||||||
|
editText.injectPanguText(injectHint = false)
|
||||||
|
// Dynamic injection, re-calling setText will automatically take effect.
|
||||||
|
textView.injectRealTimePanguText()
|
||||||
|
// Dynamic injection mainly targets the input state of EditText.
|
||||||
|
editText.injectRealTimePanguText()
|
||||||
|
// Optionally choose whether to inject Hint (default is true).
|
||||||
|
textView.injectRealTimePanguText(injectHint = false)
|
||||||
|
editText.injectRealTimePanguText(injectHint = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
`PanguText` also extends the `setText` method of `TextView`, allowing you to directly set text with `PanguText` style.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Assume this is your TextView.
|
||||||
|
val textView: TextView
|
||||||
|
// Set text with PanguText style.
|
||||||
|
textView.setTextWithPangu("Xiaoming今年16岁")
|
||||||
|
// Set Hint with PanguText style.
|
||||||
|
textView.setHintWithPangu("输入Xiaoming的年龄")
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the `PanguText.format` method to directly format text.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Assume this is your TextView.
|
||||||
|
val textView: TextView
|
||||||
|
// Format text using SpannableString method.
|
||||||
|
// Requires passing the current TextView's Resources and text size.
|
||||||
|
// If the input text is already Spannable,
|
||||||
|
// it will return the original object without creating a new SpannableString.
|
||||||
|
val text = PanguText.format(textView.resources, textView.textSize, "Xiaoming今年16岁")
|
||||||
|
// Set text.
|
||||||
|
textView.text = text
|
||||||
|
// Directly format text using whitespace characters for insertion.
|
||||||
|
// This method adds extra whitespace characters " " (HSP) to the text.
|
||||||
|
// The result below will output the string "Xiaoming 今年 16 岁".
|
||||||
|
// You can also customize the whitespace character at the end of the method.
|
||||||
|
val text = PanguText.format("Xiaoming今年16岁")
|
||||||
|
// Set text.
|
||||||
|
textView.text = text
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
The `injectPanguText`, `injectRealTimePanguText`, `setTextWithPangu`, `setHintWithPangu`, and `PanguText.format` methods support the `config` parameter.
|
||||||
|
You can refer to the [Personalized Configuration](#personalized-configuration) section below.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Custom View
|
||||||
|
|
||||||
|
`PanguText` can also be used with custom `View`. You can extend your `View` to `AppCompatTextView` and override the `setText` method.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyTextView(context: Context, attrs: AttributeSet? = null) : AppCompatTextView(context, attrs) {
|
||||||
|
|
||||||
|
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||||
|
// Manually inject here.
|
||||||
|
val panguText = text?.let { PanguText.format(resources, textSize, it) }
|
||||||
|
super.setText(panguText, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
After injecting `PanguText` into `TextView`, if you use `android:singleLine="true"` in XML layout or `TextView.setSingleLine(true)` in code along with `android:ellipsize="..."`,
|
||||||
|
this method of setting single-line text may cause unresolvable `OBJ` characters (truncated by ellipsis) to appear when the text exceeds the screen width, because `TextView` does not account for `Span` during measurement, leading to incorrect text width calculation.
|
||||||
|
|
||||||
|
The solution is to use `android:maxLines="1"` in XML layout or `TextView.setMaxLines(1)` in code instead.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="这是一段很长很长长长长长长长长长长长长还有English混入的的文本"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Personalized Configuration
|
||||||
|
|
||||||
|
`PanguText` supports personalized configuration. You can use the global static instance `PanguText.globalConfig` to get the global configuration or configure it individually.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Get global configuration.
|
||||||
|
val config = PanguText.globalConfig
|
||||||
|
// Enable or disable the feature.
|
||||||
|
config.isEnabled = true
|
||||||
|
// Process Spanned text.
|
||||||
|
// Processing Spanned text is enabled by default, but this feature is experimental.
|
||||||
|
// If issues occur, you can disable it. When disabled, Spanned text will return the original text.
|
||||||
|
config.isProcessedSpanned = true
|
||||||
|
// Set patterns to exclude during formatting using regular expressions.
|
||||||
|
// For example, exclude all URLs.
|
||||||
|
config.excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
// For example, exclude emoji placeholders like "[doge]",
|
||||||
|
// if you use [ImageSpan] to display emoji images, you can choose to exclude these placeholders.
|
||||||
|
config.excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
// Set the spacing ratio for CJK characters.
|
||||||
|
// This determines the final layout effect.
|
||||||
|
// It is recommended to keep the default ratio and adjust it according to personal preference.
|
||||||
|
config.cjkSpacingRatio = 7f
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
If you integrated using the [Inject to LayoutInflater](#inject-to-layoutinflater) method, configure `PanguText.globalConfig` before executing `PanguTextFactory2.inject(...)`, otherwise the configuration will not take effect.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
You can also pass the `config` parameter for personalized configuration when manually injecting or formatting text.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Assume this is your TextView.
|
||||||
|
val textView: TextView
|
||||||
|
// Create a new configuration.
|
||||||
|
// You can set [copyFromGlobal] to false to not copy from the global configuration.
|
||||||
|
val config = PanguTextConfig(copyFromGlobal = false) {
|
||||||
|
excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
cjkSpacingRatio = 7f
|
||||||
|
}
|
||||||
|
// You can also copy and create a new configuration from any configuration.
|
||||||
|
val config2 = config.copy {
|
||||||
|
excludePatterns.clear()
|
||||||
|
excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
cjkSpacingRatio = 7f
|
||||||
|
}
|
||||||
|
// Manually inject and configure.
|
||||||
|
textView.injectPanguText(config = config2)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you integrated using the [Inject to LayoutInflater](#inject-to-layoutinflater) method, you can use the following attributes in the XML layout declaration of `TextView`, `EditText`, or their subclasses for personalized configuration.
|
||||||
|
|
||||||
|
- `panguText_enabled` corresponds to `PanguTextConfig.isEnabled`
|
||||||
|
- `panguText_processedSpanned` corresponds to `PanguTextConfig.isProcessedSpanned`
|
||||||
|
- `panguText_excludePatterns` corresponds to `PanguTextConfig.excludePatterns`, string array, multiple patterns separated by `|@|`
|
||||||
|
- `panguText_cjkSpacingRatio` corresponds to `PanguTextConfig.cjkSpacingRatio`
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Xiaoming今年16岁"
|
||||||
|
app:panguText_enabled="true"
|
||||||
|
app:panguText_processedSpanned="true"
|
||||||
|
app:panguText_excludePatterns="https?://\\S+;\\[.*?]|@|\\[.*?]"
|
||||||
|
app:panguText_cjkSpacingRatio="7.0" />
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
Due to issues with Android Studio, the above attributes may not have auto-completion hints. Please complete them manually.
|
||||||
|
|
||||||
|
Don't forget to add the declaration `xmlns:app="http://schemas.android.com/apk/res-auto"`.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
In custom `View`, you can extend your `View` to implement the `PanguTextView` interface to achieve the same functionality.
|
||||||
|
|
||||||
|
> The following example
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyTextView(context: Context, attrs: AttributeSet? = null) : AppCompatTextView(context, attrs),
|
||||||
|
PanguTextView {
|
||||||
|
|
||||||
|
override fun configurePanguText(config: PanguTextConfig) {
|
||||||
|
// Configure your [PanguTextConfig].
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
The `PanguTextView` interface takes precedence over attributes used directly in the XML layout. If you use both methods for configuration, the `PanguTextView` interface configuration will override the XML layout configuration.
|
||||||
|
|
||||||
|
Individual configurations will override global configurations, and options not configured will follow the global configuration.
|
||||||
|
|
||||||
|
:::
|
9
docs-source/src/en/library/compose.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Jetpack Compose
|
||||||
|
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
|
||||||
|
This is the core dependency for Jetpack Compose (multiplatform). When using `PanguText` in Jetpack Compose, you need to include this module.
|
||||||
|
|
||||||
|
This module is currently under development and will be gradually improved in the future.
|
17
docs-source/src/index.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
navbar: false
|
||||||
|
sidebar: false
|
||||||
|
title: null
|
||||||
|
heroAlt: null
|
||||||
|
heroText: null
|
||||||
|
tagline: Select a language
|
||||||
|
actions:
|
||||||
|
- text: English
|
||||||
|
link: /en/
|
||||||
|
type: secondary
|
||||||
|
- text: 简体中文
|
||||||
|
link: /zh-cn/
|
||||||
|
type: secondary
|
||||||
|
footer: Apache-2.0 License | Copyright (C) 2019 HighCapable
|
||||||
|
---
|
27
docs-source/src/zh-cn/about/about.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 关于此文档
|
||||||
|
|
||||||
|
> 此文档由 [VuePress](https://v2.vuepress.vuejs.org/zh) 强力驱动。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
[Apache-2.0](https://github.com/HighCapable/YukiReflection/blob/master/LICENSE)
|
||||||
|
|
||||||
|
```:no-line-numbers
|
||||||
|
Apache License Version 2.0
|
||||||
|
|
||||||
|
Copyright (C) 2019 HighCapable
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
```
|
||||||
|
|
||||||
|
版权所有 © 2019 HighCapable
|
19
docs-source/src/zh-cn/about/changelog.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
> 这里记录了 `PanguText` 的版本更新历史。
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
|
||||||
|
我们只会对最新的 API 版本进行维护,若你正在使用过时的 API 版本则代表你自愿放弃一切维护的可能性。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## pangutext-android
|
||||||
|
|
||||||
|
### 1.0.0 | 2025.02.10  <Badge type="tip" text="最新" vertical="middle" />
|
||||||
|
|
||||||
|
- 首个版本提交至 Maven
|
||||||
|
|
||||||
|
## pangutext-compose
|
||||||
|
|
||||||
|
暂未发布。
|
15
docs-source/src/zh-cn/about/contacts.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 联系我们
|
||||||
|
|
||||||
|
> 如在使用中有任何问题,或有任何建设性的建议,都可以联系我们。
|
||||||
|
|
||||||
|
加入我们的开发者群组。
|
||||||
|
|
||||||
|
- [点击加入 Telegram 群组](https://t.me/BetterAndroid)
|
||||||
|
- [点击加入 Telegram 群组 (开发者)](https://t.me/HighCapable_Dev)
|
||||||
|
- [点击加入 QQ 群 (开发者)](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf)
|
||||||
|
|
||||||
|
在 **酷安** 找到我 [@星夜不荟](http://www.coolapk.com/u/876977)。
|
||||||
|
|
||||||
|
## 助力维护
|
||||||
|
|
||||||
|
感谢您选择并使用 `PanguText`,如有代码相关的建议和请求,可在 GitHub 提交 Pull Request。
|
15
docs-source/src/zh-cn/about/future.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 展望未来
|
||||||
|
|
||||||
|
> 未来是美好的,也是不确定的,让我们共同期待 `PanguText` 在未来的发展空间。
|
||||||
|
|
||||||
|
## 未来的计划
|
||||||
|
|
||||||
|
> 这里收录了 `PanguText` 可能会在后期添加的功能。
|
||||||
|
|
||||||
|
### SpannableString 的局限性
|
||||||
|
|
||||||
|
`PanguText` 目前在 Android 平台上的主要功能来自 `SpannableString`,目前尚未完全解决处理复杂的文本样式以及性能开销问题。
|
||||||
|
|
||||||
|
### Jetpack Compose 计划
|
||||||
|
|
||||||
|
`PanguText` 未来将会支持 Jetpack Compose,并计划采用 `AnnotatedString` 作为主要的文本处理方式以实现对底层的最小化侵入。
|
5
docs-source/src/zh-cn/config/r8-proguard.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# R8 与 Proguard 混淆
|
||||||
|
|
||||||
|
> 大部分场景下应用程序安装包可通过混淆压缩体积,这里介绍了混淆规则的配置方法。
|
||||||
|
|
||||||
|
`PanguText` 不需要额外配置混淆规则。
|
55
docs-source/src/zh-cn/guide/home.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 介绍
|
||||||
|
|
||||||
|
> `PanguText` 是一个中日韩 (CJK) 与英文单词、半角数字排版的解决方案。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
这个项目的起因是因为直到目前为止还没有一套公开的方案能够完美解决中文、日文、韩文与英文之间的排版问题,
|
||||||
|
正常情况下我们将 CJK (即中日韩) 与英文混排的时候,都会涉及到美观性问题,这算是一个历史遗留问题,全角文字与半角文字之间的书写规范不一样。虽然现在 W3C 规定了 CJK 排版规范,
|
||||||
|
但是还是仅有部分愿意遵守排版要求的个人或企业选择了这种方案。
|
||||||
|
|
||||||
|
目前已知的厂商解决方案如下
|
||||||
|
|
||||||
|
- Apple 全系 (iOS、iPadOS、macOS、tvOS、watchOS) 文本排版解决方案
|
||||||
|
- 小米 (HyperOS) 文本排版优化
|
||||||
|
- OrginOS 基于字体的文本排版优化
|
||||||
|
|
||||||
|
但是这些方案都是封闭的,无法在其他平台上使用,因此我们希望能够提供一套开源的解决方案,能够适应各种场景、侵入性低且更容易集成,让更多的开发者能够使用这个方案来解决文本排版问题。
|
||||||
|
|
||||||
|
本项目得以进行的主要来源为 [pangu.js](https://github.com/vinta/pangu.js),它提供了一套 CJK 排版的正则,我们对其加以优化,实现各个平台不需要插入空格字符即可格式化文本排版的效果,
|
||||||
|
衷心感谢这个项目的开发者提供的方案,我们在这个方案上加以扩展,提供了更多解决方案的可能性。
|
||||||
|
|
||||||
|
## 效果
|
||||||
|
|
||||||
|
如你所见,`PanguText` 的排版方案并不是向 CJK 与英文单词之间插入空格来完成,而是使用每个平台对应的处理方案自动在这些字符之间添加空白间距来达到排版效果以达到最低的侵入性。
|
||||||
|
|
||||||
|
> 应用前 (上)、应用后 (下)
|
||||||
|
|
||||||
|
<img src="/images/demo_01.png" width="300" />
|
||||||
|
|
||||||
|
> 动态应用
|
||||||
|
|
||||||
|
<img src="/images/demo_02.gif" width="480" />
|
||||||
|
|
||||||
|
`PanguText` 支持动态应用,它允许你在输入文本的同时动态为每个字符添加空白间距。
|
||||||
|
|
||||||
|
::: tip 开发者的观点
|
||||||
|
|
||||||
|
我个人依然不提倡手动为 CJK 和英文字符之间添加空格来达到排版美化效果 (如果软件、系统本身支持这种排版美化方式),
|
||||||
|
因为空格在不同的字体中的间距也是不一样的,这会造成排版效果出现问题,也会被加入本不应该出现的空格字符,在某些场景下,例如网址、文件名或者带有 “#”
|
||||||
|
的话题标签,不允许出现这些空格。但是,在一些特殊场景,例如代码的注释中,涉及到代码的说明文档,建议加入空格,因为这些范围内可能不会有排版格式化工具。
|
||||||
|
|
||||||
|
还有一点就是,在不同的语言中使用不同的标点符号,切忌全角和半角标点符号混用,如果一定要使用半角标点符号来标记全角文字,在句子未结束时将半角符号向后推进一个空格补全字符空间 (英文也是如此)。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 语言要求
|
||||||
|
|
||||||
|
推荐使用 Kotlin 作为首选开发语言,本项目完全使用 Kotlin 编写,在部分内容上对 Java 做了兼容处理,但也许无法做到完全兼容。
|
||||||
|
|
||||||
|
文档全部的 Demo 示例代码都将使用 Kotlin 进行描述,如果你完全不会使用 Kotlin,那么你将有可能无法获得最佳使用体验。
|
||||||
|
|
||||||
|
## 功能贡献
|
||||||
|
|
||||||
|
本项目的维护离不开各位开发者的支持和贡献,目前这个项目处于初期阶段,可能依然存在一些问题或者缺少你需要的功能,
|
||||||
|
如果可能,欢迎提交 PR 为此项目贡献你认为需要的功能或前往 [GitHub Issues](repo://issues) 向我们提出建议。
|
83
docs-source/src/zh-cn/guide/quick-start.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 快速开始
|
||||||
|
|
||||||
|
> 集成 `PanguText` 到你的项目中。
|
||||||
|
|
||||||
|
## 项目要求
|
||||||
|
|
||||||
|
项目需要使用 `Android Studio` 或 `IntelliJ IDEA` 创建且类型为 Android 或 Kotlin Multiplatform 项目并已集成 Kotlin 环境依赖。
|
||||||
|
|
||||||
|
- Android Studio (建议 [从这里](https://developer.android.com/studio) 获取最新版本)
|
||||||
|
|
||||||
|
- IntelliJ IDEA (建议 [从这里](https://www.jetbrains.com/idea) 获取最新版本)
|
||||||
|
|
||||||
|
- Kotlin 1.9.0+、Gradle 8+、Java 17+、Android Gradle Plugin 8+
|
||||||
|
|
||||||
|
### 配置存储库
|
||||||
|
|
||||||
|
`PanguText` 的依赖发布在 **Maven Central** 和我们的公共存储库中,你可以使用如下方式配置存储库。
|
||||||
|
|
||||||
|
我们推荐使用 Kotlin DSL 作为 Gradle 构建脚本语言并推荐使用 [SweetDependency](https://github.com/HighCapable/SweetDependency) 来管理依赖。
|
||||||
|
|
||||||
|
#### SweetDependency (推荐)
|
||||||
|
|
||||||
|
在你的项目 `SweetDependency` 配置文件中配置存储库。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
repositories:
|
||||||
|
google:
|
||||||
|
maven-central:
|
||||||
|
# (可选) 你可以添加此 URL 以使用我们的公共存储库
|
||||||
|
# 当 Sonatype-OSS 发生故障无法发布依赖时,此存储库作为备选进行添加
|
||||||
|
# 详情请前往:https://github.com/HighCapable/maven-repository
|
||||||
|
highcapable-maven-releases:
|
||||||
|
# 中国大陆用户请将下方的 "raw.githubusercontent.com" 修改为 "raw.gitmirror.com"
|
||||||
|
url: https://raw.githubusercontent.com/HighCapable/maven-repository/main/repository/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 传统方式
|
||||||
|
|
||||||
|
在你的项目 `build.gradle.kts` 中配置存储库。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
// (可选) 你可以添加此 URL 以使用我们的公共存储库
|
||||||
|
// 当 Sonatype-OSS 发生故障无法发布依赖时,此存储库作为备选进行添加
|
||||||
|
// 详情请前往:https://github.com/HighCapable/maven-repository
|
||||||
|
// 中国大陆用户请将下方的 "raw.githubusercontent.com" 修改为 "raw.gitmirror.com"
|
||||||
|
maven("https://raw.githubusercontent.com/HighCapable/maven-repository/main/repository/releases")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置 Java 版本
|
||||||
|
|
||||||
|
在你的项目 `build.gradle.kts` 中修改 Kotlin 的 Java 版本为 17 及以上。
|
||||||
|
|
||||||
|
```kt
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能一览
|
||||||
|
|
||||||
|
整个项目分为多个模块,Android 平台与 Jetpack Compose (多平台),你可以选择你希望引入的模块作为依赖应用到你的项目中。
|
||||||
|
|
||||||
|
你可以点击下方对应的模块前往查看详细的功能介绍。
|
||||||
|
|
||||||
|
- [Android](../library/android.md)
|
||||||
|
- [Jetpack Compose](../library/compose.md)
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
你可以在下方找到一些示例,查看对应的演示项目来更好地了解这些功能的运作方式,快速地挑选出你需要的功能。
|
||||||
|
|
||||||
|
- [Android](repo://tree/main/demo-android)
|
||||||
|
- [Jetpack Compose (敬请期待)](repo://tree/main/demo-compose)
|
13
docs-source/src/zh-cn/index.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
title: 首页
|
||||||
|
heroImage: /images/logo.png
|
||||||
|
actions:
|
||||||
|
- text: 快速上手
|
||||||
|
link: /zh-cn/guide/home
|
||||||
|
type: primary
|
||||||
|
- text: 更新日志
|
||||||
|
link: /zh-cn/about/changelog
|
||||||
|
type: secondary
|
||||||
|
footer: Apache-2.0 License | Copyright (C) 2019 HighCapable
|
||||||
|
---
|
365
docs-source/src/zh-cn/library/android.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# Android
|
||||||
|
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
|
||||||
|
这是 Android 平台的核心依赖,在 Android 平台上使用 `PanguText` 时,你需要引入此模块。
|
||||||
|
|
||||||
|
## 配置依赖
|
||||||
|
|
||||||
|
你可以使用如下方式将此模块添加到你的项目中。
|
||||||
|
|
||||||
|
### SweetDependency (推荐)
|
||||||
|
|
||||||
|
在你的项目 `SweetDependency` 配置文件中添加依赖。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
libraries:
|
||||||
|
com.highcapable.pangutext:
|
||||||
|
pangutext-android:
|
||||||
|
version: +
|
||||||
|
```
|
||||||
|
|
||||||
|
在你的项目 `build.gradle.kts` 中配置依赖。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
implementation(com.highcapable.pangutext.pangutext.android)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 传统方式
|
||||||
|
|
||||||
|
在你的项目 `build.gradle.kts` 中配置依赖。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
implementation("com.highcapable.pangutext:pangutext-android:<version>")
|
||||||
|
```
|
||||||
|
|
||||||
|
请将 `<version>` 修改为此文档顶部显示的版本。
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
你可以 [点击这里](kdoc://pangutext-android) 查看 KDoc。
|
||||||
|
|
||||||
|
### 实现原理
|
||||||
|
|
||||||
|
`PanguText` 在 Android 平台有两种方案对文本进行格式化,一种为 `SpannableString` (不破坏原始文本长度),另一种则是直接插入空白字符 (破坏原始文本长度)。
|
||||||
|
|
||||||
|
第一种方案为 `SpannableString`,它会在需要增加间距的字符的前一个字符后增加应用了间距的 `Span` 来实现文本在样式上的改变,而不实际改变字符串的内容,最后交由 `TextView` 层完成渲染 (或手动使用 `TextPaint` 基于 `Spanned` 做布局样式处理),实现无侵入式为文本设置样式。
|
||||||
|
|
||||||
|
第一种方案同样支持直接处理已经应用了样式的文本 (`Spanned`),例如通过 `Html.fromHtml` 创建的文本,**但是目前尚处于实验性阶段,可能仍然会出现非预期样式错误问题**,
|
||||||
|
你可以参考下方的 [个性化配置](#个性化配置) 选择禁用它。
|
||||||
|
|
||||||
|
动态应用 (注入) 功能主要针对 `EditText` 的输入状态,它会为 `EditText` 设置一个自定义的 `TextWatcher` 来监听输入状态,当输入状态发生变化时,从 `afterTextChanged` 中获取 `Editable` 并进行格式化。
|
||||||
|
|
||||||
|
第二种方案则是直接插入空白字符,它会直接在需要增加间距的字符后插入空白字符,这种方案会破坏原始文本的长度并且会改变文本内容自身,
|
||||||
|
但是可以不依赖于 `TextView` 层完成渲染,直接使用 `TextPaint` 绘制文本即可,适用于所有场景,**但不支持动态应用 (注入)**。
|
||||||
|
|
||||||
|
::: warning 尚未解决的问题
|
||||||
|
|
||||||
|
`PanguText` 可能会与 Material 组件 `TextInputEditText`、`MaterialAutoCompleteTextView` 与 `TextInputLayout` 结合时在 `setHint` 效果上产生冲突,
|
||||||
|
因为 `TextView` 不会在测量时计算文本中的 `Span`,在单行文本中此类问题尤为明显,暂时还没有解决方案,请谨慎配合此类组件使用。
|
||||||
|
|
||||||
|
受制于上述问题,通过 `View.measure` 方法计算包含了 `PanguText` 风格的 `TextView` 宽度时也可能会出现错误。
|
||||||
|
|
||||||
|
`PanguText` 目前不能处理 `Spanned` 文本中的下划线、删除线这种连续的字符,添加空白间距后线条会中断,
|
||||||
|
并且它可能会在一些特殊字符上发生样式错误或样式没有被正确应用,为了稳定性考虑请尽量不要对非常复杂的富文本启用 `PanguText` 或参考下方的 [个性化配置](#个性化配置) 设置 `excludePatterns`。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 集成到现有项目
|
||||||
|
|
||||||
|
将 `PanguText` 集成到你的当前项目中非常容易,你不需要改动过多代码,挑选以下你喜欢的方案进行,即可完成集成。
|
||||||
|
|
||||||
|
#### 注入布局装载器 (LayoutInflater)
|
||||||
|
|
||||||
|
`PanguText` 支持直接注入 `LayoutInflater.Factory2` 或为当前 `Activity` 创建 `LayoutInflater.Factory2` 实例以接管整个视图,
|
||||||
|
这是推荐的集成方案,这种方式不需要修改任何现有布局即可实现无侵入式快速集成。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : AppCompactActivity() {
|
||||||
|
|
||||||
|
val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// 在这里注入
|
||||||
|
PanguTextFactory2.inject(this)
|
||||||
|
setContentView(binding.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
由于接管了 `LayoutInflater.Factory2`,所以包括类似 `ListView`、`RecyclerView` 的回收式布局也能被正确接管。
|
||||||
|
|
||||||
|
注入 `Activity` 中的 `LayoutInflater` 实例后,以下附属于当前 `Context` 的实例都会自动生效。
|
||||||
|
|
||||||
|
- `Fragment`
|
||||||
|
- `Dialog`
|
||||||
|
- `PopupWindow`
|
||||||
|
- `Toast` (在高版本系统中仅前台)
|
||||||
|
|
||||||
|
基于 `RemoteView` 的布局将无法生效,因为它们是远程对象,不会使用当前 `Context` 的 `LayoutInflater` 进行布局装载。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
如果你正在使用 `BetterAndroid` 中的 [ui-compoment → AppBindingActivity](https://betterandroid.github.io/BetterAndroid/KDoc/ui-component/ui-component/com.highcapable.betterandroid.ui.component.activity/-app-binding-activity),你需要稍微改动当前代码。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : AppBindingActivity<ActivityMainBinding>() {
|
||||||
|
|
||||||
|
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||||
|
val inflater = super.onPrepareContentView(savedInstanceState)
|
||||||
|
// 在这里注入
|
||||||
|
PanguTextFactory2.inject(inflater)
|
||||||
|
return inflater
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// Your code here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你的应用程序没有使用 `AppCompatActivity` 也没有使用 `ViewBinding`,没有关系,你依然可以使用最初的方案进行。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MainActivity : Activity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// 在这里注入
|
||||||
|
PanguTextFactory2.inject(this)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
`PanguTextFactory2` 除了可以配合 `Activity` 使用,它还支持注入到任何现有的 `LayoutInflater` 实例中,但请在 `LayoutInflater` 实例被用于装载布局前进行注入,否则将无法生效。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 手动注入或格式化文本
|
||||||
|
|
||||||
|
`PanguText` 同样支持手动注入,你可以在需要的 `TextView` 或 `EditText` 上手动进行注入。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 假设这就是你的 TextView
|
||||||
|
val textView: TextView
|
||||||
|
// 假设这就是你的 EditText
|
||||||
|
val editText: EditText
|
||||||
|
// 注入到现有文本
|
||||||
|
textView.injectPanguText()
|
||||||
|
editText.injectPanguText()
|
||||||
|
// 可以选择是否同时注入 Hint (默认是)
|
||||||
|
textView.injectPanguText(injectHint = false)
|
||||||
|
editText.injectPanguText(injectHint = false)
|
||||||
|
// 动态注入,重新调用 setText 也会自动生效
|
||||||
|
textView.injectRealTimePanguText()
|
||||||
|
// 动态注入主要针对于 EditText 的输入状态
|
||||||
|
editText.injectRealTimePanguText()
|
||||||
|
// 同样可以选择是否同时注入 Hint (默认是)
|
||||||
|
textView.injectRealTimePanguText(injectHint = false)
|
||||||
|
editText.injectRealTimePanguText(injectHint = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
`PanguText` 还对 `TextView` 的 `setText` 方法进行了扩展,你可以使用如下方式直接设置带有 `PanugText` 样式的文本。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 假设这就是你的 TextView
|
||||||
|
val textView: TextView
|
||||||
|
// 设置带有 PanguText 样式的文本
|
||||||
|
textView.setTextWithPangu("Xiaoming今年16岁")
|
||||||
|
// 设置带有 PanguText 样式的 Hint
|
||||||
|
textView.setHintWithPangu("输入Xiaoming的年龄")
|
||||||
|
```
|
||||||
|
|
||||||
|
你还可以使用 `PanguText.format` 方法直接格式化文本。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 假设这就是你的 TextView
|
||||||
|
val textView: TextView
|
||||||
|
// 使用 SpannableString 方案格式化文本
|
||||||
|
// 需要传入当前 TextView 的 Resources 以及字体大小
|
||||||
|
// 如果传入的文本自身为 Spannable 类型,则不会创建新的 SpannableString,而是返回原始对象
|
||||||
|
val text = PanguText.format(textView.resources, textView.textSize, "Xiaoming今年16岁")
|
||||||
|
// 设置文本
|
||||||
|
textView.text = text
|
||||||
|
// 直接使用空白字符以插入破坏的方式格式化文本
|
||||||
|
// 这个方案会为文本增加额外的空白字符 " " (HSP)
|
||||||
|
// 下方的结果会输出字符串 "Xiaoming 今年 16 岁"
|
||||||
|
// 你也可以在方法末位自定义要使用的空白字符
|
||||||
|
val text = PanguText.format("Xiaoming今年16岁")
|
||||||
|
// 设置文本
|
||||||
|
textView.text = text
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
`injectPanguText`、`injectRealTimePanguText`、`setTextWithPangu`、`setHintWithPangu`、`PanguText.format` 方法支持 `config` 参数,你可以参考下方的 [个性化配置](#个性化配置)。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 自定义 View
|
||||||
|
|
||||||
|
`PanguText` 还可以配合自定义 `View` 进行使用,你可以将你的 `View` 继承到 `AppCompatTextView` 并重写 `setText` 方法。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyTextView(context: Context, attrs: AttributeSet? = null) : AppCompatTextView(context, attrs) {
|
||||||
|
|
||||||
|
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||||
|
// 在这里手动进行注入
|
||||||
|
val panguText = text?.let { PanguText.format(resources, textSize, it) }
|
||||||
|
super.setText(panguText, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
`TextView` 在注入 `PanguText` 后,如果你在 XML 布局中使用了 `android:singleLine="true"` 或在代码中使用了 `TextView.setSingleLine(true)` 并且配合 `android:elipsize="..."`,
|
||||||
|
那么这种方式设置单行文本可能会造成文本超出屏幕后其中会中显示出无法解析的 `OBJ` 字符 (被省略号截断),因为 `TextView` 不会在测量时计算文本中的 `Span`,这会导致文本宽度计算错误。
|
||||||
|
解决方案为在 XML 布局中使用 `android:maxLines="1"` 或在代码中使用 `TextView.setMaxLines(1)` 来代替。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="这是一段很长很长长长长长长长长长长长长还有English混入的的文本"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 个性化配置
|
||||||
|
|
||||||
|
`PanguText` 支持个性化配置,你可以使用全局静态实例 `PanguText.globalConfig` 获取全局配置,或单独进行配置。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 获取全局配置
|
||||||
|
val config = PanguText.globalConfig
|
||||||
|
// 开关,禁用将使所有功能失效
|
||||||
|
config.isEnabled = true
|
||||||
|
// 处理 Spanned 文本
|
||||||
|
// Spanned 文本处理默认启用,但此功能尚处于实验性阶段,
|
||||||
|
// 如果发生问题你可以选择禁用,禁用后遇到 Spanned 文本将返回原始文本
|
||||||
|
config.isProcessedSpanned = true
|
||||||
|
// 设置在格式化过程中以正则形式定义需要排除的内容
|
||||||
|
// 例如排除全部 URL
|
||||||
|
config.excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
// 例如排除类似 "[doge]" 的 emoji 占位符,
|
||||||
|
// 如果你需要使用 [ImageSpan] 显示 emoji 图片,你可以选择排除这些占位符
|
||||||
|
config.excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
// 设置 CJK 空白占位间距比例
|
||||||
|
// 这会决定最终的排版效果,建议保持默认比例,然后再以此跟随个人喜好进行调整
|
||||||
|
config.cjkSpacingRatio = 7f
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
如果你使用了 [注入布局装载器](#注入布局装载器-layoutinflater) 的方案进行集成,请在 `PanguTextFactory2.inject(...)` 执行前配置 `PanguText.globalConfig`,否则配置将无法生效。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
你还可以在手动注入或格式化文本时传入 `config` 参数以进行个性化配置。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 假设这就是你的 TextView
|
||||||
|
val textView: TextView
|
||||||
|
// 创建一个新配置
|
||||||
|
// 你可以设置 [copyFromGlobal] 为 false 来不从全局配置中复制配置
|
||||||
|
val config = PanguTextConfig(copyFromGlobal = false) {
|
||||||
|
excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
cjkSpacingRatio = 7f
|
||||||
|
}
|
||||||
|
// 你还可以从任意一个配置中复制并创建新配置
|
||||||
|
val config2 = config.copy {
|
||||||
|
excludePatterns.clear()
|
||||||
|
excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
cjkSpacingRatio = 7f
|
||||||
|
}
|
||||||
|
// 手动注入并配置
|
||||||
|
textView.injectPanguText(config = config2)
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你使用了 [注入布局装载器](#注入布局装载器-layoutinflater) 的方案进行集成,你可以在 `TextView`、`EditText` 或继承于它们的 XML 布局声明中使用以下属性来进行个性化配置。
|
||||||
|
|
||||||
|
- `panguText_enabled` 对应 `PanguTextConfig.isEnabled`
|
||||||
|
- `panguText_processedSpanned` 对应 `PanguTextConfig.isProcessedSpanned`
|
||||||
|
- `panguText_excludePatterns` 对应 `PanguTextConfig.excludePatterns`,字符串数组,多个使用 `|@|` 分隔
|
||||||
|
- `panguText_cjkSpacingRatio` 对应 `PanguTextConfig.cjkSpacingRatio`
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Xiaoming今年16岁"
|
||||||
|
app:panguText_enabled="true"
|
||||||
|
app:panguText_processedSpanned="true"
|
||||||
|
app:panguText_excludePatterns="https?://\\S+;\\[.*?]|@|\\[.*?]"
|
||||||
|
app:panguText_cjkSpacingRatio="7.0" />
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
由于 Android Studio 的问题,上述属性可能不会有补全提示,请自行补全。
|
||||||
|
|
||||||
|
不要忘记加入声明 `xmlns:app="http://schemas.android.com/apk/res-auto"`。
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
在自定义 `View` 中,你可以将你的 `View` 继承于 `PanguTextView` 接口以同样实现上述功能。
|
||||||
|
|
||||||
|
> 示例如下
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MyTextView(context: Context, attrs: AttributeSet? = null) : AppCompatTextView(context, attrs),
|
||||||
|
PanguTextView {
|
||||||
|
|
||||||
|
override fun configurePanguText(config: PanguTextConfig) {
|
||||||
|
// 配置你的 [PanguTextConfig]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
`PanguTextView` 接口的优先级将高于直接在 XML 布局中使用的属性,如果你同时使用了这两种方式进行配置,`PanguTextView` 接口的配置将覆盖 XML 布局中的配置。
|
||||||
|
|
||||||
|
单独配置将覆盖全局配置,未配置的选项将跟随全局配置。
|
||||||
|
|
||||||
|
:::
|
9
docs-source/src/zh-cn/library/compose.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Jetpack Compose
|
||||||
|
|
||||||
|

|
||||||
|
<span style="margin-left: 5px"/>
|
||||||
|

|
||||||
|
|
||||||
|
这是 Jetpack Compose (多平台) 的核心依赖,在 Jetpack Compose 上使用 `PanguText` 时,你需要引入此模块。
|
||||||
|
|
||||||
|
此模块尚在开发阶段,将在后期逐渐进行完善。
|
2004
docs-source/yarn.lock
Normal file
35
gradle.properties
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Compiler Configuration
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
kotlin.incremental.useClasspathSnapshot=true
|
||||||
|
# Project Configuration
|
||||||
|
project.name=PanguText
|
||||||
|
project.url=https://github.com/BetterAndroid/PanguText
|
||||||
|
project.groupName=com.highcapable.pangutext
|
||||||
|
project.android.compileSdk=35
|
||||||
|
project.android.minSdk=21
|
||||||
|
project.android.targetSdk=35
|
||||||
|
project.app.packageName=com.highcapable.pangutext.demo
|
||||||
|
project.app.versionName=universal
|
||||||
|
project.app.versionCode=1
|
||||||
|
project.pangutext-android.namespace=${project.groupName}.android
|
||||||
|
project.pangutext-android.version="1.0.0"
|
||||||
|
# Maven Publish Configuration
|
||||||
|
SONATYPE_HOST=CENTRAL_PORTAL
|
||||||
|
RELEASE_SIGNING_ENABLED=true
|
||||||
|
# Maven POM Configuration
|
||||||
|
POM_NAME=PanguText
|
||||||
|
POM_DESCRIPTION=A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
POM_URL=https://github.com/BetterAndroid/PanguText
|
||||||
|
POM_LICENSE_NAME=Apache License 2.0
|
||||||
|
POM_LICENSE_URL=https://github.com/BetterAndroid/PanguText/blob/main/LICENSE
|
||||||
|
POM_LICENSE_DIST=repo
|
||||||
|
POM_SCM_URL=https://github.com/BetterAndroid/PanguText
|
||||||
|
POM_SCM_CONNECTION=scm:git:git://github.com/BetterAndroid/PanguText.git
|
||||||
|
POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/BetterAndroid/PanguText.git
|
||||||
|
POM_DEVELOPER_ID=0
|
||||||
|
POM_DEVELOPER_NAME=fankes
|
||||||
|
POM_DEVELOPER_EMAIL=qzmmcn@163.com
|
||||||
|
POM_DEVELOPER_URL=https://github.com/fankes
|
65
gradle/sweet-dependency/sweet-dependency-config.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
preferences:
|
||||||
|
autowire-on-sync-mode: UPDATE_OPTIONAL_DEPENDENCIES
|
||||||
|
repositories-mode: FAIL_ON_PROJECT_REPOS
|
||||||
|
|
||||||
|
repositories:
|
||||||
|
gradle-plugin-portal:
|
||||||
|
scope: PLUGINS
|
||||||
|
google:
|
||||||
|
maven-central:
|
||||||
|
highcapable-maven-releases:
|
||||||
|
url: https://raw.githubusercontent.com/HighCapable/maven-repository/main/repository/releases
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
com.android.application:
|
||||||
|
alias: android-application
|
||||||
|
version: 8.5.2
|
||||||
|
com.android.library:
|
||||||
|
alias: android-library
|
||||||
|
version-ref: com.android.application
|
||||||
|
org.jetbrains.kotlin.android:
|
||||||
|
alias: kotlin-android
|
||||||
|
version: 2.0.21
|
||||||
|
org.jetbrains.dokka:
|
||||||
|
alias: kotlin-dokka
|
||||||
|
version: 1.9.20
|
||||||
|
com.vanniktech.maven.publish:
|
||||||
|
alias: maven-publish
|
||||||
|
version: 0.30.0
|
||||||
|
|
||||||
|
libraries:
|
||||||
|
com.highcapable.betterandroid:
|
||||||
|
ui-component:
|
||||||
|
version: 1.0.6
|
||||||
|
ui-extension:
|
||||||
|
version: 1.0.5
|
||||||
|
system-extension:
|
||||||
|
version: 1.0.2
|
||||||
|
com.highcapable.yukireflection:
|
||||||
|
api:
|
||||||
|
version: 1.0.3
|
||||||
|
androidx.core:
|
||||||
|
core:
|
||||||
|
version: 1.15.0
|
||||||
|
core-ktx:
|
||||||
|
version-ref: <this>::core
|
||||||
|
androidx.appcompat:
|
||||||
|
appcompat:
|
||||||
|
version: 1.7.0
|
||||||
|
com.google.android.material:
|
||||||
|
material:
|
||||||
|
# Workaround for a bug in version 1.12.0
|
||||||
|
version: 1.11.0
|
||||||
|
auto-update: false
|
||||||
|
androidx.constraintlayout:
|
||||||
|
constraintlayout:
|
||||||
|
version: 2.2.0
|
||||||
|
junit:
|
||||||
|
junit:
|
||||||
|
version: 4.13.2
|
||||||
|
androidx.test.ext:
|
||||||
|
junit:
|
||||||
|
version: 1.2.1
|
||||||
|
androidx.test.espresso:
|
||||||
|
espresso-core:
|
||||||
|
version: 3.6.1
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
252
gradlew
vendored
Executable file
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
94
gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
BIN
img-src/icon.png
Normal file
After Width: | Height: | Size: 31 KiB |
49
pangutext-android/build.gradle.kts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
plugins {
|
||||||
|
autowire(libs.plugins.android.library)
|
||||||
|
autowire(libs.plugins.kotlin.android)
|
||||||
|
autowire(libs.plugins.kotlin.dokka)
|
||||||
|
autowire(libs.plugins.maven.publish)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = property.project.groupName
|
||||||
|
version = property.project.pangutext.android.version
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = property.project.pangutext.android.namespace
|
||||||
|
compileSdk = property.project.android.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = property.project.android.minSdk
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
"-Xno-param-assertions",
|
||||||
|
"-Xno-call-assertions",
|
||||||
|
"-Xno-receiver-assertions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(com.highcapable.yukireflection.api)
|
||||||
|
implementation(com.highcapable.betterandroid.ui.extension)
|
||||||
|
implementation(com.highcapable.betterandroid.system.extension)
|
||||||
|
implementation(androidx.core.core.ktx)
|
||||||
|
implementation(androidx.appcompat.appcompat)
|
||||||
|
testImplementation(junit.junit)
|
||||||
|
androidTestImplementation(androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(androidx.test.espresso.espresso.core)
|
||||||
|
}
|
0
pangutext-android/consumer-rules.pro
Normal file
21
pangutext-android/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.highcapable.pangutext
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.highcapable.pangutext.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
4
pangutext-android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/12.
|
||||||
|
*/
|
||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package com.highcapable.pangutext.android
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.CharacterStyle
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import com.highcapable.pangutext.android.core.PanguMarginSpan
|
||||||
|
import com.highcapable.pangutext.android.core.PanguPatterns
|
||||||
|
import com.highcapable.pangutext.android.extension.injectPanguText
|
||||||
|
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||||
|
import com.highcapable.pangutext.android.extension.setTextWithPangu
|
||||||
|
import com.highcapable.yukireflection.factory.classOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The library core of Pangu text processor.
|
||||||
|
*
|
||||||
|
* Bigger thanks for [this](https://github.com/vinta/pangu.java) project.
|
||||||
|
* @see PanguPatterns
|
||||||
|
*/
|
||||||
|
object PanguText {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a placeholder character for replacing the content of the regular expression,
|
||||||
|
* with no actual meaning.
|
||||||
|
*/
|
||||||
|
private const val PH = '\u001C'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The global configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
val globalConfig = PanguTextConfig()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [PanguText] to format specified text.
|
||||||
|
*
|
||||||
|
* [PanguText] will automatically set [PanguMarginSpan] for some characters in
|
||||||
|
* the text to achieve white space typesetting effect without actually inserting
|
||||||
|
* any characters or changing the length of the original text.
|
||||||
|
*
|
||||||
|
* This function will insert a style for the current given [text] without actually changing the string position in the text.
|
||||||
|
* If the current [text] is of type [Spannable], it will return the original unmodified object,
|
||||||
|
* otherwise it will return the wrapped object [SpannableString] after.
|
||||||
|
*
|
||||||
|
* - Note: Processed [Spanned] text is in experimental stage and may not be fully supported,
|
||||||
|
* if the text is not processed correctly, please disable [PanguTextConfig.isProcessedSpanned].
|
||||||
|
* @see PanguTextConfig.isProcessedSpanned
|
||||||
|
* @see PanguTextConfig.cjkSpacingRatio
|
||||||
|
* @see TextView.injectPanguText
|
||||||
|
* @see TextView.injectRealTimePanguText
|
||||||
|
* @see TextView.setTextWithPangu
|
||||||
|
* @param resources the current resources.
|
||||||
|
* @param textSize the text size (px).
|
||||||
|
* @param text text to be formatted.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
@JvmStatic
|
||||||
|
fun format(resources: Resources, @Px textSize: Float, text: CharSequence, config: PanguTextConfig = globalConfig): CharSequence {
|
||||||
|
if (!config.isEnabled) return text
|
||||||
|
if (text.isBlank()) return text
|
||||||
|
val formatted = format(text, PH, config)
|
||||||
|
return text.applySpans(formatted, resources, textSize, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [PanguText] to format the current text content.
|
||||||
|
*
|
||||||
|
* Using this function will add extra [whiteSpace] as character spacing to the text,
|
||||||
|
* changing the length of the original text.
|
||||||
|
*
|
||||||
|
* - Note: Processed [Spanned] text is in experimental stage and may not be fully supported,
|
||||||
|
* if the text is not processed correctly, please disable [PanguTextConfig.isProcessedSpanned].
|
||||||
|
* @see PanguTextConfig.isProcessedSpanned
|
||||||
|
* @param text text to be formatted.
|
||||||
|
* @param whiteSpace the spacing character, default is 'U+200A'.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
@JvmStatic
|
||||||
|
fun format(text: CharSequence, whiteSpace: Char = ' ', config: PanguTextConfig = globalConfig): CharSequence {
|
||||||
|
if (!config.isEnabled) return text
|
||||||
|
// In any case, always perform a cleanup operation before accepting text.
|
||||||
|
val processed = text.clearSpans()
|
||||||
|
val patterns = config.excludePatterns.toTypedArray()
|
||||||
|
return if ((config.isProcessedSpanned || text !is Spanned) && text.isNotBlank() && text.length > 1)
|
||||||
|
PanguPatterns.matchAndReplace(processed, whiteSpace, *patterns)
|
||||||
|
else processed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the [PanguMarginSpan] to the text.
|
||||||
|
* @receiver [CharSequence]
|
||||||
|
* @param formatted the formatted text.
|
||||||
|
* @param resources the current resources.
|
||||||
|
* @param textSize the text size (px).
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
* @param whiteSpace the spacing character, default is [PH].
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
private fun CharSequence.applySpans(
|
||||||
|
formatted: CharSequence,
|
||||||
|
resources: Resources,
|
||||||
|
@Px textSize: Float,
|
||||||
|
config: PanguTextConfig = globalConfig,
|
||||||
|
whiteSpace: Char = PH
|
||||||
|
): CharSequence {
|
||||||
|
val builder = SpannableStringBuilder(formatted)
|
||||||
|
formatted.forEachIndexed { index, c ->
|
||||||
|
// Add spacing to the previous character.
|
||||||
|
if (c == whiteSpace && index in 0..formatted.lastIndex) {
|
||||||
|
val span = PanguMarginSpan.Placeholder()
|
||||||
|
builder.setSpan(span, index - 1, index, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete the placeholder character.
|
||||||
|
for (i in (builder.length - 1) downTo 0) {
|
||||||
|
if (builder[i] == whiteSpace) builder.delete(i, i + 1)
|
||||||
|
}
|
||||||
|
// Find the [PanguMarginSpan.Placeholder] subscript in [builder] and use [PanguMarginSpan] to set it to [original].
|
||||||
|
val builderSpans = builder.getSpans(0, builder.length, classOf<PanguMarginSpan.Placeholder>())
|
||||||
|
val spannable = if (this !is Spannable) SpannableString(this) else this
|
||||||
|
// Add new [PanguMarginSpan].
|
||||||
|
builderSpans.forEach {
|
||||||
|
val start = builder.getSpanStart(it)
|
||||||
|
val end = builder.getSpanEnd(it)
|
||||||
|
val span = PanguMarginSpan.create(resources, textSize, config.cjkSpacingRatio)
|
||||||
|
spannable.setSpan(span, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
|
}; builder.clear()
|
||||||
|
return spannable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the [PanguMarginSpan] from the text.
|
||||||
|
*
|
||||||
|
* Workaround for the issue that the [PanguMarginSpan] repeatedly sets
|
||||||
|
* the same range causes performance degradation.
|
||||||
|
* @receiver [CharSequence]
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
private fun CharSequence.clearSpans(): CharSequence {
|
||||||
|
if (this !is Spannable || isBlank() || !hasSpan<PanguMarginSpan>()) return this
|
||||||
|
getSpans(0, length, classOf<PanguMarginSpan>()).forEach { span ->
|
||||||
|
val start = getSpanStart(span)
|
||||||
|
val end = getSpanEnd(span)
|
||||||
|
// Clear the [PanguMarginSpan].
|
||||||
|
if (start < length && end > 0) removeSpan(span)
|
||||||
|
}; return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the text contains a specific span [T].
|
||||||
|
* @receiver [CharSequence]
|
||||||
|
* @return [Boolean]
|
||||||
|
*/
|
||||||
|
private inline fun <reified T : CharacterStyle> CharSequence.hasSpan(): Boolean {
|
||||||
|
val spannable = this as? Spanned ?: return false
|
||||||
|
val spans = spannable.getSpans(0, spannable.length, classOf<T>())
|
||||||
|
return spans.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/2/6.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.android
|
||||||
|
|
||||||
|
import android.text.Spanned
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [PanguText] configuration.
|
||||||
|
*/
|
||||||
|
class PanguTextConfig internal constructor() : Serializable {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default CJK spacing ratio, adjusted to 7f.
|
||||||
|
* This ratio is considered to be the most comfortable size for reading after a series of comparisons.
|
||||||
|
*/
|
||||||
|
private const val DEFAULT_CJK_SPACING_RATIO = 7f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the [PanguText].
|
||||||
|
*
|
||||||
|
* This is a global switch that can be used to enable or disable the [PanguText] processor.
|
||||||
|
*/
|
||||||
|
var isEnabled = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processed [Spanned] text (experimental).
|
||||||
|
*
|
||||||
|
* - Note: This feature is in experimental stage and may not be fully supported,
|
||||||
|
* if the text is not processed correctly, please disable this feature.
|
||||||
|
*/
|
||||||
|
var isProcessedSpanned = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regular expression for text content that needs to be excluded.
|
||||||
|
* [PanguText] processing will be skipped after matching these texts.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* val config: PanguTextConfig
|
||||||
|
* // Exclude all URLs.
|
||||||
|
* config.excludePatterns.add("https?://\\S+".toRegex())
|
||||||
|
* // Exclude emoji symbol placeholder like "[doge]".
|
||||||
|
* config.excludePatterns.add("\\[.*?]".toRegex())
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
val excludePatterns = mutableSetOf<Regex>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CJK spacing ratio, default is [DEFAULT_CJK_SPACING_RATIO].
|
||||||
|
*
|
||||||
|
* The larger the value, the smaller the spacing, and cannot be less than 0.1f.
|
||||||
|
*
|
||||||
|
* It is recommended to adjust with caution, it will only affect the spacing of CJK characters.
|
||||||
|
*/
|
||||||
|
var cjkSpacingRatio = DEFAULT_CJK_SPACING_RATIO
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the current configuration.
|
||||||
|
* @param body the configuration body.
|
||||||
|
* @return [PanguTextConfig]
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun copy(body: PanguTextConfig.() -> Unit = {}) = PanguTextConfig().also {
|
||||||
|
it.isEnabled = this.isEnabled
|
||||||
|
it.isProcessedSpanned = this.isProcessedSpanned
|
||||||
|
it.excludePatterns.addAll(this.excludePatterns)
|
||||||
|
it.cjkSpacingRatio = this.cjkSpacingRatio
|
||||||
|
it.body()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/14.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.android.core
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.BackgroundColorSpan
|
||||||
|
import android.text.style.CharacterStyle
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.core.text.getSpans
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.toDp
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.toPx
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pangu span with margin.
|
||||||
|
* @param margin the margin size (px).
|
||||||
|
*/
|
||||||
|
internal class PanguMarginSpan(@Px val margin: Int) : ReplacementSpan() {
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of [PanguMarginSpan].
|
||||||
|
* @param resources the current resources.
|
||||||
|
* @param textSize the text size (px).
|
||||||
|
* @param ratio the CJK spacing ratio.
|
||||||
|
* @return [PanguMarginSpan]
|
||||||
|
*/
|
||||||
|
internal fun create(resources: Resources, @Px textSize: Float, ratio: Float) =
|
||||||
|
PanguMarginSpan(getSpanMargin(resources, textSize, ratio))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the margin size (px).
|
||||||
|
* @param resources the current resources.
|
||||||
|
* @param textSize the text size (px).
|
||||||
|
* @param ratio the CJK spacing ratio.
|
||||||
|
* @return [Int]
|
||||||
|
*/
|
||||||
|
private fun getSpanMargin(resources: Resources, @Px textSize: Float, ratio: Float) =
|
||||||
|
round(textSize.toDp(resources) / ratio.coerceAtLeast(0.1f)).toInt().toPx(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContentDescription() = "PanguMarginSpan"
|
||||||
|
|
||||||
|
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?) =
|
||||||
|
(paint.measureText(text, start, end) + margin).toInt()
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
|
||||||
|
if (text is Spanned) text.getSpans<Any>(start, end).forEach { span ->
|
||||||
|
when {
|
||||||
|
span is BackgroundColorSpan -> {
|
||||||
|
// Get background color.
|
||||||
|
val color = span.backgroundColor
|
||||||
|
val originalColor = paint.color
|
||||||
|
// Save the current [paint] color.
|
||||||
|
paint.color = color
|
||||||
|
// Get the width of the text.
|
||||||
|
val textWidth = paint.measureText(text, start, end)
|
||||||
|
// Draw background rectangle.
|
||||||
|
canvas.drawRect(x, top.toFloat(), x + textWidth + margin, bottom.toFloat(), paint)
|
||||||
|
// Restore original color.
|
||||||
|
paint.color = originalColor
|
||||||
|
}
|
||||||
|
span is CharacterStyle && paint is TextPaint -> span.updateDrawState(paint)
|
||||||
|
}
|
||||||
|
}; text?.let { canvas.drawText(it, start, end, x, y.toFloat(), paint) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder span.
|
||||||
|
*/
|
||||||
|
class Placeholder : ReplacementSpan() {
|
||||||
|
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?) = 0
|
||||||
|
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/20.
|
||||||
|
*/
|
||||||
|
@file:Suppress("RegExpRedundantEscape", "RegExpSimplifiable")
|
||||||
|
|
||||||
|
package com.highcapable.pangutext.android.core
|
||||||
|
|
||||||
|
import com.highcapable.pangutext.android.PanguText
|
||||||
|
import com.highcapable.pangutext.android.extension.replaceAndPreserveSpans
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regular expression patterns for [PanguText].
|
||||||
|
*
|
||||||
|
* Some schemes are copied from [Pangu.java](https://github.com/vinta/pangu.java/blob/master/src/main/java/ws/vinta/pangu/Pangu.java),
|
||||||
|
* and some modifications have been made to adapt to the Android environment.
|
||||||
|
*/
|
||||||
|
internal object PanguPatterns {
|
||||||
|
|
||||||
|
private const val CJK = "\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-" +
|
||||||
|
"\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff"
|
||||||
|
|
||||||
|
private val ANY_CJK = "[$CJK]".toRegex()
|
||||||
|
|
||||||
|
private val DOTS_CJK = "([\\.]{2,}|\\u2026)([$CJK])".toRegex()
|
||||||
|
|
||||||
|
private val FIX_CJK_COLON_ANS = "([$CJK])\\:([A-Z0-9\\(\\)])".toRegex()
|
||||||
|
private val CJK_QUOTE = "([$CJK])([\\`\"\\u05f4])".toRegex()
|
||||||
|
|
||||||
|
private val QUOTE_CJK = "([\\`\"\\u05f4])([$CJK])".toRegex()
|
||||||
|
private val FIX_QUOTE_ANY_QUOTE = "([`\"\\u05f4]+)[ ]*(.+?)[ ]*([`\"\\u05f4]+)".toRegex()
|
||||||
|
private val CJK_SINGLE_QUOTE_BUT_POSSESSIVE = "([$CJK])('[^s])".toRegex()
|
||||||
|
|
||||||
|
private val SINGLE_QUOTE_CJK = "(')([$CJK])".toRegex()
|
||||||
|
private val HASH_ANS_CJK_HASH = "([$CJK])(#)([$CJK]+)(#)([$CJK])".toRegex()
|
||||||
|
|
||||||
|
private val CJK_HASH = "([$CJK])(#([^ ]))".toRegex()
|
||||||
|
private val HASH_CJK = "(([^ ])#)([$CJK])".toRegex()
|
||||||
|
private val CJK_OPERATOR_ANS = "([$CJK])([\\+\\-\\*\\/=&\\|<>])([A-Za-z0-9])".toRegex()
|
||||||
|
|
||||||
|
private val ANS_OPERATOR_CJK = "([A-Za-z0-9])([\\+\\-\\*\\/=&\\|<>])([$CJK])".toRegex()
|
||||||
|
private val FIX_SLASH_AS = "([/]) ([a-z\\-\\_\\./]+)".toRegex()
|
||||||
|
|
||||||
|
private val FIX_SLASH_AS_SLASH = "([/\\.])([A-Za-z\\-\\_\\./]+) ([/])".toRegex()
|
||||||
|
private val CJK_LEFT_BRACKET = "([$CJK])([\\(\\[\\{<>\\u201c])".toRegex()
|
||||||
|
|
||||||
|
private val RIGHT_BRACKET_CJK = "([\\)\\]\\}>\\u201d])([$CJK])".toRegex()
|
||||||
|
private val FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET = "([\\(\\[\\{<\\u201c]+)[ ]*(.+?)[ ]*([\\)\\]\\}>\u201d]+)".toRegex()
|
||||||
|
private val AN_LEFT_BRACKET = "([A-Za-z0-9])([\\(\\[\\{])".toRegex()
|
||||||
|
|
||||||
|
private val RIGHT_BRACKET_AN = "([\\)\\]\\}])([A-Za-z0-9])".toRegex()
|
||||||
|
private val CJK_ANS = ("([$CJK])([A-Za-z\\u0370-\\u03ff0-9@\$%\\^&\\*\\-\\+\\\\=\\|" +
|
||||||
|
"/\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])").toRegex()
|
||||||
|
|
||||||
|
private val ANS_CJK = ("([A-Za-z\\u0370-\\u03ff0-9~\\\$%\\^&\\*\\-\\+\\\\=\\|" +
|
||||||
|
"/!;:,\\.\\?\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])([$CJK])").toRegex()
|
||||||
|
private val S_A = "(%)([A-Za-z])".toRegex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match and replace the text with the given regular expression.
|
||||||
|
* @param text text to be formatted.
|
||||||
|
* @param whiteSpace the spacing character.
|
||||||
|
* @param excludePatterns the regular expression to exclude from replacement.
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
internal fun matchAndReplace(text: CharSequence, whiteSpace: Char, vararg excludePatterns: Regex) =
|
||||||
|
if (ANY_CJK.containsMatchIn(text))
|
||||||
|
text.replaceAndPreserveSpans(DOTS_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(FIX_CJK_COLON_ANS, "$1:$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_QUOTE, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(QUOTE_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(FIX_QUOTE_ANY_QUOTE, "$1$2$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_SINGLE_QUOTE_BUT_POSSESSIVE, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(SINGLE_QUOTE_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(HASH_ANS_CJK_HASH, "$1$whiteSpace$2$3$4$whiteSpace$5", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_HASH, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(HASH_CJK, "$1$whiteSpace$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_OPERATOR_ANS, "$1$whiteSpace$2$whiteSpace$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(ANS_OPERATOR_CJK, "$1$whiteSpace$2$whiteSpace$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(FIX_SLASH_AS, "$1$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(FIX_SLASH_AS_SLASH, "$1$2$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_LEFT_BRACKET, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(RIGHT_BRACKET_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET, "$1$2$3", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(AN_LEFT_BRACKET, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(RIGHT_BRACKET_AN, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(CJK_ANS, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(ANS_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
.replaceAndPreserveSpans(S_A, "$1$whiteSpace$2", *excludePatterns)
|
||||||
|
else text
|
||||||
|
}
|
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/2/6.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.android.core
|
||||||
|
|
||||||
|
import com.highcapable.pangutext.android.PanguText
|
||||||
|
import com.highcapable.pangutext.android.PanguTextConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [PanguText] config interface.
|
||||||
|
*/
|
||||||
|
interface PanguTextView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the [PanguText].
|
||||||
|
*
|
||||||
|
* Configuring this item separately will override global settings.
|
||||||
|
* @see PanguText.globalConfig
|
||||||
|
*/
|
||||||
|
fun configurePanguText(config: PanguTextConfig)
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/26.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.android.core
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.highcapable.pangutext.android.PanguText
|
||||||
|
import com.highcapable.pangutext.android.PanguTextConfig
|
||||||
|
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TextWatcher] that automatically applies [PanguText] to the text content.
|
||||||
|
*
|
||||||
|
* You don't need to create it manually, use [TextView.injectRealTimePanguText] instead.
|
||||||
|
* @param base the base [TextView].
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
class PanguTextWatcher internal constructor(private val base: TextView, private val config: PanguTextConfig) : TextWatcher {
|
||||||
|
override fun afterTextChanged(editable: Editable?) {
|
||||||
|
editable?.let { PanguText.format(base.resources, base.textSize, it, config) }
|
||||||
|
}
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
}
|
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/12.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
@file:JvmName("PanguTextUtils")
|
||||||
|
|
||||||
|
package com.highcapable.pangutext.android.extension
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
|
import androidx.core.view.doOnDetach
|
||||||
|
import com.highcapable.betterandroid.ui.extension.view.getTag
|
||||||
|
import com.highcapable.pangutext.android.PanguText
|
||||||
|
import com.highcapable.pangutext.android.PanguTextConfig
|
||||||
|
import com.highcapable.pangutext.android.R
|
||||||
|
import com.highcapable.pangutext.android.core.PanguTextWatcher
|
||||||
|
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of [PanguTextConfig].
|
||||||
|
* @see PanguTextConfig
|
||||||
|
* @param copyFromGlobal whether to copy the [PanguText.globalConfig], default is true.
|
||||||
|
* @param body the configuration body.
|
||||||
|
* @return [PanguTextConfig]
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun PanguTextConfig(copyFromGlobal: Boolean = true, body: PanguTextConfig.() -> Unit) =
|
||||||
|
if (copyFromGlobal) PanguText.globalConfig.copy(body) else PanguTextConfig().apply(body)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject [PanguText] to the current text content once.
|
||||||
|
* @see TextView.setTextWithPangu
|
||||||
|
* @see TextView.setHintWithPangu
|
||||||
|
* @see PanguText.format
|
||||||
|
* @receiver [TextView]
|
||||||
|
* @param injectHint whether to apply [TextView.setHint], default is true.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
|
if (!config.isEnabled) return
|
||||||
|
setTextWithPangu(this.text, config)
|
||||||
|
if (injectHint) setHintWithPangu(this.hint, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject [PanguText] to the current text content in real time.
|
||||||
|
* @see TextView.setTextWithPangu
|
||||||
|
* @see TextView.setHintWithPangu
|
||||||
|
* @see PanguText.format
|
||||||
|
* @receiver [TextView]
|
||||||
|
* @param injectHint whether to apply [TextView.setHint], default is true.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
|
if (!config.isEnabled) return
|
||||||
|
val observerKey = R.id.flag_inject_real_time_pangu_text
|
||||||
|
if (getTag<Boolean>(observerKey) == true) return run {
|
||||||
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Duplicate injection of real-time PanguText ($this).")
|
||||||
|
}
|
||||||
|
setTag(observerKey, injectHint)
|
||||||
|
injectPanguText(injectHint, config)
|
||||||
|
var currentHint = this.hint
|
||||||
|
val textWatcher = PanguTextWatcher(base = this, config)
|
||||||
|
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
val self = this@injectRealTimePanguText
|
||||||
|
if (self.hint != currentHint)
|
||||||
|
self.setHintWithPangu(self.hint, config)
|
||||||
|
currentHint = self.hint
|
||||||
|
}
|
||||||
|
doOnAttach {
|
||||||
|
addTextChangedListener(textWatcher)
|
||||||
|
// Add a global layout listener to monitor the hint text changes.
|
||||||
|
if (injectHint) viewTreeObserver?.addOnGlobalLayoutListener(listener)
|
||||||
|
doOnDetach {
|
||||||
|
removeTextChangedListener(textWatcher)
|
||||||
|
// Remove the global layout listener when the view is detached.
|
||||||
|
if (injectHint) viewTreeObserver?.removeOnGlobalLayoutListener(listener)
|
||||||
|
setTag(observerKey, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [PanguText.format] to format the text content.
|
||||||
|
* @see PanguText.format
|
||||||
|
* @receiver [TextView]
|
||||||
|
* @param text the text content.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
|
if (!config.isEnabled) return
|
||||||
|
this.text = text?.let { PanguText.format(resources, textSize, it, config) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [PanguText.format] to format the hint text content.
|
||||||
|
* @see PanguText.format
|
||||||
|
* @receiver [TextView]
|
||||||
|
* @param text the text content.
|
||||||
|
* @param config the configuration of [PanguText].
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun TextView.setHintWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
|
if (!config.isEnabled) return
|
||||||
|
this.hint = text?.let { PanguText.format(resources, textSize, it, config) }
|
||||||
|
}
|
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/16.
|
||||||
|
*/
|
||||||
|
@file:JvmName("ReplacementUtils")
|
||||||
|
|
||||||
|
package com.highcapable.pangutext.android.extension
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.util.Log
|
||||||
|
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||||
|
import com.highcapable.yukireflection.factory.classOf
|
||||||
|
import com.highcapable.yukireflection.factory.field
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the text content and preserve the original span style.
|
||||||
|
* @see CharSequence.replace
|
||||||
|
* @receiver [CharSequence]
|
||||||
|
* @param regex the regular expression.
|
||||||
|
* @param replacement the replacement text.
|
||||||
|
* @param excludePatterns the regular expression to exclude from replacement, default is null.
|
||||||
|
* @return [CharSequence]
|
||||||
|
*/
|
||||||
|
internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: String, vararg excludePatterns: Regex) =
|
||||||
|
runCatching {
|
||||||
|
val builder = SpannableStringBuilder(this)
|
||||||
|
val matcher = regex.toPattern().matcher(this)
|
||||||
|
val excludeMatchers = excludePatterns.map { it.toPattern().matcher(this) }
|
||||||
|
val excludeIndexs = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
excludeMatchers.forEach {
|
||||||
|
while (it.find()) excludeIndexs.add(it.start() to it.end())
|
||||||
|
}
|
||||||
|
var offset = 0
|
||||||
|
// Offset adjustment to account for changes in the text length after replacements.
|
||||||
|
while (matcher.find()) {
|
||||||
|
val start = matcher.start() + offset
|
||||||
|
val end = matcher.end() + offset
|
||||||
|
// Skip the replacement if the matched range is excluded.
|
||||||
|
// The character range offset is adjusted by 1 to avoid the exclusion of the matched range.
|
||||||
|
if (excludeIndexs.any { it.first <= start + 1 && it.second >= end - 1 }) continue
|
||||||
|
// Perform the replacement.
|
||||||
|
val replacementText = matcher.buildReplacementText(replacement)
|
||||||
|
builder.replace(start, end, replacementText)
|
||||||
|
// Adjust offset based on the length of the replacement.
|
||||||
|
offset += replacementText.length - (end - start)
|
||||||
|
}; builder
|
||||||
|
}.onFailure {
|
||||||
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to replace span text content.", it)
|
||||||
|
}.getOrNull() ?: this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the replacement text based on the matched groups.
|
||||||
|
* @receiver [Matcher]
|
||||||
|
* @param replacement the replacement text.
|
||||||
|
* @return [String]
|
||||||
|
*/
|
||||||
|
private fun Matcher.buildReplacementText(replacement: String): String {
|
||||||
|
val matcher = this
|
||||||
|
var result = replacement
|
||||||
|
// Check for group references (like $1, $2, ...).
|
||||||
|
val pattern = "\\$(\\d+)".toRegex()
|
||||||
|
result = pattern.replace(result) { matchResult ->
|
||||||
|
val groupIndex = matchResult.groupValues[1].toInt()
|
||||||
|
if (groupIndex <= matcher.groupCount())
|
||||||
|
matcher.group(groupIndex) ?: ""
|
||||||
|
else ""
|
||||||
|
}
|
||||||
|
// Check for named groups (like ${groupName}).
|
||||||
|
val namedGroupPattern = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}".toRegex()
|
||||||
|
result = namedGroupPattern.replace(result) { matchResult ->
|
||||||
|
val groupName = matchResult.groupValues[1]
|
||||||
|
val groupIndex = matcher.getNamedGroupIndex(groupName)
|
||||||
|
if (groupIndex >= 0)
|
||||||
|
matcher.group(groupIndex) ?: ""
|
||||||
|
else ""
|
||||||
|
}; return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to find the group index for a named group.
|
||||||
|
* @receiver [Matcher]
|
||||||
|
* @param groupName the group name.
|
||||||
|
* @return [Int]
|
||||||
|
*/
|
||||||
|
private fun Matcher.getNamedGroupIndex(groupName: String): Int {
|
||||||
|
val namedGroups = classOf<Matcher>()
|
||||||
|
.field { name = "namedGroups" }
|
||||||
|
.ignored()
|
||||||
|
.get(this)
|
||||||
|
.cast<Map<String, Int>>()
|
||||||
|
return namedGroups?.get(groupName) ?: -1
|
||||||
|
}
|
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/19.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package com.highcapable.pangutext.android.factory
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import com.highcapable.betterandroid.ui.extension.view.layoutInflater
|
||||||
|
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||||
|
import com.highcapable.yukireflection.factory.field
|
||||||
|
import com.highcapable.yukireflection.type.android.LayoutInflaterClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pangu text factory 2 for [LayoutInflater.Factory2].
|
||||||
|
* @param base the base factory.
|
||||||
|
*/
|
||||||
|
class PanguTextFactory2 private constructor(private val base: LayoutInflater.Factory2?) : LayoutInflater.Factory2 {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject [PanguTextFactory2] to the current [LayoutInflater] of [context].
|
||||||
|
*
|
||||||
|
* Simple Usage:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* class MainActivity : AppCompactActivity() {
|
||||||
|
*
|
||||||
|
* val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
|
||||||
|
*
|
||||||
|
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
* super.onCreate(savedInstanceState)
|
||||||
|
* // Inject here.
|
||||||
|
* PanguTextFactory2.inject(this)
|
||||||
|
* setContentView(binding.root)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Traditional Usage:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* class MainActivity : Activity() {
|
||||||
|
*
|
||||||
|
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
* super.onCreate(savedInstanceState)
|
||||||
|
* // Inject here.
|
||||||
|
* PanguTextFactory2.inject(this)
|
||||||
|
* setContentView(R.layout.activity_main)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Usage with BetterAndroid's AppBindingActivity:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* class MainActivity : AppBindingActivity<ActivityMainBinding>() {
|
||||||
|
*
|
||||||
|
* override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||||
|
* val inflater = super.onPrepareContentView(savedInstanceState)
|
||||||
|
* // Inject here.
|
||||||
|
* PanguTextFactory2.inject(inflater)
|
||||||
|
* return inflater
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
* super.onCreate(savedInstanceState)
|
||||||
|
* // Your code here.
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* @param context the current context.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun inject(context: Context) = inject(context.layoutInflater)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject [PanguTextFactory2] to the current [LayoutInflater].
|
||||||
|
* @see inject
|
||||||
|
* @param inflater the current inflater.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun inject(inflater: LayoutInflater) {
|
||||||
|
val original = inflater.factory2
|
||||||
|
if (original is PanguTextFactory2) return run {
|
||||||
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "PanguTextFactory2 was already injected.")
|
||||||
|
}
|
||||||
|
val replacement = PanguTextFactory2(original)
|
||||||
|
if (original != null)
|
||||||
|
LayoutInflaterClass.field {
|
||||||
|
name = "mFactory2"
|
||||||
|
}.ignored().onNoSuchField {
|
||||||
|
Log.e(PangutextAndroidProperties.PROJECT_NAME, "LayoutInflater.mFactory2 not found.", it)
|
||||||
|
}.get(inflater).set(replacement)
|
||||||
|
else inflater.factory2 = replacement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet) =
|
||||||
|
base?.onCreateView(parent, name, context, attrs).let {
|
||||||
|
PanguWidget.process(name, it, context, attrs) ?: it
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet) =
|
||||||
|
base?.onCreateView(name, context, attrs).let {
|
||||||
|
PanguWidget.process(name, it, context, attrs) ?: it
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||||
|
* Copyright (C) 2019 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/PanguText
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2025/1/19.
|
||||||
|
*/
|
||||||
|
package com.highcapable.pangutext.android.factory
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.getBooleanOrNull
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.getFloatOrNull
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.getStringOrNull
|
||||||
|
import com.highcapable.betterandroid.ui.extension.component.base.obtainStyledAttributes
|
||||||
|
import com.highcapable.pangutext.android.PanguText
|
||||||
|
import com.highcapable.pangutext.android.PanguTextConfig
|
||||||
|
import com.highcapable.pangutext.android.R
|
||||||
|
import com.highcapable.pangutext.android.core.PanguTextView
|
||||||
|
import com.highcapable.pangutext.android.extension.injectPanguText
|
||||||
|
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||||
|
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||||
|
import com.highcapable.yukireflection.factory.classOf
|
||||||
|
import com.highcapable.yukireflection.factory.constructor
|
||||||
|
import com.highcapable.yukireflection.factory.notExtends
|
||||||
|
import com.highcapable.yukireflection.factory.toClassOrNull
|
||||||
|
import com.highcapable.yukireflection.type.android.AttributeSetClass
|
||||||
|
import com.highcapable.yukireflection.type.android.ContextClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A widgets processor that automatically applies [PanguText] to the text content.
|
||||||
|
*/
|
||||||
|
internal object PanguWidget {
|
||||||
|
|
||||||
|
/** The text regex split symbol. */
|
||||||
|
private const val TEXT_REGEX_SPLITE_SYMBOL = "|@|"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the widget by the given name.
|
||||||
|
* @param name the widget name.
|
||||||
|
* @param view the current view.
|
||||||
|
* @param context the context.
|
||||||
|
* @param attrs the attributes.
|
||||||
|
* @return [View] or null.
|
||||||
|
*/
|
||||||
|
fun process(name: String, view: View?, context: Context, attrs: AttributeSet): View? {
|
||||||
|
val instance = view ?: name.let {
|
||||||
|
// There will be commonly used view class names in the XML layout, which is converted here.
|
||||||
|
if (!it.contains(".")) "android.widget.$it" else it
|
||||||
|
}.toClassOrNull()?.let {
|
||||||
|
// Avoid creating unnecessary components for waste.
|
||||||
|
if (it notExtends classOf<TextView>()) return null
|
||||||
|
val twoParams = it.constructor {
|
||||||
|
param(ContextClass, AttributeSetClass)
|
||||||
|
}.ignored().get()
|
||||||
|
val onceParam = it.constructor {
|
||||||
|
param(ContextClass)
|
||||||
|
}.ignored().get()
|
||||||
|
twoParams.newInstance<View>(context, attrs) ?: onceParam.newInstance<View>(context)
|
||||||
|
}
|
||||||
|
// Ignore if the instance is not a [TextView].
|
||||||
|
if (instance !is TextView) return null
|
||||||
|
var config = PanguText.globalConfig
|
||||||
|
if (instance is PanguTextView) {
|
||||||
|
val configCopy = config.copy()
|
||||||
|
instance.configurePanguText(configCopy)
|
||||||
|
config = configCopy
|
||||||
|
if (!config.isEnabled) return instance
|
||||||
|
} else instance.obtainStyledAttributes(attrs, R.styleable.PanguTextHelper) {
|
||||||
|
val isEnabled = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_enabled)
|
||||||
|
val isProcessedSpanned = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_processedSpanned)
|
||||||
|
val cjkSpacingRatio = it.getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
||||||
|
val excludePatterns = it.getStringOrNull(R.styleable.PanguTextHelper_panguText_excludePatterns)
|
||||||
|
?.split(TEXT_REGEX_SPLITE_SYMBOL)?.mapNotNull { regex ->
|
||||||
|
runCatching { regex.toRegex() }.onFailure { th ->
|
||||||
|
Log.e(PangutextAndroidProperties.PROJECT_NAME, "Invalid exclude pattern of $instance: $regex", th)
|
||||||
|
}.getOrNull()
|
||||||
|
}?.toTypedArray() ?: emptyArray()
|
||||||
|
if (isEnabled == false) return instance
|
||||||
|
if (isProcessedSpanned != null || cjkSpacingRatio != null || excludePatterns.isNotEmpty()) {
|
||||||
|
val configCopy = config.copy()
|
||||||
|
configCopy.isProcessedSpanned = isProcessedSpanned ?: config.isProcessedSpanned
|
||||||
|
configCopy.cjkSpacingRatio = cjkSpacingRatio ?: config.cjkSpacingRatio
|
||||||
|
if (excludePatterns.isNotEmpty()) {
|
||||||
|
config.excludePatterns.clear()
|
||||||
|
config.excludePatterns.addAll(excludePatterns)
|
||||||
|
}; config = configCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (instance.javaClass.name) {
|
||||||
|
// Specialize those components because loading "hint" style after [doOnAttachRepeatable] causes problems.
|
||||||
|
"com.google.android.material.textfield.TextInputEditText",
|
||||||
|
"com.google.android.material.textfield.MaterialAutoCompleteTextView" -> {
|
||||||
|
instance.injectPanguText(config = config)
|
||||||
|
instance.doOnAttachRepeatable(config) { it.injectRealTimePanguText(injectHint = false, config) }
|
||||||
|
}
|
||||||
|
else -> instance.doOnAttachRepeatable(config) {
|
||||||
|
it.injectRealTimePanguText(config = config)
|
||||||
|
}
|
||||||
|
}; return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copied from [View.doOnAttach]. */
|
||||||
|
private inline fun <reified V : View> V.doOnAttachRepeatable(config: PanguTextConfig, crossinline action: (view: V) -> Unit) {
|
||||||
|
if (!config.isEnabled) return
|
||||||
|
if (isAttachedToWindow) action(this)
|
||||||
|
addOnAttachStateChangeListener(
|
||||||
|
object : View.OnAttachStateChangeListener {
|
||||||
|
override fun onViewAttachedToWindow(view: View) {
|
||||||
|
// Re-execute it every time to prevent layout re-creation problems
|
||||||
|
// similar to [RecyclerView.Adapter] or [BaseAdapter] after reuse.
|
||||||
|
if (config.isEnabled) action(view as V)
|
||||||
|
}
|
||||||
|
override fun onViewDetachedFromWindow(view: View) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
13
pangutext-android/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="PanguTextHelper">
|
||||||
|
<!-- Enable [PanguText] for this view. -->
|
||||||
|
<attr name="panguText_enabled" format="boolean" />
|
||||||
|
<!-- Processed [Spanned] text (experimental). -->
|
||||||
|
<attr name="panguText_processedSpanned" format="boolean" />
|
||||||
|
<!-- The regular expression for text content that needs to be excluded. -->
|
||||||
|
<attr name="panguText_excludePatterns" format="string" />
|
||||||
|
<!-- The CJK spacing ratio. -->
|
||||||
|
<attr name="panguText_cjkSpacingRatio" format="float" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
4
pangutext-android/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<id name="flag_inject_real_time_pangu_text" type="id" />
|
||||||
|
</resources>
|
@@ -0,0 +1,18 @@
|
|||||||
|
package com.highcapable.pangutext
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
23
settings.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
id("com.highcapable.sweetdependency") version "1.0.4"
|
||||||
|
id("com.highcapable.sweetproperty") version "1.0.5"
|
||||||
|
}
|
||||||
|
sweetProperty {
|
||||||
|
rootProject { all { isEnable = false } }
|
||||||
|
project(":pangutext-android") {
|
||||||
|
sourcesCode {
|
||||||
|
isEnableRestrictedAccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "PanguText"
|
||||||
|
include(":demo-android")
|
||||||
|
include(":pangutext-android")
|