Initial commit

This commit is contained in:
2025-02-10 03:05:25 +08:00
commit 57a0ecc385
93 changed files with 6685 additions and 0 deletions

35
.editorconfig Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,92 @@
# Pangu Text
[![GitHub license](https://img.shields.io/github/license/BetterAndroid/android-app-template?color=blue)](https://github.com/BetterAndroid/android-app-template/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram)](https://t.me/BetterAndroid)
[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram)](https://t.me/HighCapable_Dev)
[![QQ](https://img.shields.io/badge/discussion%20dev-QQ-blue.svg?logo=tencent-qq&logoColor=red)](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
![Star History Chart](https://api.star-history.com/svg?repos=BetterAndroid/PanguText&type=Date)
## 许可证
- [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
View File

@@ -0,0 +1,100 @@
# Pangu Text
[![GitHub license](https://img.shields.io/github/license/BetterAndroid/android-app-template?color=blue)](https://github.com/BetterAndroid/android-app-template/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram)](https://t.me/BetterAndroid)
[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram)](https://t.me/HighCapable_Dev)
[![QQ](https://img.shields.io/badge/discussion%20dev-QQ-blue.svg?logo=tencent-qq&logoColor=red)](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 hasnt 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
- Xiaomis (HyperOS) text typography optimization
- OrginOSs 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
![Star History Chart](https://api.star-history.com/svg?repos=BetterAndroid/PanguText&type=Date)
## 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
View 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"
}

View 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
View 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);
}

View File

@@ -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)
}
}

View 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>

View File

@@ -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()
)
}
}
}
}

View File

@@ -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>()
}
}
}

View File

@@ -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
}
}

View File

@@ -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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View 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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Pangu Text 演示</string>
</resources>

View 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>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Pangu Text Demo</string>
</resources>

View 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>

View 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>

View File

@@ -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
View File

@@ -0,0 +1,4 @@
/node_modules
/src/.vuepress/.cache
/src/.vuepress/.temp
/dist

3
docs-source/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}

View 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
View 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": {}
}

View 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: '搜索' }
}
})
]
};

View 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')
}]
};

View 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);
};
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View 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};
}
}

View 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

View 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 &ensp;<Badge type="tip" text="latest" vertical="middle" />
- The first version is submitted to Maven
## pangutext-compose
Not yet released.

View 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.

View 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.

View 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.

View 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 hasnt 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
- Xiaomis (HyperOS) text typography optimization
- OrginOSs 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 dont 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.

View 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)

View 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
---

View File

@@ -0,0 +1,367 @@
# Android
![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.pangutext/pangutext-android?logo=apachemaven&logoColor=orange)
<span style="margin-left: 5px"/>
![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fraw.githubusercontent.com%2FHighCapable%2Fmaven-repository%2Frefs%2Fheads%2Fmain%2Frepository%2Freleases%2Fcom%2Fhighcapable%2Fpangutext%2Fpangutext-android%2Fmaven-metadata.xml&logo=apachemaven&logoColor=orange&label=highcapable-maven-releases)
<span style="margin-left: 5px"/>
![Android Min SDK](https://img.shields.io/badge/Min%20SDK-21-orange?logo=android)
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.
:::

View File

@@ -0,0 +1,9 @@
# Jetpack Compose
![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.pangutext/pangutext-compose?logo=apachemaven&logoColor=orange)
<span style="margin-left: 5px"/>
![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fraw.githubusercontent.com%2FHighCapable%2Fmaven-repository%2Frefs%2Fheads%2Fmain%2Frepository%2Freleases%2Fcom%2Fhighcapable%2Fpangutext%2Fpangutext-compose%2Fmaven-metadata.xml&logo=apachemaven&logoColor=orange&label=highcapable-maven-releases)
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
View 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
---

View 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

View File

@@ -0,0 +1,19 @@
# 更新日志
> 这里记录了 `PanguText` 的版本更新历史。
::: danger
我们只会对最新的 API 版本进行维护,若你正在使用过时的 API 版本则代表你自愿放弃一切维护的可能性。
:::
## pangutext-android
### 1.0.0 | 2025.02.10 &ensp;<Badge type="tip" text="最新" vertical="middle" />
- 首个版本提交至 Maven
## pangutext-compose
暂未发布。

View 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。

View File

@@ -0,0 +1,15 @@
# 展望未来
> 未来是美好的,也是不确定的,让我们共同期待 `PanguText` 在未来的发展空间。
## 未来的计划
> 这里收录了 `PanguText` 可能会在后期添加的功能。
### SpannableString 的局限性
`PanguText` 目前在 Android 平台上的主要功能来自 `SpannableString`,目前尚未完全解决处理复杂的文本样式以及性能开销问题。
### Jetpack Compose 计划
`PanguText` 未来将会支持 Jetpack Compose并计划采用 `AnnotatedString` 作为主要的文本处理方式以实现对底层的最小化侵入。

View File

@@ -0,0 +1,5 @@
# R8 与 Proguard 混淆
> 大部分场景下应用程序安装包可通过混淆压缩体积,这里介绍了混淆规则的配置方法。
`PanguText` 不需要额外配置混淆规则。

View 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) 向我们提出建议。

View 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)

View 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
---

View File

@@ -0,0 +1,365 @@
# Android
![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.pangutext/pangutext-android?logo=apachemaven&logoColor=orange)
<span style="margin-left: 5px"/>
![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fraw.githubusercontent.com%2FHighCapable%2Fmaven-repository%2Frefs%2Fheads%2Fmain%2Frepository%2Freleases%2Fcom%2Fhighcapable%2Fpangutext%2Fpangutext-android%2Fmaven-metadata.xml&logo=apachemaven&logoColor=orange&label=highcapable-maven-releases)
<span style="margin-left: 5px"/>
![Android Min SDK](https://img.shields.io/badge/Min%20SDK-21-orange?logo=android)
这是 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 布局中的配置。
单独配置将覆盖全局配置,未配置的选项将跟随全局配置。
:::

View File

@@ -0,0 +1,9 @@
# Jetpack Compose
![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.pangutext/pangutext-compose?logo=apachemaven&logoColor=orange)
<span style="margin-left: 5px"/>
![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fraw.githubusercontent.com%2FHighCapable%2Fmaven-repository%2Frefs%2Fheads%2Fmain%2Frepository%2Freleases%2Fcom%2Fhighcapable%2Fpangutext%2Fpangutext-compose%2Fmaven-metadata.xml&logo=apachemaven&logoColor=orange&label=highcapable-maven-releases)
这是 Jetpack Compose (多平台) 的核心依赖,在 Jetpack Compose 上使用 `PanguText` 时,你需要引入此模块。
此模块尚在开发阶段,将在后期逐渐进行完善。

2004
docs-source/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

35
gradle.properties Normal file
View 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

View 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

Binary file not shown.

View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View 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)
}

View File

21
pangutext-android/proguard-rules.pro vendored Normal file
View 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

View File

@@ -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)
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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) {}
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {}
}

View File

@@ -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) }
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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) {}
}
)
}
}

View 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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<id name="flag_inject_real_time_pangu_text" type="id" />
</resources>

View File

@@ -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
View 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")