├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── RELEASING.md
├── ViewPumpSample
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── io
│ │ └── github
│ │ └── inflationx
│ │ └── viewpump
│ │ └── sample
│ │ ├── CustomTextView.java
│ │ ├── CustomTextViewInterceptor.java
│ │ ├── MainActivity.java
│ │ ├── SampleApplication.java
│ │ ├── SampleFragment.java
│ │ └── TextUpdatingInterceptor.java
│ └── res
│ ├── layout
│ ├── activity_main.xml
│ └── fragment_sample.xml
│ └── values
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── viewpump
├── Module.md
├── build.gradle.kts
└── src
├── main
├── java
│ └── io
│ │ └── github
│ │ └── inflationx
│ │ └── viewpump
│ │ ├── FallbackViewCreator.kt
│ │ ├── InflateRequest.kt
│ │ ├── InflateResult.kt
│ │ ├── Interceptor.kt
│ │ ├── ViewPump.kt
│ │ ├── ViewPumpContextWrapper.kt
│ │ └── internal
│ │ ├── -FallbackViewCreationInterceptor.kt
│ │ ├── -InterceptorChain.kt
│ │ ├── -ReflectionUtils.kt
│ │ ├── -ReflectiveFallbackViewCreator.kt
│ │ ├── -ViewPumpActivityFactory.kt
│ │ └── -ViewPumpLayoutInflater.kt
└── res
│ └── values
│ └── ids.xml
└── test
└── java
└── io
└── github
└── inflationx
└── viewpump
├── test
└── ViewPumpTest.java
└── util
├── AnotherTestView.java
├── AnotherTestViewNewingPreInflationInterceptor.java
├── NameChangingPreInflationInterceptor.java
├── SingleConstructorTestView.java
├── TestFallbackViewCreator.java
├── TestPostInflationInterceptor.java
└── TestView.java
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | # Only run push on main
5 | push:
6 | branches:
7 | - main
8 | paths-ignore:
9 | - '**/*.md'
10 | # Always run on PRs
11 | pull_request:
12 | branches: [ main ]
13 | merge_group:
14 |
15 | concurrency:
16 | group: 'ci-${{ github.head_ref }}-${{ github.workflow }}'
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 |
26 | - name: Gradle Wrapper Validation
27 | uses: gradle/wrapper-validation-action@v1
28 |
29 | - name: Install JDK
30 | uses: actions/setup-java@v3
31 | with:
32 | distribution: 'zulu'
33 | java-version: '19'
34 |
35 | - name: Build and run tests
36 | id: gradle
37 | uses: gradle/gradle-build-action@v2
38 | with:
39 | arguments: check
40 | gradle-home-cache-cleanup: true
41 | cache-read-only: false
42 |
43 | - name: (Fail-only) Upload build reports
44 | if: failure()
45 | uses: actions/upload-artifact@v3
46 | with:
47 | name: reports
48 | path: |
49 | **/build/reports/**
50 |
51 | - name: Publish snapshot (main branch only)
52 | if: github.repository == 'inflationx/viewpump' && github.ref == 'refs/heads/main'
53 | run: ./gradlew publishRelease -PSONATYPE_USERNAME=${{ secrets.SONATYPE_USERNAME }} -PSONATYPE_USERNAME=${{ secrets.SONATYPE_PASSWORD }} --no-configuration-cache
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/workspace.xml
38 | .idea
39 |
40 | # Keystore files
41 | *.jks
42 |
43 | # OSX
44 | .DS_Store
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Version 2.1.1 *(2023-04-05)*
4 | - Fix nullability issues - @ZacSweers
5 |
6 | ## Version 2.1.0 *(2023-03-30)*
7 | - Deprecate static `init`, `get`, and related APIs in favor of instance-based use. Instead, install local `ViewPump` instances as needed via `ViewPumpContextWrapper.wrap(context, viewPump)`.
8 | - Optimize internal `cloneInContext()` calls.
9 | - Update to Kotlin `1.8.10`.
10 | - Update to Android compile SDK 33. Note this is a source breaking change in some places where new nullability annotations on `AttributeSet` are used. These are propagated as needed.
11 | - Build against `androidx.appcompat:appcompat` to `1.6.1`.
12 | - Use new `maven-publish` plugin.
13 |
14 | ## Version 2.0.3 *(2019-06-07)*
15 | - Update the LayoutInflater to be compatible with Android Q
16 |
17 | ## Version 2.0.2 *(2019-04-16)*
18 |
19 | - Fix SAM invocation of Interceptor instances via `operator fun invoke()` extension
20 | - Add `Builder.addInterceptor()` convenience extension function that accepts a `(Chain) -> InflateResult` parameter
21 |
22 | ## Version 2.0.1 *(2019-04-03)*
23 |
24 | - Fix nullability and reflection field name
25 | - Disabled generation of unused BuildConfig
26 | - Cleaned up consumer proguard rules
27 | - Update Kotlin to 1.3.21 and use it as `api` dependency
28 | - Add proper Dokka support
29 |
30 | ## Version 2.0.0 *(2019-01-28)*
31 |
32 | - **Breaking change:** Project migrated to [AndroidX](https://developer.android.com/jetpack/androidx/). See the [class and package mappings](https://developer.android.com/jetpack/androidx/migrate) for help migrating
33 | - **Breaking change:** The previously `public` `ReflectionUtils` class is now internal only, and inaccessible to Java users. This class should never have been used anyway though, so ideally should not be a breaking change to most.
34 | - Migrated library to Kotlin. Aside from the two breaking changes listed above, this should otherwise be a non-breaking API change for Java users.
35 |
36 | ## Version 1.0.0 *(2017-05-01)*
37 |
38 | - Initial Release
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ViewPump
2 | ========
3 |
4 | View inflation you can intercept.
5 |
6 | ViewPump installs a custom `LayoutInflater` via a `ContextThemeWrapper` and provides an API of pre/post-inflation interceptors.
7 |
8 | ## Getting started
9 |
10 | ### Dependency
11 |
12 | Include the dependency [Download (.aar)](http://search.maven.org/remotecontent?filepath=io/github/inflationx/viewpump/2.1.0/viewpump-2.1.0.aar) :
13 |
14 | ```groovy
15 | dependencies {
16 | implementation 'io.github.inflationx:viewpump:2.1.0'
17 | }
18 | ```
19 |
20 | ### Usage
21 |
22 | Define your interceptor. Below is a somewhat arbitrary example of a post-inflation interceptor that prefixes the text in a TextView.
23 |
24 | ```java
25 | public class TextUpdatingInterceptor implements Interceptor {
26 | @Override
27 | public InflateResult intercept(Chain chain) {
28 | InflateResult result = chain.proceed(chain.request());
29 | if (result.view() instanceof TextView) {
30 | // Do something to result.view()
31 | // You have access to result.context() and result.attrs()
32 | TextView textView = (TextView) result.view();
33 | textView.setText("[Prefix] " + textView.getText());
34 | }
35 | return result;
36 | }
37 | }
38 | ```
39 |
40 | Below is an example of a pre-inflation interceptor that returns a CustomTextView when a TextView was defined in the layout's XML.
41 |
42 | ```java
43 | public class CustomTextViewInterceptor implements Interceptor {
44 | @Override
45 | public InflateResult intercept(Chain chain) {
46 | InflateRequest request = chain.request();
47 | if (request.name().endsWith("TextView")) {
48 | CustomTextView view = new CustomTextView(request.context(), request.attrs());
49 | return InflateResult.builder()
50 | .view(view)
51 | .name(view.getClass().getName())
52 | .context(request.context())
53 | .attrs(request.attrs())
54 | .build();
55 | } else {
56 | return chain.proceed(request);
57 | }
58 | }
59 | }
60 | ```
61 |
62 | ## Installation
63 |
64 | Create a `ViewPump` instance via `ViewPump.builder()` and add your interceptors. The order of the interceptors is important since they form the interceptor chain of requests and results.
65 |
66 | An interceptor may choose to return a programmatically instantiated view rather than letting the default inflation occur, in which case interceptors added after it will be skipped. For this reason, it is better to add your post-inflation interceptors before the pre-inflation interceptors
67 |
68 | ```java
69 | ViewPump viewPump = ViewPump.builder()
70 | .addInterceptor(new TextUpdatingInterceptor())
71 | .addInterceptor(new CustomTextViewInterceptor())
72 | .build()
73 | ```
74 |
75 | Once the instance is created (via dependency injection or otherwise), provide it to your relevant context via wrapping the `Activity` context:
76 |
77 | ```java
78 | @Override
79 | protected void attachBaseContext(Context newBase) {
80 | super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase, viewPump));
81 | }
82 | ```
83 |
84 | _You're good to go!_
85 |
86 | To see more ideas for potential use cases, check out the [Recipes](https://github.com/InflationX/ViewPump/wiki/Recipes) wiki page.
87 |
88 | ## Testing
89 |
90 | If you need to test views in isolation (i.e. not under the indirect umbrella of an `Activity`), you need to set `factory2` manually in order for `ViewPump` to work.
91 |
92 | ```kotlin
93 | val context = ViewPumpContextWrapper.wrap(textContext, viewPump)
94 | LayoutInflater.from(context).factory2 = FakeFactory2() // Can be a stub that just returns null
95 |
96 | // Now inflate your view
97 | val view = LayoutInflater.from(context).inflate(R.layout.view, null)
98 | ```
99 |
100 | Otherwise, no `factory2` instance will be set in the underlying `LayoutInflater` and subsequently `ViewPump` will never be called during inflation.
101 |
102 | ## Collaborators
103 |
104 | - [@jbarr21](https://github.com/jbarr21)
105 | - [@chrisjenx](https://github.com/chrisjenx)
106 |
107 | ## Licence
108 |
109 | Copyright 2017 InflationX
110 |
111 | Licensed under the Apache License, Version 2.0 (the "License");
112 | you may not use this file except in compliance with the License.
113 | You may obtain a copy of the License at
114 |
115 | http://www.apache.org/licenses/LICENSE-2.0
116 |
117 | Unless required by applicable law or agreed to in writing, software
118 | distributed under the License is distributed on an "AS IS" BASIS,
119 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
120 | See the License for the specific language governing permissions and
121 | limitations under the License.
122 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | Releasing
2 | =========
3 |
4 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version.
5 | 2. Update the `CHANGELOG.md` for the impending release
6 | 3. Update the `README.md` with the new version.
7 | 4. `git checkout -b release/X.Y.0`
8 | 6. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
9 | 7. `./gradlew clean publishRelease`
10 | 8. `git push --tags`
11 | 10. Update the `gradle.properties` to the next SNAPSHOT version.
12 | 11. `git commit -am "Prepare next development version"`
13 | 12. `git push -u origin release/X.Y.0`
14 | 13. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
15 | 14. Merge `release/X.Y.0` Pull-Request
16 |
--------------------------------------------------------------------------------
/ViewPumpSample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.agp.application)
3 | }
4 |
5 | android {
6 | compileSdk 33
7 | namespace = "io.github.inflationx.viewpump.sample"
8 |
9 | defaultConfig {
10 | minSdkVersion 14
11 | targetSdkVersion 33
12 | versionCode 1
13 | versionName "1"
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility JavaVersion.VERSION_1_8
18 | targetCompatibility JavaVersion.VERSION_1_8
19 | }
20 |
21 | buildTypes {
22 | debug {
23 | matchingFallbacks = ['release']
24 | minifyEnabled true
25 | proguardFiles getDefaultProguardFile('proguard-android.txt')
26 | }
27 | release {
28 | minifyEnabled true
29 | proguardFiles getDefaultProguardFile('proguard-android.txt')
30 | }
31 | }
32 |
33 | lintOptions {
34 | // we don't need to be indexed by firebase and we don't have a custom icon for our sample app
35 | disable 'GoogleAppIndexingWarning', 'MissingApplicationIcon'
36 | textReport System.getenv('CI') == 'true'
37 | }
38 | }
39 |
40 | dependencies {
41 | implementation project(':viewpump')
42 | implementation libs.androidx.appcompat
43 |
44 | debugImplementation libs.leakcanary.android
45 | }
46 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/CustomTextView.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.util.AttributeSet;
6 | import android.widget.TextView;
7 |
8 | @SuppressLint("AppCompatCustomView")
9 | public class CustomTextView extends TextView {
10 |
11 | public CustomTextView(Context context) {
12 | super(context);
13 | }
14 |
15 | public CustomTextView(Context context, AttributeSet attrs) {
16 | super(context, attrs);
17 | }
18 |
19 | public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) {
20 | super(context, attrs, defStyleAttr);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/CustomTextViewInterceptor.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 |
7 | import androidx.annotation.Nullable;
8 | import io.github.inflationx.viewpump.InflateRequest;
9 | import io.github.inflationx.viewpump.InflateResult;
10 | import io.github.inflationx.viewpump.Interceptor;
11 |
12 | /**
13 | * This is an example of a pre-inflation interceptor that returns programmatically instantiated
14 | * CustomTextViews instead of inflating TextViews.
15 | */
16 | public class CustomTextViewInterceptor implements Interceptor {
17 |
18 | @Override
19 | public InflateResult intercept(Chain chain) {
20 | InflateRequest request = chain.request();
21 | View view = inflateView(request.name(), request.context(), request.attrs());
22 |
23 | if (view != null) {
24 | return InflateResult.builder()
25 | .view(view)
26 | .name(view.getClass().getName())
27 | .context(request.context())
28 | .attrs(request.attrs())
29 | .build();
30 | } else {
31 | return chain.proceed(request);
32 | }
33 | }
34 |
35 | @Nullable
36 | private View inflateView(String name, Context context, AttributeSet attrs) {
37 | if ("TextView".equals(name)) {
38 | return new CustomTextView(context, attrs);
39 | }
40 | return null;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 |
6 | import androidx.appcompat.app.AppCompatActivity;
7 | import androidx.appcompat.widget.Toolbar;
8 |
9 | import io.github.inflationx.viewpump.ViewPump;
10 | import io.github.inflationx.viewpump.ViewPumpContextWrapper;
11 |
12 |
13 | public class MainActivity extends AppCompatActivity {
14 |
15 | private final ViewPump pump = ViewPump.builder()
16 | .addInterceptor(new TextUpdatingInterceptor())
17 | .addInterceptor(new CustomTextViewInterceptor())
18 | .build();
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_main);
24 | final Toolbar toolbar = findViewById(R.id.toolbar);
25 | setSupportActionBar(toolbar);
26 | toolbar.setTitle(R.string.app_name);
27 | }
28 |
29 | /*
30 | Uncomment if you disable PrivateFactory injection. See ViewPumpConfig#setPrivateFactoryInjectionEnabled(boolean)
31 | */
32 | // @Override
33 | // @TargetApi(Build.VERSION_CODES.HONEYCOMB)
34 | // public View onCreateView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
35 | // return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
36 | // }
37 |
38 | @Override
39 | protected void attachBaseContext(Context newBase) {
40 | // This is the new way
41 | super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase, pump));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/SampleApplication.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import android.app.Application;
4 |
5 | public class SampleApplication extends Application {
6 | }
7 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/SampleFragment.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import androidx.fragment.app.Fragment;
4 |
5 | public class SampleFragment extends Fragment {
6 |
7 | public SampleFragment() {
8 | super(R.layout.fragment_sample);
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/java/io/github/inflationx/viewpump/sample/TextUpdatingInterceptor.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.sample;
2 |
3 | import android.content.res.TypedArray;
4 |
5 | import io.github.inflationx.viewpump.InflateResult;
6 | import io.github.inflationx.viewpump.Interceptor;
7 |
8 | /**
9 | * This is an example of a post-inflation interceptor that modifies the properties of a view
10 | * after it has been created. Here we prefix the text for any view that has been replaced with
11 | * a custom version by the {@link CustomTextViewInterceptor}.
12 | */
13 | public class TextUpdatingInterceptor implements Interceptor {
14 |
15 | @Override
16 | public InflateResult intercept(Chain chain) {
17 | InflateResult result = chain.proceed(chain.request());
18 | if (result.view() instanceof CustomTextView) {
19 | CustomTextView textView = (CustomTextView) result.view();
20 |
21 | TypedArray a = result.context().obtainStyledAttributes(result.attrs(), new int[]{android.R.attr.text});
22 | try {
23 | CharSequence text = a.getText(0);
24 | if (text != null && text.length() > 0) {
25 | if (text.toString().startsWith("\n")) {
26 | text = text.toString().substring(1);
27 | }
28 | textView.setText(textView.getContext().getString(R.string.custom_textview_prefixed_text, text));
29 | }
30 | } finally {
31 | a.recycle();
32 | }
33 | }
34 | return result;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
23 |
24 |
28 |
29 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/res/layout/fragment_sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ViewPump
4 | \n[CustomTextView] %s
5 | This is a regular Button
6 | This is a regular TextView
7 |
8 |
--------------------------------------------------------------------------------
/ViewPumpSample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.jvm) apply false
3 | alias(libs.plugins.kotlin.android) apply false
4 | alias(libs.plugins.agp.application) apply false
5 | alias(libs.plugins.agp.library) apply false
6 | alias(libs.plugins.dokka) apply false
7 | }
8 |
9 | allprojects {
10 | version = project.findProperty("version")
11 | group = "io.github.inflationx"
12 |
13 | pluginManager.withPlugin("java") {
14 | java {
15 | toolchain {
16 | languageVersion.set(
17 | JavaLanguageVersion.of(libs.versions.jdk.get().removeSuffix("-ea").toInt())
18 | )
19 | }
20 | }
21 |
22 | tasks.withType(JavaCompile).configureEach {
23 | options.release.set(8)
24 | }
25 | }
26 |
27 | pluginManager.withPlugin("org.gradle.maven-publish") {
28 | tasks.register("publishRelease") {
29 | it.dependsOn("publishReleasePublicationToSonatypeRepository")
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M '-Dfile.encoding=UTF-8'
2 |
3 | version=2.1.2-SNAPSHOT
4 |
5 | android.useAndroidX=true
6 |
7 | # Kotlin code style
8 | kotlin.code.style=official
9 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "7.4.2"
3 | dokka = "1.8.10"
4 | jdk = "19"
5 | kotlin = "1.8.10"
6 | leakcanary = "2.10"
7 |
8 | [plugins]
9 | agp-application = { id = "com.android.application", version.ref = "agp" }
10 | agp-library = { id = "com.android.library", version.ref = "agp" }
11 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
12 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
13 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
14 |
15 | [libraries]
16 | androidx-annotation = "androidx.annotation:annotation:1.6.0"
17 | androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
18 | androidx-test-runner = "androidx.test:runner:1.5.2"
19 | assertj = "org.assertj:assertj-core:3.24.2"
20 | junit = "junit:junit:4.13.2"
21 | leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
22 | mockito = "org.mockito:mockito-core:4.11.0"
23 | dokka-android = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InflationX/ViewPump/bc406fb2d2ae061d68fe071f6159be5fce7e8ab8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | include ':viewpump'
17 | include ':ViewPumpSample'
18 |
--------------------------------------------------------------------------------
/viewpump/Module.md:
--------------------------------------------------------------------------------
1 | # Package io.github.inflationx.viewpump
2 |
3 | View inflation with pre/post-inflation interceptors.
4 |
--------------------------------------------------------------------------------
/viewpump/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5 |
6 | plugins {
7 | alias(libs.plugins.agp.library)
8 | alias(libs.plugins.kotlin.android)
9 | alias(libs.plugins.dokka)
10 | `maven-publish`
11 | signing
12 | }
13 |
14 | // Register dokka to generate javadoc
15 | val dokkaJavadoc = tasks.register("dokkaJavadocJar") {
16 | dependsOn(tasks.dokkaJavadoc)
17 | from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
18 | archiveClassifier.set("javadoc")
19 | }
20 |
21 | // Move to common place if more modules exist
22 | publishing {
23 | publications {
24 | create("release") {
25 | artifactId = "viewpump"
26 | afterEvaluate {
27 | from(components["release"])
28 | }
29 | artifacts {
30 | artifact(dokkaJavadoc)
31 | }
32 | pom {
33 | name.set("ViewPump")
34 | description.set("View inflation with pre/post-inflation interceptors")
35 | packaging = "aar"
36 | url.set("https://github.com/InflationX/ViewPump")
37 | scm {
38 | url.set("https://github.com/InflationX/ViewPump")
39 | connection.set("scm:git@github.com:InflationX/ViewPump.git")
40 | developerConnection.set("scm:git@github.com:InflationX/ViewPump.git")
41 | }
42 | licenses {
43 | license {
44 | name.set("The Apache Software License, Version 2.0")
45 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
46 | distribution.set("repo")
47 | }
48 | }
49 | developers {
50 | developer {
51 | id.set("InflationX")
52 | name.set("InflationX")
53 | }
54 | }
55 | }
56 | }
57 | }
58 | repositories {
59 | maven {
60 | name = "sonatype"
61 | val releaseRepoUrl = uri(
62 | "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
63 | )
64 | val snapshotRepoUrl = uri(
65 | "https://oss.sonatype.org/content/repositories/snapshots/"
66 | )
67 | url = if (version.toString().endsWith("SNAPSHOT")) snapshotRepoUrl else releaseRepoUrl
68 | credentials {
69 | username = propOrEnv("SONATYPE_USERNAME")
70 | password = propOrEnv("SONATYPE_PASSWORD")
71 | }
72 | }
73 | }
74 | }
75 |
76 | signing {
77 | sign(publishing.publications["release"])
78 | }
79 |
80 | tasks.withType().configureEach {
81 | onlyIf("Is not SNAPSHOT") { !version.toString().endsWith("SNAPSHOT") }
82 | }
83 |
84 |
85 | android {
86 | compileSdk = 33
87 | namespace = "io.github.inflationx.viewpump"
88 |
89 | defaultConfig {
90 | minSdk = 14
91 | }
92 |
93 | lint {
94 | // we don't always want to use the latest version of the support library
95 | disable.add("GradleDependency")
96 | textReport = (System.getenv("CI") == "true")
97 | }
98 |
99 | buildFeatures {
100 | buildConfig = false
101 | }
102 |
103 | compileOptions {
104 | sourceCompatibility = JavaVersion.VERSION_1_8
105 | targetCompatibility = JavaVersion.VERSION_1_8
106 | }
107 |
108 | publishing {
109 | singleVariant("release") {
110 | withSourcesJar()
111 | // withJavadocJar()
112 | }
113 | }
114 | }
115 |
116 | androidComponents {
117 | beforeVariants(selector().all()) { variant ->
118 | variant.enable = (variant.buildType == "release")
119 | }
120 | }
121 |
122 | tasks.withType(KotlinCompile::class.java).configureEach {
123 | compilerOptions {
124 | jvmTarget.set(JvmTarget.JVM_1_8)
125 | }
126 | }
127 |
128 | dependencies {
129 | compileOnly(libs.androidx.appcompat)
130 | dokkaPlugin(libs.dokka.android)
131 |
132 | testImplementation(libs.junit)
133 | testImplementation(libs.assertj)
134 | testImplementation(libs.mockito)
135 |
136 | testImplementation(libs.androidx.annotation)
137 | testImplementation(libs.androidx.test.runner)
138 | }
139 |
140 | fun propOrEnv(key: String): String {
141 | return System.getenv(key) ?: project.findProperty(key)?.toString() ?: ""
142 | }
143 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreator.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 |
7 | fun interface FallbackViewCreator {
8 | fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet?): View?
9 | }
10 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/InflateRequest.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 |
7 | data class InflateRequest(
8 | @get:JvmName("name")
9 | val name: String,
10 | @get:JvmName("context")
11 | val context: Context,
12 | @get:JvmName("attrs")
13 | val attrs: AttributeSet? = null,
14 | @get:JvmName("parent")
15 | val parent: View? = null,
16 | @get:JvmName("fallbackViewCreator")
17 | val fallbackViewCreator: FallbackViewCreator
18 | ) {
19 |
20 | fun toBuilder(): Builder {
21 | return Builder(this)
22 | }
23 |
24 | class Builder {
25 | private var name: String? = null
26 | private var context: Context? = null
27 | private var attrs: AttributeSet? = null
28 | private var parent: View? = null
29 | private var fallbackViewCreator: FallbackViewCreator? = null
30 |
31 | internal constructor()
32 |
33 | internal constructor(request: InflateRequest) {
34 | this.name = request.name
35 | this.context = request.context
36 | this.attrs = request.attrs
37 | this.parent = request.parent
38 | this.fallbackViewCreator = request.fallbackViewCreator
39 | }
40 |
41 | fun name(name: String) = apply {
42 | this.name = name
43 | }
44 |
45 | fun context(context: Context) = apply {
46 | this.context = context
47 | }
48 |
49 | fun attrs(attrs: AttributeSet?) = apply {
50 | this.attrs = attrs
51 | }
52 |
53 | fun parent(parent: View?) = apply {
54 | this.parent = parent
55 | }
56 |
57 | fun fallbackViewCreator(fallbackViewCreator: FallbackViewCreator) = apply {
58 | this.fallbackViewCreator = fallbackViewCreator
59 | }
60 |
61 | fun build() =
62 | InflateRequest(name = name ?: throw IllegalStateException("name == null"),
63 | context = context ?: throw IllegalStateException("context == null"),
64 | attrs = attrs,
65 | parent = parent,
66 | fallbackViewCreator = fallbackViewCreator ?: throw IllegalStateException("fallbackViewCreator == null")
67 | )
68 | }
69 |
70 | companion object {
71 |
72 | @JvmStatic
73 | fun builder(): Builder {
74 | return Builder()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/InflateResult.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 |
7 | data class InflateResult(
8 | @get:JvmName("view")
9 | val view: View? = null,
10 | @get:JvmName("name")
11 | val name: String,
12 | @get:JvmName("context")
13 | val context: Context,
14 | @get:JvmName("attrs")
15 | val attrs: AttributeSet? = null
16 | ) {
17 |
18 | fun toBuilder(): Builder {
19 | return Builder(this)
20 | }
21 |
22 | class Builder {
23 | private var view: View? = null
24 | private var name: String? = null
25 | private var context: Context? = null
26 | private var attrs: AttributeSet? = null
27 |
28 | internal constructor()
29 |
30 | internal constructor(result: InflateResult) {
31 | this.view = result.view
32 | this.name = result.name
33 | this.context = result.context
34 | this.attrs = result.attrs
35 | }
36 |
37 | fun view(view: View?) = apply {
38 | this.view = view
39 | }
40 |
41 | fun name(name: String) = apply {
42 | this.name = name
43 | }
44 |
45 | fun context(context: Context) = apply {
46 | this.context = context
47 | }
48 |
49 | fun attrs(attrs: AttributeSet?) = apply {
50 | this.attrs = attrs
51 | }
52 |
53 | fun build(): InflateResult {
54 | val finalName = checkNotNull(name) { "name == null" }
55 | return InflateResult(
56 | view = view?.also {
57 | check(finalName == it.javaClass.name) {
58 | "name ($finalName) must be the view's fully qualified name (${it.javaClass.name})"
59 | }
60 | },
61 | name = finalName,
62 | context = context ?: throw IllegalStateException("context == null"),
63 | attrs = attrs
64 | )
65 | }
66 | }
67 |
68 | companion object {
69 |
70 | @JvmStatic
71 | fun builder(): Builder {
72 | return Builder()
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/Interceptor.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | /**
4 | * Observes, modifies, and potentially short-circuits inflation requests going out and the
5 | * corresponding views that are inflated or returned. Typically interceptors change the name
6 | * of the view to be inflated, return a programmatically instantiated view, or perform actions
7 | * on a view after it is inflated based on its Context or AttributeSet.
8 | */
9 | interface Interceptor {
10 | fun intercept(chain: Chain): InflateResult
11 |
12 | companion object {
13 | // This lambda conversion is for Kotlin callers expecting a Java SAM (single-abstract-method).
14 | @JvmName("-deprecated_Interceptor")
15 | inline operator fun invoke(
16 | crossinline block: (chain: Chain) -> InflateResult
17 | ): Interceptor = object: Interceptor {
18 | override fun intercept(chain: Chain) = block(chain)
19 | }
20 | }
21 |
22 | interface Chain {
23 | fun request(): InflateRequest
24 |
25 | fun proceed(request: InflateRequest): InflateResult
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 | import io.github.inflationx.viewpump.Interceptor.Chain
7 | import io.github.inflationx.viewpump.ViewPump.Builder
8 | import io.github.inflationx.viewpump.internal.`-FallbackViewCreationInterceptor`
9 | import io.github.inflationx.viewpump.internal.`-InterceptorChain`
10 | import io.github.inflationx.viewpump.internal.`-ReflectiveFallbackViewCreator`
11 |
12 | class ViewPump private constructor(
13 | /** List of interceptors. */
14 | @get:JvmName("interceptors")
15 | val interceptors: List,
16 |
17 | /** Use Reflection to inject the private factory. */
18 | @get:JvmName("isReflection")
19 | val isReflection: Boolean,
20 |
21 | /** Use Reflection to intercept CustomView inflation with the correct Context. */
22 | @get:JvmName("isCustomViewCreation")
23 | val isCustomViewCreation: Boolean,
24 |
25 | /** Store the resourceId for the layout used to inflate the View in the View tag. */
26 | @get:JvmName("isStoreLayoutResId")
27 | val isStoreLayoutResId: Boolean
28 | ) {
29 |
30 | /** List that gets cleared and reused as it holds interceptors with the fallback added. */
31 | private val interceptorsWithFallback: List = (interceptors + `-FallbackViewCreationInterceptor`()).toMutableList()
32 |
33 | fun inflate(originalRequest: InflateRequest): InflateResult {
34 | val chain = `-InterceptorChain`(interceptorsWithFallback, 0,
35 | originalRequest)
36 | return chain.proceed(originalRequest)
37 | }
38 |
39 | /**
40 | * Allows for programmatic creation of Views via reflection on class name that are still
41 | * pre/post-processed by the inflation interceptors.
42 | *
43 | * @param context The context.
44 | * @param clazz The class of View to be created.
45 | * @return The processed view, which might not necessarily be the same type as clazz.
46 | */
47 | fun create(context: Context, clazz: Class, attrs: AttributeSet?): View? {
48 | return inflate(InflateRequest(
49 | context = context,
50 | name = clazz.name,
51 | attrs = attrs,
52 | fallbackViewCreator = reflectiveFallbackViewCreator
53 | ))
54 | .view
55 | }
56 |
57 | class Builder internal constructor() {
58 |
59 | /** List of interceptors. */
60 | private val interceptors = mutableListOf()
61 |
62 | /** Use Reflection to inject the private factory. Defaults to true. */
63 | private var reflection = true
64 |
65 | /** Use Reflection to intercept CustomView inflation with the correct Context. */
66 | private var customViewCreation = true
67 |
68 | /** Store the resourceId for the layout used to inflate the View in the View tag. */
69 | private var storeLayoutResId = false
70 |
71 | /** A FallbackViewCreator used to instantiate a view via reflection when using the create() API. */
72 | private var reflectiveFallbackViewCreator: FallbackViewCreator? = null
73 |
74 | fun addInterceptor(interceptor: Interceptor) = apply {
75 | interceptors.add(interceptor)
76 | }
77 |
78 | /**
79 | *
80 | * Turn of the use of Reflection to inject the private factory.
81 | * This has operational consequences! Please read and understand before disabling.
82 | *
83 | *
84 | * If you disable this you will need to override your [android.app.Activity.onCreateView]
85 | * as this is set as the [android.view.LayoutInflater] private factory.
86 | *
87 | * ** Use the following code in the Activity if you disable FactoryInjection:**
88 | *
89 | * ```
90 | * @Override
91 | * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
92 | * return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
93 | * }
94 | * ```
95 | *
96 | * ```
97 | * @Override
98 | * override fun onCreateView(parent: View, name: String, context: Context, attrs: AttributeSet): View {
99 | * return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs)
100 | * }
101 | * ```
102 | *
103 | * @param enabled True if private factory inject is allowed; otherwise, false.
104 | */
105 | fun setPrivateFactoryInjectionEnabled(enabled: Boolean) = apply {
106 | this.reflection = enabled
107 | }
108 |
109 | /**
110 | * Due to the poor inflation order where custom views are created and never returned inside an
111 | * `onCreateView(...)` method. We have to create CustomView's at the latest point in the
112 | * overrideable injection flow.
113 | *
114 | * On HoneyComb+ this is inside the [android.app.Activity.onCreateView]
115 | *
116 | * We wrap base implementations, so if you LayoutInflater/Factory/Activity creates the
117 | * custom view before we get to this point, your view is used. (Such is the case with the
118 | * TintEditText etc)
119 | *
120 | * The problem is, the native methods pass there parents context to the constructor in a really
121 | * specific place. We have to mimic this in [ViewPumpLayoutInflater.createCustomViewInternal]
122 | * To mimic this we have to use reflection as the Class constructor args are hidden to us.
123 | *
124 | * We have discussed other means of doing this but this is the only semi-clean way of doing it.
125 | * (Without having to do proxy classes etc).
126 | *
127 | * Calling this will of course speed up inflation by turning off reflection, but not by much,
128 | * But if you want ViewPump to inject the correct typeface then you will need to make sure your CustomView's
129 | * are created before reaching the LayoutInflater onViewCreated.
130 | *
131 | * @param enabled True if custom view inflated is allowed; otherwise, false.
132 | */
133 | fun setCustomViewInflationEnabled(enabled: Boolean) = apply {
134 | this.customViewCreation = enabled
135 | }
136 |
137 | fun setReflectiveFallbackViewCreator(
138 | reflectiveFallbackViewCreator: FallbackViewCreator) = apply {
139 | this.reflectiveFallbackViewCreator = reflectiveFallbackViewCreator
140 | }
141 |
142 | /**
143 | * The LayoutInflater can store the layout resourceId used to inflate a view into the inflated view's tag
144 | * where it can be later retrieved by an interceptor.
145 | *
146 | * @param enabled True if the view should store the resId; otherwise, false.
147 | */
148 | fun setStoreLayoutResId(enabled: Boolean) = apply {
149 | this.storeLayoutResId = enabled
150 | }
151 |
152 | fun build(): ViewPump {
153 | return ViewPump(
154 | interceptors = interceptors.toList(),
155 | isReflection = reflection,
156 | isCustomViewCreation = customViewCreation,
157 | isStoreLayoutResId = storeLayoutResId
158 | )
159 | }
160 | }
161 |
162 | companion object {
163 |
164 | private var INSTANCE: ViewPump? = null
165 |
166 | /** A FallbackViewCreator used to instantiate a view via reflection when using the create() API. */
167 | private val reflectiveFallbackViewCreator: FallbackViewCreator by lazy {
168 | `-ReflectiveFallbackViewCreator`()
169 | }
170 |
171 | @Deprecated(
172 | "Global singletons are bad for testing, scoping, and composition. Use local ViewPump instances instead.",
173 | level = DeprecationLevel.ERROR
174 | )
175 | @JvmStatic
176 | fun init(viewPump: ViewPump) {
177 | INSTANCE = viewPump
178 | }
179 |
180 | @Deprecated(
181 | "Global singletons are bad for testing, scoping, and composition. Use local ViewPump instances instead.",
182 | level = DeprecationLevel.ERROR
183 | )
184 | @JvmStatic
185 | fun get(): ViewPump {
186 | return INSTANCE ?: builder().build().also { INSTANCE = it }
187 | }
188 |
189 | @Deprecated(
190 | "Global singletons are bad for testing, scoping, and composition. Use local ViewPump instances instead.",
191 | level = DeprecationLevel.ERROR
192 | )
193 | @JvmStatic
194 | fun reset() {
195 | INSTANCE = null
196 | }
197 |
198 | @Deprecated("This no longer works!", level = DeprecationLevel.ERROR)
199 | @JvmStatic
200 | fun create(context: Context, clazz: Class): View? {
201 | error("This no longer works, use the overload that takes an AttributeSet!")
202 | }
203 |
204 | @Deprecated("Global singletons are bad for testing, scoping, and composition. Use local ViewPump instances instead.")
205 | @JvmName("staticCreateDeprecated")
206 | @JvmStatic
207 | fun create(context: Context, clazz: Class, attrs: AttributeSet?): View? {
208 | @Suppress("DEPRECATION_ERROR")
209 | return get()
210 | .inflate(InflateRequest(
211 | context = context,
212 | name = clazz.name,
213 | attrs = attrs,
214 | fallbackViewCreator = reflectiveFallbackViewCreator
215 | ))
216 | .view
217 | }
218 |
219 | @JvmStatic
220 | fun builder(): Builder {
221 | return Builder()
222 | }
223 | }
224 | }
225 |
226 | /**
227 | * Adds an [Interceptor] to this current [Builder] for idiomatic Kotlin higher order function usage.
228 | */
229 | inline fun Builder.addInterceptor(crossinline block: (chain: Chain) -> InflateResult) = apply {
230 | addInterceptor(Interceptor.invoke(block))
231 | }
232 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpContextWrapper.kt:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.ContextWrapper
6 | import android.util.AttributeSet
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import io.github.inflationx.viewpump.internal.`-ViewPumpActivityFactory`
10 | import io.github.inflationx.viewpump.internal.`-ViewPumpLayoutInflater`
11 | import kotlin.LazyThreadSafetyMode.NONE
12 |
13 | /**
14 | * Uses the default configuration from [ViewPump]
15 | *
16 | * Remember if you are defining default in the
17 | * [ViewPump] make sure this is initialised before
18 | * the activity is created.
19 | *
20 | * @param base ContextBase to Wrap
21 | */
22 | class ViewPumpContextWrapper private constructor(
23 | base: Context,
24 | private val viewPump: ViewPump,
25 | ) : ContextWrapper(base) {
26 |
27 | private val inflater: `-ViewPumpLayoutInflater` by lazy(NONE) {
28 | `-ViewPumpLayoutInflater`(
29 | viewPump = viewPump,
30 | original = LayoutInflater.from(baseContext),
31 | newContext = this,
32 | cloned = false
33 | )
34 | }
35 |
36 | override fun getSystemService(name: String): Any? {
37 | if (Context.LAYOUT_INFLATER_SERVICE == name) {
38 | return inflater
39 | }
40 | return super.getSystemService(name)
41 | }
42 |
43 | companion object {
44 |
45 | @Deprecated(
46 | message = "Global singletons are bad for testing, scoping, and composition. Use local ViewPump instances instead.",
47 | replaceWith = ReplaceWith("wrap(base, viewPump)"),
48 | level = DeprecationLevel.ERROR
49 | )
50 | @JvmStatic
51 | fun wrap(base: Context): ContextWrapper {
52 | @Suppress("DEPRECATION_ERROR")
53 | return wrap(base, ViewPump.get())
54 | }
55 |
56 | /**
57 | * Uses the default configuration from [ViewPump]
58 | *
59 | * Remember if you are defining default in the [ViewPump] make sure this
60 | * is initialised before the activity is created.
61 | *
62 | * @param base ContextBase to Wrap.
63 | * @return ContextWrapper to pass back to the activity.
64 | */
65 | @JvmStatic
66 | fun wrap(base: Context, viewPump: ViewPump): ContextWrapper {
67 | return ViewPumpContextWrapper(base, viewPump)
68 | }
69 |
70 | /**
71 | * You only need to call this **IF** you disable
72 | * [ViewPump.Builder.setPrivateFactoryInjectionEnabled]
73 | * This will need to be called from the
74 | * [Activity.onCreateView]
75 | * method to enable view font injection if the view is created inside the activity onCreateView.
76 | *
77 | * You would implement this method like so in you base activity.
78 | *
79 | * ```
80 | * @Override
81 | * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
82 | * return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
83 | * }
84 | * ```
85 | *
86 | * ```
87 | * override fun onCreateView(parent: View, name: String, context: Context, attrs: AttributeSet): View {
88 | * return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs)
89 | * }
90 | * ```
91 | *
92 | * @param activity The activity the original that the ContextWrapper was attached too.
93 | * @param parent Parent view from onCreateView
94 | * @param view The View Created inside onCreateView or from super.onCreateView
95 | * @param name The View name from onCreateView
96 | * @param context The context from onCreateView
97 | * @param attr The AttributeSet from onCreateView
98 | * @return The same view passed in, or null if null passed in.
99 | */
100 | @JvmStatic
101 | fun onActivityCreateView(
102 | activity: Activity, parent: View?, view: View, name: String,
103 | context: Context, attr: AttributeSet,
104 | ): View? {
105 | return get(activity).onActivityCreateView(parent, view, name, context, attr)
106 | }
107 |
108 | /**
109 | * Get the ViewPump Activity Fragment Instance to allow callbacks for when views are created.
110 | *
111 | * @param activity The activity the original that the ContextWrapper was attached too.
112 | * @return Interface allowing you to call onActivityViewCreated
113 | */
114 | @JvmStatic
115 | internal fun get(activity: Activity): `-ViewPumpActivityFactory` {
116 | if (activity.layoutInflater !is `-ViewPumpLayoutInflater`) {
117 | throw RuntimeException(
118 | "This activity does not wrap the Base Context! See ViewPumpContextWrapper.wrap(Context)"
119 | )
120 | }
121 | return activity.layoutInflater as `-ViewPumpActivityFactory`
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-FallbackViewCreationInterceptor.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-FallbackViewCreationInterceptor")
2 | package io.github.inflationx.viewpump.internal
3 |
4 | import io.github.inflationx.viewpump.InflateResult
5 | import io.github.inflationx.viewpump.Interceptor
6 | import io.github.inflationx.viewpump.Interceptor.Chain
7 |
8 | @Suppress("ClassName")
9 | internal class `-FallbackViewCreationInterceptor` : Interceptor {
10 |
11 | override fun intercept(chain: Chain): InflateResult {
12 | val request = chain.request()
13 | val viewCreator = request.fallbackViewCreator
14 | val fallbackView = viewCreator.onCreateView(request.parent, request.name, request.context,
15 | checkNotNull(request.attrs)
16 | )
17 |
18 | return InflateResult(
19 | view = fallbackView,
20 | name = fallbackView?.javaClass?.name ?: request.name,
21 | context = request.context,
22 | attrs = request.attrs
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-InterceptorChain.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-InterceptorChain")
2 | package io.github.inflationx.viewpump.internal
3 |
4 | import io.github.inflationx.viewpump.InflateRequest
5 | import io.github.inflationx.viewpump.InflateResult
6 | import io.github.inflationx.viewpump.Interceptor
7 | import io.github.inflationx.viewpump.Interceptor.Chain
8 |
9 | /**
10 | * A concrete interceptor chain that carries the entire interceptor chain.
11 | */
12 | @Suppress("ClassName")
13 | internal class `-InterceptorChain`(private val interceptors: List, private val index: Int,
14 | private val request: InflateRequest) : Chain {
15 |
16 | override fun request(): InflateRequest {
17 | return request
18 | }
19 |
20 | override fun proceed(request: InflateRequest): InflateResult {
21 | if (index >= interceptors.size) {
22 | throw AssertionError("no interceptors added to the chain")
23 | }
24 |
25 | // Call the next interceptor in the chain.
26 | val next = `-InterceptorChain`(interceptors, index + 1,
27 | request)
28 | val interceptor = interceptors[index]
29 |
30 | return interceptor.intercept(next)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-ReflectionUtils.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-ReflectionUtils")
2 | package io.github.inflationx.viewpump.internal
3 |
4 | import android.util.Log
5 |
6 | import java.lang.reflect.Field
7 | import java.lang.reflect.InvocationTargetException
8 | import java.lang.reflect.Method
9 |
10 | private const val TAG = "ReflectionUtils"
11 |
12 | internal fun Field.setValueQuietly(obj: Any, value: Any) {
13 | try {
14 | set(obj, value)
15 | } catch (ignored: IllegalAccessException) {
16 | }
17 | }
18 |
19 | internal fun Class<*>.getAccessibleMethod(methodName: String): Method? {
20 | val methods = methods
21 | for (method in methods) {
22 | if (method.name == methodName) {
23 | method.isAccessible = true
24 | return method
25 | }
26 | }
27 | return null
28 | }
29 |
30 | internal fun Method?.invokeMethod(target: Any, vararg args: Any) {
31 | if (this == null) {
32 | return
33 | }
34 | try {
35 | invoke(target, *args)
36 | } catch (e: IllegalAccessException) {
37 | Log.d(TAG, "Can't access method using reflection", e)
38 | } catch (e: InvocationTargetException) {
39 | Log.d(TAG, "Can't invoke method using reflection", e)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-ReflectiveFallbackViewCreator.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-ReflectiveFallbackViewCreator")
2 | package io.github.inflationx.viewpump.internal
3 |
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import android.view.View
7 | import io.github.inflationx.viewpump.FallbackViewCreator
8 |
9 | import java.lang.reflect.Constructor
10 | import java.lang.reflect.InvocationTargetException
11 |
12 | @Suppress("ClassName")
13 | internal class `-ReflectiveFallbackViewCreator` : FallbackViewCreator {
14 | companion object {
15 | private val CONSTRUCTOR_SIGNATURE_1: Array> = arrayOf(Context::class.java)
16 | private val CONSTRUCTOR_SIGNATURE_2: Array> = arrayOf(Context::class.java, AttributeSet::class.java)
17 | }
18 |
19 | override fun onCreateView(parent: View?, name: String, context: Context,
20 | attrs: AttributeSet?): View? {
21 | try {
22 | val clazz = Class.forName(name).asSubclass(View::class.java)
23 | var constructor: Constructor
24 | var constructorArgs: Array
25 | try {
26 | constructor = clazz.getConstructor(*CONSTRUCTOR_SIGNATURE_2)
27 | constructorArgs = arrayOf(context, attrs)
28 | } catch (e: NoSuchMethodException) {
29 | constructor = clazz.getConstructor(*CONSTRUCTOR_SIGNATURE_1)
30 | constructorArgs = arrayOf(context)
31 | }
32 |
33 | constructor.isAccessible = true
34 | return constructor.newInstance(*constructorArgs)
35 | } catch(e: Exception) {
36 | when (e) {
37 | is ClassNotFoundException -> {
38 | e.printStackTrace()
39 | }
40 | is NoSuchMethodException -> {
41 | e.printStackTrace()
42 | }
43 | is IllegalAccessException -> {
44 | e.printStackTrace()
45 | }
46 | is InstantiationException -> {
47 | e.printStackTrace()
48 | }
49 | is InvocationTargetException -> {
50 | e.printStackTrace()
51 | }
52 | else -> throw e
53 | }
54 | }
55 |
56 | return null
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-ViewPumpActivityFactory.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-ViewPumpActivityFactory")
2 | package io.github.inflationx.viewpump.internal
3 |
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import android.view.View
7 |
8 | @Suppress("ClassName")
9 | internal interface `-ViewPumpActivityFactory` {
10 |
11 | /**
12 | * Used to Wrap the Activity onCreateView method.
13 | *
14 | * You implement this method like so in you base activity.
15 | *
16 | * ```
17 | * @Override
18 | * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
19 | * return ViewPumpContextWrapper.get(getBaseContext()).onActivityCreateView(super.onCreateView(parent, name, context, attrs), attrs);
20 | * }
21 | * ```
22 | *
23 | * ```
24 | * override fun onCreateView(parent: View, name: String, context: Context, attrs: AttributeSet): View {
25 | * return ViewPumpContextWrapper.get(getBaseContext()).onActivityCreateView(super.onCreateView(parent, name, context, attrs), attrs)
26 | * }
27 | * ```
28 | *
29 | * @param parent parent view, can be null.
30 | * @param view result of `super.onCreateView(parent, name, context, attrs)`, this might be null, which is fine.
31 | * @param name Name of View we are trying to inflate
32 | * @param context current context (normally the Activity's)
33 | * @param attrs see [android.view.LayoutInflater.Factory2.onCreateView] @return the result from the activities `onCreateView()`
34 | * @return The view passed in, or null if nothing was passed in.
35 | * @see android.view.LayoutInflater.Factory2
36 | */
37 | fun onActivityCreateView(parent: View?, view: View, name: String, context: Context,
38 | attrs: AttributeSet?): View?
39 | }
40 |
--------------------------------------------------------------------------------
/viewpump/src/main/java/io/github/inflationx/viewpump/internal/-ViewPumpLayoutInflater.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("-ViewPumpLayoutInflater")
2 |
3 | package io.github.inflationx.viewpump.internal
4 |
5 | import android.content.Context
6 | import android.os.Build
7 | import android.util.AttributeSet
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import androidx.annotation.ChecksSdkIntAtLeast
12 | import io.github.inflationx.viewpump.FallbackViewCreator
13 | import io.github.inflationx.viewpump.InflateRequest
14 | import io.github.inflationx.viewpump.R.id
15 | import io.github.inflationx.viewpump.ViewPump
16 | import org.xmlpull.v1.XmlPullParser
17 | import java.lang.reflect.Field
18 |
19 | @Suppress("ClassName")
20 | internal class `-ViewPumpLayoutInflater`(
21 | private val viewPump: ViewPump,
22 | original: LayoutInflater,
23 | newContext: Context,
24 | cloned: Boolean,
25 | ) : LayoutInflater(original, newContext), `-ViewPumpActivityFactory` {
26 |
27 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
28 | private val isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
29 |
30 | private val nameAndAttrsViewCreator: FallbackViewCreator = NameAndAttrsViewCreator(this)
31 | private val parentAndNameAndAttrsViewCreator: FallbackViewCreator =
32 | ParentAndNameAndAttrsViewCreator(this)
33 |
34 | // Reflection Hax
35 | private var setPrivateFactory = false
36 |
37 | private var storeLayoutResId = viewPump.isStoreLayoutResId
38 |
39 | init {
40 | setUpLayoutFactories(cloned)
41 | }
42 |
43 | /**
44 | * We use this for internal cloning to be a little more efficient with memory.
45 | */
46 | internal fun internalCloneInContext(newContext: Context): LayoutInflater {
47 | // Same context, use the same inflater
48 | if (newContext == context) return this
49 | return cloneInContext(newContext)
50 | }
51 |
52 | override fun cloneInContext(newContext: Context): LayoutInflater {
53 | return `-ViewPumpLayoutInflater`(viewPump, this, newContext, true)
54 | }
55 |
56 | // ===
57 | // Wrapping goodies
58 | // ===
59 |
60 | override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View? {
61 | val view = super.inflate(resource, root, attachToRoot)
62 | if (view != null && storeLayoutResId) {
63 | view.setTag(id.viewpump_layout_res, resource)
64 | }
65 | return view
66 | }
67 |
68 | override fun inflate(parser: XmlPullParser, root: ViewGroup?, attachToRoot: Boolean): View {
69 | setPrivateFactoryInternal()
70 | return super.inflate(parser, root, attachToRoot)
71 | }
72 |
73 | /**
74 | * We don't want to unnecessary create/set our factories if there are none there. We try to be
75 | * as lazy as possible.
76 | */
77 | private fun setUpLayoutFactories(cloned: Boolean) {
78 | if (cloned) return
79 | // If we are HC+ we get and set Factory2 otherwise we just wrap Factory1
80 | if (factory2 != null && factory2 !is WrapperFactory2) {
81 | // Sets both Factory/Factory2
82 | factory2 = factory2
83 | }
84 | // We can do this as setFactory2 is used for both methods.
85 | if (factory != null && factory !is WrapperFactory) {
86 | factory = factory
87 | }
88 | }
89 |
90 | override fun setFactory(factory: Factory) {
91 | // Only set our factory and wrap calls to the Factory trying to be set!
92 | if (factory !is WrapperFactory) {
93 | super.setFactory(
94 | WrapperFactory(factory, viewPump)
95 | )
96 | } else {
97 | super.setFactory(factory)
98 | }
99 | }
100 |
101 | override fun setFactory2(factory2: Factory2) {
102 | // Only set our factory and wrap calls to the Factory2 trying to be set!
103 | if (factory2 !is WrapperFactory2) {
104 | // LayoutInflaterCompat.setFactory(this, new WrapperFactory2(factory2, mViewPumpFactory));
105 | super.setFactory2(
106 | WrapperFactory2(factory2, viewPump)
107 | )
108 | } else {
109 | super.setFactory2(factory2)
110 | }
111 | }
112 |
113 | private fun setPrivateFactoryInternal() {
114 | // Already tried to set the factory.
115 | if (setPrivateFactory) return
116 | // Reflection (Or Old Device) skip.
117 | if (!viewPump.isReflection) return
118 | // Skip if not attached to an activity.
119 | if (context !is Factory2) {
120 | setPrivateFactory = true
121 | return
122 | }
123 |
124 | // TODO: we need to get this and wrap it if something has already set this
125 | val setPrivateFactoryMethod =
126 | LayoutInflater::class.java.getAccessibleMethod("setPrivateFactory")
127 |
128 | setPrivateFactoryMethod.invokeMethod(
129 | this,
130 | PrivateWrapperFactory2(context as Factory2, viewPump, this)
131 | )
132 | setPrivateFactory = true
133 | }
134 |
135 | // ===
136 | // LayoutInflater ViewCreators
137 | // Works in order of inflation
138 | // ===
139 |
140 | /**
141 | * The Activity onCreateView (PrivateFactory) is the third port of call for LayoutInflation.
142 | * We opted to manual injection over aggressive reflection, this should be less fragile.
143 | */
144 | override fun onActivityCreateView(
145 | parent: View?,
146 | view: View,
147 | name: String,
148 | context: Context,
149 | attrs: AttributeSet?
150 | ): View? {
151 | return viewPump
152 | .inflate(
153 | InflateRequest(
154 | name = name,
155 | context = context,
156 | attrs = attrs,
157 | parent = parent,
158 | fallbackViewCreator = ActivityViewCreator(
159 | this, view
160 | )
161 | )
162 | )
163 | .view
164 | }
165 |
166 | /**
167 | * The LayoutInflater onCreateView is the fourth port of call for LayoutInflation.
168 | * BUT only for none CustomViews.
169 | */
170 | @Throws(ClassNotFoundException::class)
171 | override fun onCreateView(parent: View?, name: String, attrs: AttributeSet?): View? {
172 | return viewPump
173 | .inflate(
174 | InflateRequest(
175 | name = name,
176 | context = context,
177 | attrs = attrs,
178 | parent = parent,
179 | fallbackViewCreator = parentAndNameAndAttrsViewCreator
180 | )
181 | )
182 | .view
183 | }
184 |
185 | /**
186 | * The LayoutInflater onCreateView is the fourth port of call for LayoutInflation.
187 | * BUT only for none CustomViews.
188 | * Basically if this method doesn't inflate the View nothing probably will.
189 | */
190 | @Throws(ClassNotFoundException::class)
191 | override fun onCreateView(name: String, attrs: AttributeSet?): View? {
192 | return viewPump
193 | .inflate(
194 | InflateRequest(
195 | name = name,
196 | context = context,
197 | attrs = attrs,
198 | fallbackViewCreator = nameAndAttrsViewCreator
199 | )
200 | )
201 | .view
202 | }
203 |
204 | /**
205 | * Nasty method to inflate custom layouts that haven't been handled else where. If this fails it
206 | * will fall back through to the PhoneLayoutInflater method of inflating custom views where
207 | * ViewPump will NOT have a hook into.
208 | *
209 | * @param view view if it has been inflated by this point, if this is not null this method
210 | * just returns this value.
211 | * @param name name of the thing to inflate.
212 | * @param viewContext Context to inflate by if parent is null
213 | * @param attrs Attr for this view which we can steal fontPath from too.
214 | * @return view or the View we inflate in here.
215 | */
216 | private fun createCustomViewInternal(
217 | view: View?,
218 | name: String,
219 | viewContext: Context,
220 | attrs: AttributeSet?
221 | ): View? {
222 | var mutableView = view
223 | // I by no means advise anyone to do this normally, but Google have locked down access to
224 | // the createView() method, so we never get a callback with attributes at the end of the
225 | // createViewFromTag chain (which would solve all this unnecessary rubbish).
226 | // We at the very least try to optimise this as much as possible.
227 | // We only call for customViews (As they are the ones that never go through onCreateView(...)).
228 | // We also maintain the Field reference and make it accessible which will make a pretty
229 | // significant difference to performance on Android 4.0+.
230 |
231 | // If CustomViewCreation is off skip this.
232 | if (!viewPump.isCustomViewCreation) return mutableView
233 | if (mutableView == null && name.indexOf('.') > -1) {
234 | if (isAtLeastQ) {
235 | mutableView = internalCloneInContext(viewContext).createView(name, null, attrs)
236 | } else {
237 | @Suppress("UNCHECKED_CAST")
238 | val constructorArgsArr = CONSTRUCTOR_ARGS_FIELD.get(this) as Array
239 | val lastContext = constructorArgsArr[0]
240 | // The LayoutInflater actually finds out the correct context to use. We just need to set
241 | // it on the mConstructor for the internal method.
242 | // Set the constructor ars up for the createView, not sure why we can't pass these in.
243 | constructorArgsArr[0] = viewContext
244 | CONSTRUCTOR_ARGS_FIELD.setValueQuietly(this, constructorArgsArr)
245 | try {
246 | mutableView = createView(name, null, attrs)
247 | } catch (ignored: ClassNotFoundException) {
248 | } finally {
249 | constructorArgsArr[0] = lastContext
250 | CONSTRUCTOR_ARGS_FIELD.setValueQuietly(this, constructorArgsArr)
251 | }
252 | }
253 | }
254 | return mutableView
255 | }
256 |
257 | private fun superOnCreateView(parent: View?, name: String, attrs: AttributeSet?): View? {
258 | return try {
259 | super.onCreateView(parent, name, attrs)
260 | } catch (e: ClassNotFoundException) {
261 | null
262 | }
263 | }
264 |
265 | private fun superOnCreateView(name: String, attrs: AttributeSet?): View? {
266 | return try {
267 | super.onCreateView(name, attrs)
268 | } catch (e: ClassNotFoundException) {
269 | null
270 | }
271 | }
272 |
273 | // ===
274 | // View creators
275 | // ===
276 |
277 | private class ActivityViewCreator(
278 | private val inflater: `-ViewPumpLayoutInflater`,
279 | private val view: View
280 | ) : FallbackViewCreator {
281 |
282 | override fun onCreateView(
283 | parent: View?,
284 | name: String,
285 | context: Context,
286 | attrs: AttributeSet?
287 | ): View? {
288 | return inflater.createCustomViewInternal(view, name, context, attrs)
289 | }
290 | }
291 |
292 | private class ParentAndNameAndAttrsViewCreator(
293 | private val inflater: `-ViewPumpLayoutInflater`
294 | ) : FallbackViewCreator {
295 |
296 | override fun onCreateView(
297 | parent: View?, name: String, context: Context,
298 | attrs: AttributeSet?
299 | ): View? {
300 | return inflater.superOnCreateView(parent, name, attrs)
301 | }
302 | }
303 |
304 | private class NameAndAttrsViewCreator(
305 | private val inflater: `-ViewPumpLayoutInflater`
306 | ) : FallbackViewCreator {
307 |
308 | override fun onCreateView(
309 | parent: View?,
310 | name: String,
311 | context: Context,
312 | attrs: AttributeSet?
313 | ): View? {
314 | // This mimics the {@code PhoneLayoutInflater} in the way it tries to inflate the base
315 | // classes, if this fails its pretty certain the app will fail at this point.
316 | var view: View? = null
317 | for (prefix in CLASS_PREFIX_LIST) {
318 | try {
319 | view = inflater.createView(name, prefix, attrs)
320 | if (view != null) {
321 | break
322 | }
323 | } catch (ignored: ClassNotFoundException) {
324 | }
325 | }
326 | // In this case we want to let the base class take a crack
327 | // at it.
328 | if (view == null) view = inflater.superOnCreateView(name, attrs)
329 | return view
330 | }
331 | }
332 |
333 | // ===
334 | // Wrapper Factories
335 | // ===
336 |
337 | /**
338 | * Factory 1 is the first port of call for LayoutInflation
339 | */
340 | private class WrapperFactory(
341 | factory: Factory,
342 | private val viewPump: ViewPump,
343 | ) : Factory {
344 |
345 | private val viewCreator: FallbackViewCreator = WrapperFactoryViewCreator(factory)
346 |
347 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
348 | return viewPump
349 | .inflate(
350 | InflateRequest(
351 | name = name,
352 | context = context,
353 | attrs = attrs,
354 | fallbackViewCreator = viewCreator
355 | )
356 | )
357 | .view
358 | }
359 | }
360 |
361 | private class WrapperFactoryViewCreator(
362 | private val factory: Factory
363 | ) : FallbackViewCreator {
364 |
365 | override fun onCreateView(
366 | parent: View?,
367 | name: String,
368 | context: Context,
369 | attrs: AttributeSet?
370 | ): View? {
371 | return attrs?.let { factory.onCreateView(name, context, it) }
372 | }
373 | }
374 |
375 | /**
376 | * Factory 2 is the second port of call for LayoutInflation
377 | */
378 | private open class WrapperFactory2(
379 | factory2: Factory2,
380 | private val viewPump: ViewPump,
381 | ) : Factory2 {
382 | private val viewCreator = WrapperFactory2ViewCreator(factory2)
383 |
384 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
385 | return onCreateView(null, name, context, attrs)
386 | }
387 |
388 | override fun onCreateView(
389 | parent: View?,
390 | name: String,
391 | context: Context,
392 | attrs: AttributeSet
393 | ): View? {
394 | return viewPump
395 | .inflate(
396 | InflateRequest(
397 | name = name,
398 | context = context,
399 | attrs = attrs,
400 | parent = parent,
401 | fallbackViewCreator = viewCreator
402 | )
403 | )
404 | .view
405 | }
406 | }
407 |
408 | private open class WrapperFactory2ViewCreator(
409 | protected val factory2: Factory2
410 | ) : FallbackViewCreator {
411 |
412 | override fun onCreateView(
413 | parent: View?,
414 | name: String,
415 | context: Context,
416 | attrs: AttributeSet?
417 | ): View? {
418 | return attrs?.let { factory2.onCreateView(parent, name, context, it) }
419 | }
420 | }
421 |
422 | /**
423 | * Private factory is step three for Activity Inflation, this is what is attached to the Activity
424 | */
425 | private class PrivateWrapperFactory2(
426 | factory2: Factory2,
427 | private val viewPump: ViewPump,
428 | inflater: `-ViewPumpLayoutInflater`
429 | ) : WrapperFactory2(factory2, viewPump) {
430 |
431 | private val viewCreator = PrivateWrapperFactory2ViewCreator(factory2, inflater)
432 |
433 | override fun onCreateView(
434 | parent: View?,
435 | name: String,
436 | context: Context,
437 | attrs: AttributeSet
438 | ): View? {
439 | return viewPump
440 | .inflate(
441 | InflateRequest(
442 | name = name,
443 | context = context,
444 | attrs = attrs,
445 | parent = parent,
446 | fallbackViewCreator = viewCreator
447 | )
448 | )
449 | .view
450 | }
451 | }
452 |
453 | private class PrivateWrapperFactory2ViewCreator(
454 | factory2: Factory2,
455 | private val inflater: `-ViewPumpLayoutInflater`
456 | ) : WrapperFactory2ViewCreator(factory2), FallbackViewCreator {
457 |
458 | override fun onCreateView(
459 | parent: View?,
460 | name: String,
461 | context: Context,
462 | attrs: AttributeSet?
463 | ): View? {
464 | return inflater.createCustomViewInternal(
465 | factory2.onCreateView(parent, name, context, checkNotNull(attrs) { "Should never happen!" }), name, context, attrs
466 | )
467 | }
468 | }
469 |
470 | companion object {
471 | private val CLASS_PREFIX_LIST = setOf("android.widget.", "android.webkit.")
472 | private val CONSTRUCTOR_ARGS_FIELD: Field by lazy {
473 | requireNotNull(LayoutInflater::class.java.getDeclaredField("mConstructorArgs")) {
474 | "No constructor arguments field found in LayoutInflater!"
475 | }.apply { isAccessible = true }
476 | }
477 | }
478 |
479 | }
480 |
--------------------------------------------------------------------------------
/viewpump/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/test/ViewPumpTest.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.test;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 |
7 | import org.junit.Rule;
8 | import org.junit.Test;
9 | import org.mockito.Mock;
10 | import org.mockito.junit.MockitoJUnit;
11 | import org.mockito.junit.MockitoRule;
12 |
13 | import io.github.inflationx.viewpump.FallbackViewCreator;
14 | import io.github.inflationx.viewpump.InflateRequest;
15 | import io.github.inflationx.viewpump.InflateResult;
16 | import io.github.inflationx.viewpump.Interceptor;
17 | import io.github.inflationx.viewpump.ViewPump;
18 | import io.github.inflationx.viewpump.util.AnotherTestView;
19 | import io.github.inflationx.viewpump.util.AnotherTestViewNewingPreInflationInterceptor;
20 | import io.github.inflationx.viewpump.util.NameChangingPreInflationInterceptor;
21 | import io.github.inflationx.viewpump.util.SingleConstructorTestView;
22 | import io.github.inflationx.viewpump.util.TestFallbackViewCreator;
23 | import io.github.inflationx.viewpump.util.TestPostInflationInterceptor;
24 | import io.github.inflationx.viewpump.util.TestView;
25 |
26 | import static org.assertj.core.api.Assertions.assertThat;
27 | import static org.mockito.ArgumentMatchers.eq;
28 | import static org.mockito.ArgumentMatchers.nullable;
29 | import static org.mockito.Mockito.mock;
30 | import static org.mockito.Mockito.verify;
31 | import static org.mockito.Mockito.verifyNoInteractions;
32 | import static org.mockito.Mockito.when;
33 | import static org.mockito.quality.Strictness.STRICT_STUBS;
34 |
35 | public class ViewPumpTest {
36 |
37 | @Rule
38 | public MockitoRule initRule = MockitoJUnit.rule().strictness(STRICT_STUBS);
39 |
40 | @Mock Context mockContext;
41 | @Mock AttributeSet mockAttrs;
42 | @Mock View mockParentView;
43 |
44 | private ViewPump testPump() {
45 | return ViewPump.builder().build();
46 | }
47 |
48 | /** @noinspection deprecation*/
49 | @Test
50 | public void uninitViewPump_shouldProvideDefaultInstance() {
51 | assertThat(ViewPump.get()).isNotNull();
52 | }
53 |
54 | /** @noinspection deprecation*/
55 | @Test
56 | public void initViewPump_shouldProvideConfiguredInstance() {
57 | ViewPump viewPump = ViewPump.builder().build();
58 | ViewPump.init(viewPump);
59 |
60 | assertThat(ViewPump.get())
61 | .isNotNull()
62 | .isSameAs(viewPump);
63 | }
64 |
65 | @Test
66 | public void request_withRequiredParams_shouldReturnView() {
67 | InflateResult result = testPump().inflate(InflateRequest.builder()
68 | .name(TestView.NAME)
69 | .context(mockContext)
70 | .attrs(mockAttrs)
71 | .fallbackViewCreator(new TestFallbackViewCreator())
72 | .build());
73 |
74 | assertThat(result).isNotNull();
75 | assertThat(result.name()).isEqualTo(TestView.NAME);
76 | assertThat(result.context()).isSameAs(mockContext);
77 | assertThat(result.view())
78 | .isNotNull()
79 | .isInstanceOf(TestView.class);
80 | }
81 |
82 | @Test
83 | public void request_withAdditionalParams_shouldReturnView() {
84 | InflateResult result = testPump().inflate(InflateRequest.builder()
85 | .name(TestView.NAME)
86 | .context(mockContext)
87 | .attrs(mockAttrs)
88 | .parent(mockParentView)
89 | .fallbackViewCreator(new TestFallbackViewCreator())
90 | .build());
91 |
92 | assertThat(result).isNotNull();
93 | assertThat(result.name()).isEqualTo(TestView.NAME);
94 | assertThat(result.context()).isSameAs(mockContext);
95 | assertThat(result.view())
96 | .isNotNull()
97 | .isInstanceOf(TestView.class);
98 | }
99 |
100 | @Test
101 | public void request_withInflatedNameChangeInterceptor_shouldReturnViewWithNewName() {
102 | ViewPump pump = ViewPump.builder()
103 | .addInterceptor(new NameChangingPreInflationInterceptor())
104 | .build();
105 |
106 | InflateResult result = pump.inflate(InflateRequest.builder()
107 | .name(TestView.NAME)
108 | .context(mockContext)
109 | .attrs(mockAttrs)
110 | .fallbackViewCreator(new TestFallbackViewCreator())
111 | .build());
112 |
113 | assertThat(result).isNotNull();
114 | assertThat(result.name()).isEqualTo(AnotherTestView.NAME);
115 | assertThat(result.view())
116 | .isNotNull()
117 | .isInstanceOf(AnotherTestView.class);
118 | }
119 |
120 | @Test
121 | public void request_withViewNewingInterceptor_shouldReturnViewWithoutFallingBack() {
122 | ViewPump pump = ViewPump.builder()
123 | .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor())
124 | .build();
125 |
126 | FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class);
127 |
128 | InflateResult result = pump.inflate(InflateRequest.builder()
129 | .name(AnotherTestView.NAME)
130 | .context(mockContext)
131 | .attrs(mockAttrs)
132 | .fallbackViewCreator(mockFallbackViewCreator)
133 | .build());
134 |
135 | verifyNoInteractions(mockFallbackViewCreator);
136 |
137 | assertThat(result).isNotNull();
138 | assertThat(result.name()).isEqualTo(AnotherTestView.NAME);
139 | assertThat(result.view())
140 | .isNotNull()
141 | .isInstanceOf(AnotherTestView.class);
142 | }
143 |
144 | @Test
145 | public void request_withViewNewingInterceptor_shouldShortcircuitDownstreamInterceptorsAndFallback() {
146 | Interceptor starvedInterceptor = mock(Interceptor.class);
147 |
148 | ViewPump pump = ViewPump.builder()
149 | .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor())
150 | .addInterceptor(starvedInterceptor)
151 | .build();
152 |
153 | FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class);
154 |
155 | InflateResult result = pump.inflate(InflateRequest.builder()
156 | .name(AnotherTestView.NAME)
157 | .context(mockContext)
158 | .attrs(mockAttrs)
159 | .fallbackViewCreator(mockFallbackViewCreator)
160 | .build());
161 |
162 | verifyNoInteractions(starvedInterceptor);
163 | verifyNoInteractions(mockFallbackViewCreator);
164 |
165 | assertThat(result).isNotNull();
166 | assertThat(result.name()).isEqualTo(AnotherTestView.NAME);
167 | assertThat(result.view())
168 | .isNotNull()
169 | .isInstanceOf(AnotherTestView.class);
170 | }
171 |
172 | @Test
173 | public void request_withNameChangingAndViewNewingInterceptorInOrder_shouldReturnViewWithNewNameWithoutFallback() {
174 | ViewPump pump = ViewPump.builder()
175 | .addInterceptor(new NameChangingPreInflationInterceptor())
176 | .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor())
177 | .build();
178 |
179 | FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class);
180 |
181 | InflateResult result = pump.inflate(InflateRequest.builder()
182 | .name(TestView.NAME)
183 | .context(mockContext)
184 | .attrs(mockAttrs)
185 | .fallbackViewCreator(mockFallbackViewCreator)
186 | .build());
187 |
188 | verifyNoInteractions(mockFallbackViewCreator);
189 |
190 | assertThat(result).isNotNull();
191 | assertThat(result.name()).isEqualTo(AnotherTestView.NAME);
192 | assertThat(result.view())
193 | .isNotNull()
194 | .isInstanceOf(AnotherTestView.class);
195 | }
196 |
197 | @Test
198 | public void request_withNameChangingAndViewNewingInterceptorWrongOrder_shouldReturnViewWithNewNameWithFallback() {
199 | ViewPump pump = ViewPump.builder()
200 | .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor())
201 | .addInterceptor(new NameChangingPreInflationInterceptor())
202 | .build();
203 |
204 | View fallbackView = new AnotherTestView(mockContext);
205 | FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class);
206 | when(mockFallbackViewCreator.onCreateView(
207 | nullable(View.class),
208 | eq(AnotherTestView.NAME),
209 | eq(mockContext),
210 | nullable(AttributeSet.class)))
211 | .thenReturn(fallbackView);
212 |
213 | InflateResult result = pump.inflate(InflateRequest.builder()
214 | .name(TestView.NAME)
215 | .context(mockContext)
216 | .attrs(mockAttrs)
217 | .fallbackViewCreator(mockFallbackViewCreator)
218 | .build());
219 |
220 | verify(mockFallbackViewCreator)
221 | .onCreateView(nullable(View.class), eq(AnotherTestView.NAME), eq(mockContext), nullable(AttributeSet.class));
222 |
223 | assertThat(result).isNotNull();
224 | assertThat(result.name()).isEqualTo(AnotherTestView.NAME);
225 | assertThat(result.view())
226 | .isNotNull()
227 | .isInstanceOf(AnotherTestView.class)
228 | .isSameAs(fallbackView);
229 | }
230 |
231 | @Test
232 | public void createView_fromClassName_shouldReturnView() {
233 | View view = testPump().create(mockContext, TestView.class, mockAttrs);
234 |
235 | assertThat(view)
236 | .isNotNull()
237 | .isInstanceOf(TestView.class);
238 |
239 | assertThat(((TestView) view).isSameContextAs(mockContext)).isTrue();
240 | }
241 |
242 | @Test
243 | public void createView_fromClassNameWithSingleParamConstructor_shouldReturnView() {
244 | View view = testPump().create(mockContext, SingleConstructorTestView.class, mockAttrs);
245 |
246 | assertThat(view)
247 | .isNotNull()
248 | .isInstanceOf(SingleConstructorTestView.class);
249 |
250 | assertThat(((SingleConstructorTestView) view).isSameContextAs(mockContext)).isTrue();
251 | }
252 |
253 | @Test
254 | public void createView_withPreInflationInterceptor_shouldReturnViewWithNewName() {
255 | ViewPump pump = ViewPump.builder()
256 | .addInterceptor(new NameChangingPreInflationInterceptor())
257 | .build();
258 |
259 | View view = pump.create(mockContext, TestView.class, mockAttrs);
260 |
261 | assertThat(view)
262 | .isNotNull()
263 | .isInstanceOf(AnotherTestView.class);
264 |
265 | assertThat(((AnotherTestView) view).isSameContextAs(mockContext)).isTrue();
266 | }
267 |
268 | @Test
269 | public void createView_withPostInflationInterceptor_shouldReturnPostProcessedView() {
270 | ViewPump pump = ViewPump.builder()
271 | .addInterceptor(new TestPostInflationInterceptor())
272 | .build();
273 |
274 | View view = pump.create(mockContext, TestView.class, mockAttrs);
275 |
276 | assertThat(view)
277 | .isNotNull()
278 | .isInstanceOf(TestView.class);
279 |
280 | assertThat(((TestView) view).isSameContextAs(mockContext)).isTrue();
281 | assertThat(((TestView) view).isPostProcessed()).isTrue();
282 | }
283 |
284 | /** @noinspection deprecation*/
285 | @Test
286 | public void reset() {
287 | ViewPump first = ViewPump.builder()
288 | .addInterceptor(new TestPostInflationInterceptor())
289 | .build();
290 | ViewPump.init(first);
291 |
292 | assertThat(ViewPump.get())
293 | .isSameAs(first);
294 |
295 | // Now reset
296 | ViewPump.reset();
297 |
298 | // Now it's cleared the previously installed one
299 | assertThat(ViewPump.get())
300 | .isNotSameAs(first);
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestView.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 | import androidx.annotation.Nullable;
7 |
8 | public class AnotherTestView extends View {
9 |
10 | public static final String NAME = AnotherTestView.class.getName();
11 |
12 | private Context context;
13 |
14 | public AnotherTestView(Context context) {
15 | super(context);
16 | this.context = context;
17 | }
18 |
19 | public AnotherTestView(Context context, @Nullable AttributeSet attrs) {
20 | super(context, attrs);
21 | this.context = context;
22 | }
23 |
24 | public boolean isSameContextAs(Context context) {
25 | return this.context == context;
26 | }
27 | }
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestViewNewingPreInflationInterceptor.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import io.github.inflationx.viewpump.InflateRequest;
4 | import io.github.inflationx.viewpump.InflateResult;
5 | import io.github.inflationx.viewpump.Interceptor;
6 |
7 | public class AnotherTestViewNewingPreInflationInterceptor implements Interceptor {
8 |
9 | @Override
10 | public InflateResult intercept(Chain chain) {
11 | InflateRequest request = chain.request();
12 | if (AnotherTestView.NAME.equals(request.name())) {
13 | return InflateResult.builder()
14 | .view(new AnotherTestView(request.context()))
15 | .name(AnotherTestView.NAME)
16 | .context(request.context())
17 | .attrs(request.attrs())
18 | .build();
19 | } else {
20 | return chain.proceed(request);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/NameChangingPreInflationInterceptor.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import io.github.inflationx.viewpump.InflateResult;
4 | import io.github.inflationx.viewpump.Interceptor;
5 |
6 | public class NameChangingPreInflationInterceptor implements Interceptor {
7 |
8 | @Override
9 | public InflateResult intercept(Chain chain) {
10 | return chain.proceed(
11 | chain.request()
12 | .toBuilder()
13 | .name(AnotherTestView.NAME)
14 | .build());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/SingleConstructorTestView.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 |
6 | public class SingleConstructorTestView extends View {
7 |
8 | public static final String NAME = SingleConstructorTestView.class.getName();
9 |
10 | private Context context;
11 |
12 | public SingleConstructorTestView(Context context) {
13 | super(context);
14 | this.context = context;
15 | }
16 |
17 | public boolean isSameContextAs(Context context) {
18 | return this.context == context;
19 | }
20 | }
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestFallbackViewCreator.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 | import io.github.inflationx.viewpump.FallbackViewCreator;
9 |
10 | public class TestFallbackViewCreator implements FallbackViewCreator {
11 |
12 | @Nullable
13 | @Override
14 | public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @Nullable AttributeSet attrs) {
15 | if (TestView.NAME.equals(name)) {
16 | return new TestView(context, attrs);
17 | } else if (AnotherTestView.NAME.equals(name)) {
18 | return new AnotherTestView(context, attrs);
19 | }
20 | return null;
21 | }
22 | }
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestPostInflationInterceptor.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import io.github.inflationx.viewpump.InflateResult;
4 | import io.github.inflationx.viewpump.Interceptor;
5 |
6 | public class TestPostInflationInterceptor implements Interceptor {
7 |
8 | @Override
9 | public InflateResult intercept(Chain chain) {
10 | InflateResult result = chain.proceed(chain.request());
11 | if (result.view() instanceof TestView) {
12 | ((TestView) result.view()).setPostProcessed(true);
13 | }
14 | return result;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestView.java:
--------------------------------------------------------------------------------
1 | package io.github.inflationx.viewpump.util;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.View;
6 | import androidx.annotation.Nullable;
7 |
8 | public class TestView extends View {
9 |
10 | public static final String NAME = TestView.class.getName();
11 |
12 | private Context context;
13 |
14 | private boolean isPostProcessed;
15 |
16 | public TestView(Context context) {
17 | super(context);
18 | this.context = context;
19 | isPostProcessed = false;
20 | }
21 |
22 | public TestView(Context context, @Nullable AttributeSet attrs) {
23 | super(context, attrs);
24 | this.context = context;
25 | isPostProcessed = false;
26 | }
27 |
28 | public boolean isSameContextAs(Context context) {
29 | return this.context == context;
30 | }
31 |
32 | public boolean isPostProcessed() {
33 | return isPostProcessed;
34 | }
35 |
36 | public void setPostProcessed(boolean postProcessed) {
37 | isPostProcessed = postProcessed;
38 | }
39 | }
--------------------------------------------------------------------------------