-
Notifications
You must be signed in to change notification settings - Fork 61
/
-ViewPumpLayoutInflater.kt
435 lines (385 loc) · 13.6 KB
/
-ViewPumpLayoutInflater.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
@file:JvmName("-ViewPumpLayoutInflater")
package io.github.inflationx.viewpump.internal
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.BuildCompat
import io.github.inflationx.viewpump.FallbackViewCreator
import io.github.inflationx.viewpump.InflateRequest
import io.github.inflationx.viewpump.R.id
import io.github.inflationx.viewpump.ViewPump
import org.xmlpull.v1.XmlPullParser
import java.lang.reflect.Field
@Suppress("ClassName")
internal class `-ViewPumpLayoutInflater`(
original: LayoutInflater,
newContext: Context,
cloned: Boolean
) : LayoutInflater(original, newContext), `-ViewPumpActivityFactory` {
private val IS_AT_LEAST_Q = Build.VERSION.SDK_INT > Build.VERSION_CODES.P || BuildCompat.isAtLeastQ()
private val nameAndAttrsViewCreator: FallbackViewCreator = NameAndAttrsViewCreator(this)
private val parentAndNameAndAttrsViewCreator: FallbackViewCreator = ParentAndNameAndAttrsViewCreator(this)
// Reflection Hax
private var setPrivateFactory = false
private var storeLayoutResId = ViewPump.get().isStoreLayoutResId
init {
setUpLayoutFactories(cloned)
}
override fun cloneInContext(newContext: Context): LayoutInflater {
return `-ViewPumpLayoutInflater`(this, newContext, true)
}
// ===
// Wrapping goodies
// ===
override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View? {
val view = super.inflate(resource, root, attachToRoot)
if (view != null && storeLayoutResId) {
view.setTag(id.viewpump_layout_res, resource)
}
return view
}
override fun inflate(parser: XmlPullParser, root: ViewGroup?, attachToRoot: Boolean): View {
setPrivateFactoryInternal()
return super.inflate(parser, root, attachToRoot)
}
/**
* We don't want to unnecessary create/set our factories if there are none there. We try to be
* as lazy as possible.
*/
private fun setUpLayoutFactories(cloned: Boolean) {
if (cloned) return
// If we are HC+ we get and set Factory2 otherwise we just wrap Factory1
if (factory2 != null && factory2 !is WrapperFactory2) {
// Sets both Factory/Factory2
factory2 = factory2
}
// We can do this as setFactory2 is used for both methods.
if (factory != null && factory !is WrapperFactory) {
factory = factory
}
}
override fun setFactory(factory: LayoutInflater.Factory) {
// Only set our factory and wrap calls to the Factory trying to be set!
if (factory !is WrapperFactory) {
super.setFactory(
WrapperFactory(factory))
} else {
super.setFactory(factory)
}
}
override fun setFactory2(factory2: LayoutInflater.Factory2) {
// Only set our factory and wrap calls to the Factory2 trying to be set!
if (factory2 !is WrapperFactory2) {
// LayoutInflaterCompat.setFactory(this, new WrapperFactory2(factory2, mViewPumpFactory));
super.setFactory2(
WrapperFactory2(factory2))
} else {
super.setFactory2(factory2)
}
}
private fun setPrivateFactoryInternal() {
// Already tried to set the factory.
if (setPrivateFactory) return
// Reflection (Or Old Device) skip.
if (!ViewPump.get().isReflection) return
// Skip if not attached to an activity.
if (context !is LayoutInflater.Factory2) {
setPrivateFactory = true
return
}
// TODO: we need to get this and wrap it if something has already set this
val setPrivateFactoryMethod = LayoutInflater::class.java.getAccessibleMethod("setPrivateFactory")
setPrivateFactoryMethod.invokeMethod(this, PrivateWrapperFactory2(context as Factory2, this))
setPrivateFactory = true
}
// ===
// LayoutInflater ViewCreators
// Works in order of inflation
// ===
/**
* The Activity onCreateView (PrivateFactory) is the third port of call for LayoutInflation.
* We opted to manual injection over aggressive reflection, this should be less fragile.
*/
override fun onActivityCreateView(
parent: View?,
view: View,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
parent = parent,
fallbackViewCreator = ActivityViewCreator(
this, view)
))
.view
}
/**
* The LayoutInflater onCreateView is the fourth port of call for LayoutInflation.
* BUT only for none CustomViews.
*/
@Throws(ClassNotFoundException::class)
override fun onCreateView(parent: View?, name: String, attrs: AttributeSet?): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
parent = parent,
fallbackViewCreator = parentAndNameAndAttrsViewCreator
))
.view
}
/**
* The LayoutInflater onCreateView is the fourth port of call for LayoutInflation.
* BUT only for none CustomViews.
* Basically if this method doesn't inflate the View nothing probably will.
*/
@Throws(ClassNotFoundException::class)
override fun onCreateView(name: String, attrs: AttributeSet?): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
fallbackViewCreator = nameAndAttrsViewCreator
))
.view
}
/**
* Nasty method to inflate custom layouts that haven't been handled else where. If this fails it
* will fall back through to the PhoneLayoutInflater method of inflating custom views where
* ViewPump will NOT have a hook into.
*
* @param view view if it has been inflated by this point, if this is not null this method
* just returns this value.
* @param name name of the thing to inflate.
* @param viewContext Context to inflate by if parent is null
* @param attrs Attr for this view which we can steal fontPath from too.
* @return view or the View we inflate in here.
*/
private fun createCustomViewInternal(
view: View?,
name: String,
viewContext: Context,
attrs: AttributeSet?
): View? {
var mutableView = view
// I by no means advise anyone to do this normally, but Google have locked down access to
// the createView() method, so we never get a callback with attributes at the end of the
// createViewFromTag chain (which would solve all this unnecessary rubbish).
// We at the very least try to optimise this as much as possible.
// We only call for customViews (As they are the ones that never go through onCreateView(...)).
// We also maintain the Field reference and make it accessible which will make a pretty
// significant difference to performance on Android 4.0+.
// If CustomViewCreation is off skip this.
if (!ViewPump.get().isCustomViewCreation) return mutableView
if (mutableView == null && name.indexOf('.') > -1) {
if (IS_AT_LEAST_Q) {
mutableView = cloneInContext(viewContext).createView(name, null, attrs)
} else {
@Suppress("UNCHECKED_CAST")
val constructorArgsArr = CONSTRUCTOR_ARGS_FIELD.get(this) as Array<Any>
val lastContext = constructorArgsArr[0]
// The LayoutInflater actually finds out the correct context to use. We just need to set
// it on the mConstructor for the internal method.
// Set the constructor ars up for the createView, not sure why we can't pass these in.
constructorArgsArr[0] = viewContext
CONSTRUCTOR_ARGS_FIELD.setValueQuietly(this, constructorArgsArr)
try {
mutableView = createView(name, null, attrs)
} catch (ignored: ClassNotFoundException) {
} finally {
constructorArgsArr[0] = lastContext
CONSTRUCTOR_ARGS_FIELD.setValueQuietly(this, constructorArgsArr)
}
}
}
return mutableView
}
private fun superOnCreateView(parent: View?, name: String, attrs: AttributeSet?): View? {
return try {
super.onCreateView(parent, name, attrs)
} catch (e: ClassNotFoundException) {
null
}
}
private fun superOnCreateView(name: String, attrs: AttributeSet?): View? {
return try {
super.onCreateView(name, attrs)
} catch (e: ClassNotFoundException) {
null
}
}
// ===
// View creators
// ===
private class ActivityViewCreator(
private val inflater: `-ViewPumpLayoutInflater`,
private val view: View
) : FallbackViewCreator {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return inflater.createCustomViewInternal(view, name, context, attrs)
}
}
private class ParentAndNameAndAttrsViewCreator(
private val inflater: `-ViewPumpLayoutInflater`) : FallbackViewCreator {
override fun onCreateView(parent: View?, name: String, context: Context,
attrs: AttributeSet?): View? {
return inflater.superOnCreateView(parent, name, attrs)
}
}
private class NameAndAttrsViewCreator(
private val inflater: `-ViewPumpLayoutInflater`
) : FallbackViewCreator {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
// This mimics the {@code PhoneLayoutInflater} in the way it tries to inflate the base
// classes, if this fails its pretty certain the app will fail at this point.
var view: View? = null
for (prefix in CLASS_PREFIX_LIST) {
try {
view = inflater.createView(name, prefix, attrs)
if (view != null) {
break
}
} catch (ignored: ClassNotFoundException) {
}
}
// In this case we want to let the base class take a crack
// at it.
if (view == null) view = inflater.superOnCreateView(name, attrs)
return view
}
}
// ===
// Wrapper Factories
// ===
/**
* Factory 1 is the first port of call for LayoutInflation
*/
private class WrapperFactory(factory: LayoutInflater.Factory) : LayoutInflater.Factory {
private val viewCreator: FallbackViewCreator = WrapperFactoryViewCreator(factory)
override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
fallbackViewCreator = viewCreator
))
.view
}
}
private class WrapperFactoryViewCreator(
private val factory: LayoutInflater.Factory
) : FallbackViewCreator {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return factory.onCreateView(name, context, attrs)
}
}
/**
* Factory 2 is the second port of call for LayoutInflation
*/
private open class WrapperFactory2(factory2: LayoutInflater.Factory2) : LayoutInflater.Factory2 {
private val viewCreator = WrapperFactory2ViewCreator(factory2)
override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? {
return onCreateView(null, name, context, attrs)
}
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
parent = parent,
fallbackViewCreator = viewCreator
))
.view
}
}
private open class WrapperFactory2ViewCreator(
protected val factory2: LayoutInflater.Factory2) : FallbackViewCreator {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return factory2.onCreateView(parent, name, context, attrs)
}
}
/**
* Private factory is step three for Activity Inflation, this is what is attached to the Activity
*/
private class PrivateWrapperFactory2(
factory2: LayoutInflater.Factory2,
inflater: `-ViewPumpLayoutInflater`
) : WrapperFactory2(factory2) {
private val viewCreator = PrivateWrapperFactory2ViewCreator(factory2, inflater)
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return ViewPump.get()
.inflate(InflateRequest(
name = name,
context = context,
attrs = attrs,
parent = parent,
fallbackViewCreator = viewCreator
))
.view
}
}
private class PrivateWrapperFactory2ViewCreator(
factory2: LayoutInflater.Factory2,
private val inflater: `-ViewPumpLayoutInflater`
) : WrapperFactory2ViewCreator(factory2), FallbackViewCreator {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet?
): View? {
return inflater.createCustomViewInternal(
factory2.onCreateView(parent, name, context, attrs), name, context, attrs)
}
}
companion object {
private val CLASS_PREFIX_LIST = setOf("android.widget.", "android.webkit.")
private val CONSTRUCTOR_ARGS_FIELD: Field by lazy {
requireNotNull(LayoutInflater::class.java.getDeclaredField("mConstructorArgs")) {
"No constructor arguments field found in LayoutInflater!"
}.apply { isAccessible = true }
}
}
}