diff --git a/gradle/sweet-dependency/sweet-dependency-config.yaml b/gradle/sweet-dependency/sweet-dependency-config.yaml index 417096e..69a1bbd 100644 --- a/gradle/sweet-dependency/sweet-dependency-config.yaml +++ b/gradle/sweet-dependency/sweet-dependency-config.yaml @@ -59,11 +59,13 @@ libraries: version-ref: ::kotlinpoet com.highcapable.betterandroid: ui-component: - version: 1.0.7 + version: 1.0.8 + ui-component-adapter: + version: 1.0.0 ui-extension: - version: 1.0.6 + version: 1.0.7 system-extension: - version: 1.0.2 + version: 1.0.3 org.lsposed.hiddenapibypass: hiddenapibypass: version: 6.1 @@ -74,7 +76,7 @@ libraries: version: 1.0.1 com.highcapable.pangutext: pangutext-android: - version: 1.0.2 + version: 1.0.3 androidx.core: core: version: 1.16.0 diff --git a/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/extension/HikageProcessor.kt b/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/extension/HikageProcessor.kt index 6a50811..d514833 100644 --- a/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/extension/HikageProcessor.kt +++ b/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/extension/HikageProcessor.kt @@ -39,15 +39,20 @@ fun KSDeclaration.getSimpleNameString(): String { fun KSClassDeclaration.isSubclassOf(superType: KSType?): Boolean { if (superType == null) return false if (this == superType.declaration) return true + superTypes.forEach { parent -> val resolvedParent = parent.resolve() // Direct match. if (resolvedParent == superType) return true + val parentDeclaration = resolvedParent.declaration as? KSClassDeclaration ?: return@forEach + // Recursively check the parent class. if (parentDeclaration.isSubclassOf(superType)) return true - }; return false + } + + return false } fun KSClassDeclaration.asType() = asType(emptyList()) diff --git a/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/subprocessor/HikageViewGenerator.kt b/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/subprocessor/HikageViewGenerator.kt index 69af3f2..f57366b 100644 --- a/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/subprocessor/HikageViewGenerator.kt +++ b/hikage-compiler/src/main/java/com/highcapable/hikage/compiler/subprocessor/HikageViewGenerator.kt @@ -71,6 +71,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) override fun startProcess(resolver: Resolver) { Processor.init(logger, resolver) + val dependencies = Dependencies(aggregating = true, *resolver.getAllFiles().toList().toTypedArray()) resolver.getSymbolsWithAnnotation(HikageViewSpec.CLASS) .filterIsInstance() @@ -79,9 +80,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) ksClass.annotations.forEach { // Get annotation parameters. val (annotation, declaration) = HikageViewSpec.create(resolver, it, ksClass) + performers += Performer(annotation, declaration) } } + resolver.getSymbolsWithAnnotation(HikageViewDeclarationSpec.CLASS) .filterIsInstance() .distinctBy { it.qualifiedName } @@ -89,14 +92,17 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) ksClass.annotations.forEach { // Get annotation parameters. val (annotation, declaration) = HikageViewDeclarationSpec.create(resolver, it, ksClass) + performers += Performer(annotation, declaration) } } + processPerformer(dependencies) } private fun processPerformer(dependencies: Dependencies) { val duplicatedItems = performers.groupBy { it.declaration.key }.filter { it.value.size > 1 }.flatMap { it.value } + require(duplicatedItems.isEmpty()) { "Discover duplicate @HikageView or @HikageViewDeclaration's class name or alias definitions, " + "you can re-specify the class name using the `alias` parameter.\n" + @@ -109,25 +115,30 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) private fun generateCodeFile(dependencies: Dependencies, performer: Performer) { val classNameSet = performer.declaration.alias ?: performer.declaration.className val fileName = "_$classNameSet" + val viewClass = performer.declaration.toClassName().let { val packageName = it.packageName val simpleName = it.simpleName val topClassName = if (simpleName.contains(".")) simpleName.split(".")[0] else null + // com.example.MyViewScope // com.example.MyViewScope.MyView topClassName?.let { name -> ClassName(packageName, name) } to it } + val lparamsClass = performer.annotation.lparams?.let { val packageName = it.packageName.asString() val subClassName = it.getSimpleNameString() val simpleName = it.simpleName.asString() val topClassName = subClassName.replace(".$simpleName", "") + // android.view.ViewGroup // android.view.ViewGroup.LayoutParams (if (topClassName != subClassName) ClassName(packageName, topClassName) else null) to ClassName(packageName, subClassName) } + val packageName = "$PACKAGE_NAME_PREFIX.${performer.declaration.packageName}" val fileSpec = FileSpec.builder(packageName, fileName).apply { addFileComment( @@ -140,6 +151,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) **DO NOT EDIT THIS FILE MANUALLY** """.trimIndent() ) + addAnnotation( AnnotationSpec.builder(Suppress::class) .addMember("%S, %S, %S", "unused", "FunctionName", "DEPRECATION") @@ -150,17 +162,22 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) .addMember("%S", "${classNameSet}Performer") .build() ) + addImport(DeclaredSymbol.ANDROID_VIEW_PACKAGE_NAME, DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS_NAME) addImport(DeclaredSymbol.HIKAGE_CORE_PACKAGE_NAME, DeclaredSymbol.HIKAGE_CLASS_NAME) + // Kotlin's import rule is to introduce the parent class that also needs to be introduced at the same time. // If a child class exists, it needs to import the parent class, // and kotlinpoet will not perform this operation automatically. viewClass.first?.let { addImport(it.packageName, it.simpleName) } lparamsClass?.first?.let { addImport(it.packageName, it.simpleName) } + // May conflict with other [LayoutParams]. lparamsClass?.second?.let { addAliasedImport(it, it.getTypedSimpleName()) } + addAliasedImport(ViewGroupLpClass, ViewGroupLpClass.getTypedSimpleName()) addAliasedImport(HikageLparamClass, HikageLparamClass.getTypedSimpleName()) + addFunction(FunSpec.builder(classNameSet).apply { addKdoc( """ @@ -170,10 +187,12 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) @return [${performer.declaration.className}] """.trimIndent() ) + addAnnotation(HikageableClass) addModifiers(KModifier.INLINE) addTypeVariable(TypeVariableName(name = "LP", ViewGroupLpClass).copy(reified = true)) receiver(PerformerClass.parameterizedBy(TypeVariableName("LP"))) + addParameter( ParameterSpec.builder(name = "lparams", HikageLparamClass.copy(nullable = true)) .defaultValue("null") @@ -203,9 +222,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) ) addStatement("return ViewGroup<${performer.declaration.className}, ${it.simpleName}>(lparams, id, init, performer)") } ?: addStatement("return View<${performer.declaration.className}>(lparams, id, init)") + returns(viewClass.second) }.build()) }.build() + // May already exists, no need to generate. runCatching { fileSpec.writeTo(codeGenerator, dependencies) @@ -239,6 +260,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) ksType.declaration.qualifiedName?.asString() != Any::class.qualifiedName } var resolvedLparams = lparamsType?.declaration?.getClassDeclaration(resolver) + when { // If the current view is not a view group but the lparams parameter is declared, // remove the lparams parameter. @@ -253,6 +275,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) // set the default type parameter for it. declaration.isViewGroup && resolvedLparams == null -> resolvedLparams = lparamsDeclaration } + // Verify layout parameter class. if (resolvedLparams != null) require(resolvedLparams.isSubclassOf(lparamsDeclaration.asType())) { val resolvedLparamsName = resolvedLparams.qualifiedName?.asString() @@ -271,17 +294,21 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) val packageName = ksClass.packageName.asString() val className = ksClass.getSimpleNameString() val isViewGroup = ksClass.isSubclassOf(viewGroupDeclaration.asType()) + var _alias = alias // If no alias name is set, if the class name contains a subclass, // replace it with an underscore and use it as an alias name. if (_alias.isNullOrBlank() && className.contains(".")) _alias = className.replace(".", "_") _alias = _alias?.takeIf { it.isNotBlank() } + val declaration = ViewDeclaration(packageName, className, _alias, isViewGroup, baseType) + // Verify the legality of the class name. if (!_alias.isNullOrBlank()) require(ClassDetector.verify(_alias)) { "Declares @$tagName's alias \"$_alias\" is illegal.\n${declaration.locateDesc}" } + // [ViewGroup] cannot be new instance. require(ksClass != viewGroupDeclaration) { "Declares @$tagName's class must not be a directly \"${DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS}\".\n${declaration.locateDesc}" @@ -290,6 +317,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) require(ksClass.isSubclassOf(viewDeclaration.asType())) { "Declares @$tagName's class must be subclass of \"${DeclaredSymbol.ANDROID_VIEW_CLASS}\".\n${declaration.locateDesc}" } + return declaration } } @@ -332,9 +360,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) val alias = annotation.arguments.getOrNull("alias") val requireInit = annotation.arguments.getOrNull("requireInit") ?: false val requirePerformer = annotation.arguments.getOrNull("requirePerformer") ?: false + // Solve the actual content of the annotation parameters. val declaration = Processor.createViewDeclaration(NAME, alias, ksClass) val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams) + return HikageViewSpec(resolvedLparams, alias, requireInit, requirePerformer) to declaration } } @@ -363,9 +393,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) val alias = annotation.arguments.getOrNull("alias") val requireInit = annotation.arguments.getOrNull("requireInit") ?: false val requirePerformer = annotation.arguments.getOrNull("requirePerformer") ?: false + // Solve the actual content of the annotation parameters. val resolvedView = view?.declaration?.getClassDeclaration(resolver) ?: error("Internal error.") val declaration = Processor.createViewDeclaration(NAME, alias, resolvedView, ksClass) + // Only object classes can be used as view declarations. require(ksClass.classKind == ClassKind.OBJECT) { "Declares @$NAME's class must be an object.\n${declaration.locateDesc}" @@ -373,6 +405,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment) require(!ksClass.isCompanionObject) { "Declares @$NAME's class must not be a companion object.\n${declaration.locateDesc}" } + val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams) return HikageViewDeclarationSpec(resolvedView, resolvedLparams, alias, requireInit, requirePerformer) to declaration } diff --git a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageSafeTypeCastDetector.kt b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageSafeTypeCastDetector.kt index ce94214..dda45be 100644 --- a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageSafeTypeCastDetector.kt +++ b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageSafeTypeCastDetector.kt @@ -60,6 +60,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner { override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { if (node.selector !is UBinaryExpressionWithType) return + val castExpr = node.selector as UBinaryExpressionWithType visitAndLint(context, castExpr, node) } @@ -75,11 +76,14 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner { ) { // Get the parent node, if it is wrapped with brackets will also be included. val locationNode = node.uastParent as? UParenthesizedExpression ?: node + val receiver = parent?.receiver ?: node.operand val receiverType = (node.operand as? UArrayAccessExpression)?.receiver?.getExpressionType() ?: return val receiverClass = receiverType.canonicalText + // Filter retains results that meet the conditions. if (receiverClass != DeclaredSymbol.HIKAGE_CLASS) return + // Like `hikage["your_id"] as YourView`. val exprText = node.sourcePsi?.text ?: return // Like `hikage["your_id"]`. @@ -88,11 +92,15 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner { val receiverNameText = receiverText.split("[")[0] // Like `"your_id"`. val receiverContent = runCatching { receiverText.split("[")[1].split("]")[0] }.getOrNull() ?: return + val isSafeCast = exprText.contains("as?") || exprText.endsWith("?") + // Like `YourView`. val castTypeContent = node.typeReference?.sourcePsi?.text?.removeSuffix("?") ?: return + val replacement = "$receiverNameText.${if (isSafeCast) "getOrNull" else "get"}<$castTypeContent>($receiverContent)" val replaceSuggestion = if (isSafeCast) "Hikage.getOrNull<$castTypeContent>" else "Hikage.get<$castTypeContent>" + val location = context.getLocation(locationNode) val lintFix = LintFix.create() .name("Replace with '$replacement'") @@ -101,6 +109,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner { .with(replacement) .reformat(true) .build() + context.report( ISSUE, locationNode, location, message = "Can be replaced with safe type cast `$replaceSuggestion`.", diff --git a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableBeyondScopeDetector.kt b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableBeyondScopeDetector.kt index 7f356f0..dc436d2 100644 --- a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableBeyondScopeDetector.kt +++ b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableBeyondScopeDetector.kt @@ -70,6 +70,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner { override fun visitCallExpression(node: UCallExpression) { val callExpr = node.sourcePsi as? KtCallExpression ?: return val method = node.resolve() ?: return + startLint(callExpr, method) organizeAndReport() } @@ -78,6 +79,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner { val className = method.containingClass?.qualifiedName ?: "" val hasHikageable = method.hasHikageable() val hasLayoutParams = className == DeclaredSymbol.HIKAGE_PERFORMER_CLASS && method.name == "LayoutParams" + if (hasHikageable || hasLayoutParams) visitAndLint(callExpr, method) } @@ -85,6 +87,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner { reports.forEach { // Check if the call has been reported before reporting. if (reportedNodes.contains(it.callExpr)) return@forEach + val location = context.getLocation(it.callExpr) val lintFix = LintFix.create() .name("Delete Call Expression") @@ -93,6 +96,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner { // Delete the call expression. .with("") .build() + context.report(ISSUE, it.callExpr, location, it.message, lintFix) reportedNodes.add(it.callExpr) } @@ -102,28 +106,37 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner { val bodyBlocks = mutableMapOf() val parameters = method.parameterList.parameters val valueArguments = callExpr.valueArgumentList?.arguments ?: emptyList() + fun visitValueArg(arg: KtValueArgument) { val name = arg.getArgumentName()?.asName?.identifier ?: "" val expr = arg.getArgumentExpression() + val parameter = parameters.firstOrNull { it.name == name } // If the last bit is a lambda expression, then `parameter` must have a lambda parameter defined by the last bit. ?: if (arg is KtLambdaArgument) parameters.lastOrNull() else null + val isMatched = parameter?.type?.canonicalText?.matches(DeclaredSymbol.HIKAGE_VIEW_REGEX) == true && !parameter.type.canonicalText.contains(DeclaredSymbol.HIKAGE_PERFORMER_CLASS) + if (expr is KtLambdaExpression && isMatched) expr.bodyExpression?.let { bodyBlocks[name] = it } } + // Get the last lambda expression. val lastLambda = callExpr.lambdaArguments.lastOrNull() if (lastLambda != null) visitValueArg(lastLambda) + valueArguments.forEach { arg -> visitValueArg(arg) } + bodyBlocks.toList().flatMap { (_, value) -> value.children.filterIsInstance() }.forEach { val expression = it.toUElementOfType() ?: return@forEach val sCallExpr = expression.sourcePsi as? KtCallExpression ?: return@forEach val sMethod = expression.resolve() ?: return@forEach + if (sMethod.hasHikageable()) { val message = "Performers are not allowed to appear in `${method.name}` DSL creation process." reports.add(ReportDetail(message, expression)) + // Recursively to visit next level. visitAndLint(sCallExpr, sMethod) } diff --git a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableFunctionsDetector.kt b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableFunctionsDetector.kt index a046abf..cdd8a51 100644 --- a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableFunctionsDetector.kt +++ b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/HikageableFunctionsDetector.kt @@ -65,6 +65,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner { override fun visitMethod(node: UMethod) { val uastBody = node.uastBody as? UBlockExpression ?: return + val bodyHasHikageable = uastBody.expressions.any { when (it) { is UCallExpression -> it.resolve()?.hasHikageable() ?: false @@ -73,17 +74,22 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner { else -> false } } + if (!node.hasHikageable() && bodyHasHikageable) { val location = context.getLocation(node) val nameLocation = context.getNameLocation(node) + val message = "Function `${node.name}` must be marked with the `@Hikageable` annotation." + val functionText = node.asSourceString() val hasDoubleSlash = functionText.startsWith("//") + val replacement = functionRegex.replace(functionText) { result -> val functionBody = result.groupValues.getOrNull(0) ?: functionText val prefix = if (hasDoubleSlash) "\n" else "" "$prefix@Hikageable $functionBody" } + val lintFix = LintFix.create() .name("Add '@Hikageable' to '${node.name}'") .replace() @@ -92,6 +98,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner { .imports(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS) .reformat(true) .build() + context.report(ISSUE, node, nameLocation, message, lintFix) } } diff --git a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/WidgetsUsageDetector.kt b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/WidgetsUsageDetector.kt index c60dff4..d245704 100644 --- a/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/WidgetsUsageDetector.kt +++ b/hikage-core-lint/src/main/java/com/highcapable/hikage/core/lint/detector/WidgetsUsageDetector.kt @@ -135,18 +135,23 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner { importRefs.getOrNull(1)?.trim() else null val importPrefix = importRefs[0] + val hasPrefix = importPrefix.contains(".") + val name = if (hasPrefix) importPrefix.split(".").last() else importPrefix + val packagePrefix = importPrefix.replace(if (hasPrefix) ".$name" else name, "") val reference = ImportReference(packagePrefix, name, alias) + importReferences.add(reference) } override fun visitCallExpression(node: UCallExpression) { val callExpr = node.sourcePsi as? KtCallExpression ?: return val method = node.resolve() ?: return + startLint(callExpr, method) } @@ -165,7 +170,9 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner { type.canonicalText == VIEW_GROUP_CLASS_NAME } } + val isTypedViewFunction = typedViewFunctionIndex >= 0 + val imports = typeArgumentsText.mapNotNull { typeName -> when { // Like `TextView`. @@ -179,18 +186,24 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner { else -> null } } + val matchedIndex = builtInWidgets.indexOfFirst { imports.any { e -> e.alias == it || e.name == it } } val isBuiltInWidget = matchedIndex >= 0 + if (isTypedViewFunction && isBuiltInWidget) { val widgetName = builtInWidgets[matchedIndex] + val sourceLocation = context.getLocation(callExpr) val sourceText = callExpr.toUElement()?.asSourceString() ?: return val callExprElement = callExpr.toUElement() ?: return + // Matchs '>' and like `View")[0].trim().length + 1 val nameLocation = context.getRangeLocation(callExprElement, fromDelta = 0, callExprLength) + // Only replace the first one, because there may be multiple sub-functions in DSL. val replacement = sourceText.replaceFirst(viewExpressionRegex, widgetName) + val lintFix = LintFix.create() .name("Replace with '$widgetName'") .replace() @@ -199,6 +212,7 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner { .imports("$WIDGET_FUNCTION_PREFIX.$widgetName") .reformat(true) .build() + val message = "Can be simplified to `$widgetName`." context.report(ISSUE, callExpr, nameLocation, message, lintFix) } diff --git a/hikage-core/src/main/java/com/highcapable/hikage/bypass/XmlBlockBypass.kt b/hikage-core/src/main/java/com/highcapable/hikage/bypass/XmlBlockBypass.kt index a064cbd..b121ed4 100644 --- a/hikage-core/src/main/java/com/highcapable/hikage/bypass/XmlBlockBypass.kt +++ b/hikage-core/src/main/java/com/highcapable/hikage/bypass/XmlBlockBypass.kt @@ -31,7 +31,7 @@ import android.content.res.loader.AssetsProvider import android.content.res.loader.ResourcesProvider import android.util.AttributeSet import androidx.annotation.StyleRes -import com.highcapable.betterandroid.system.extension.tool.SystemVersion +import com.highcapable.betterandroid.system.extension.tool.AndroidVersion import com.highcapable.betterandroid.ui.extension.view.inflateOrNull import com.highcapable.betterandroid.ui.extension.view.layoutInflater import com.highcapable.hikage.core.R @@ -116,12 +116,12 @@ internal object XmlBlockBypass { private val resolver = object : MemberProcessor.Resolver() { override fun getDeclaredConstructors(declaringClass: Class): List> = - SystemVersion.require(SystemVersion.P, super.getDeclaredConstructors(declaringClass)) { + AndroidVersion.require(AndroidVersion.P, super.getDeclaredConstructors(declaringClass)) { HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance>().toList() } override fun getDeclaredMethods(declaringClass: Class): List = - SystemVersion.require(SystemVersion.P, super.getDeclaredMethods(declaringClass)) { + AndroidVersion.require(AndroidVersion.P, super.getDeclaredMethods(declaringClass)) { HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance().toList() } } @@ -143,6 +143,7 @@ internal object XmlBlockBypass { fun init(context: Context) { // Context may be loaded from the preview and other non-Android platforms, ignoring this. if (context.javaClass.name.endsWith("BridgeContext")) return + init(context.applicationContext.applicationInfo) } @@ -151,11 +152,12 @@ internal object XmlBlockBypass { * @param info the application info. */ private fun init(info: ApplicationInfo) { - if (SystemVersion.isLowOrEqualsTo(SystemVersion.P)) return + if (AndroidVersion.isAtMost(AndroidVersion.P)) return if (isInitOnce) return + val sourceDir = info.sourceDir xmlBlock = when { - SystemVersion.isHighOrEqualsTo(SystemVersion.R) -> + AndroidVersion.isAtLeast(AndroidVersion.R) -> // private static native long nativeLoad(@FormatType int format, @NonNull String path, // @PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException; ApkAssetsClass.resolve() @@ -166,7 +168,7 @@ internal object XmlBlockBypass { parameters(Int::class, String::class, Int::class, AssetsProvider::class) modifiers(Modifiers.NATIVE) }?.invokeQuietly(FORMAT_APK, sourceDir, PROPERTY_SYSTEM, null) - SystemVersion.isHighOrEqualsTo(SystemVersion.P) -> + AndroidVersion.isAtLeast(AndroidVersion.P) -> // private static native long nativeLoad( // @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay) // throws IOException; @@ -180,18 +182,20 @@ internal object XmlBlockBypass { }?.invokeQuietly(sourceDir, false, false, false) else -> error("Unsupported Android version.") } as? Long? ?: error("Failed to create ApkAssets.") + blockParser = XmlBlockClass.resolve() .processor(resolver) .optional() .firstConstructorOrNull { - if (SystemVersion.isHighOrEqualsTo(36)) + if (AndroidVersion.isAtLeast(AndroidVersion.BAKLAVA)) parameters(AssetManager::class, Long::class, Boolean::class) else parameters(AssetManager::class, Long::class) }?.let { - if (SystemVersion.isHighOrEqualsTo(36)) + if (AndroidVersion.isAtLeast(AndroidVersion.BAKLAVA)) it.createQuietly(null, xmlBlock, false) else it.createQuietly(null, xmlBlock) } ?: error("Failed to create XmlBlock\$Parser.") + isInitOnce = true } @@ -208,8 +212,10 @@ internal object XmlBlockBypass { */ fun createViewAttrs() = context.layoutInflater.inflateOrNull(R.layout.layout_hikage_attrs_view)?.attrs as? XmlResourceParser? ?: error("Failed to create AttributeSet.") - return if (SystemVersion.isHighOrEqualsTo(SystemVersion.P)) { + + return if (AndroidVersion.isAtLeast(AndroidVersion.P)) { if (!isInitOnce) return createViewAttrs() + require(blockParser != null) { "Hikage initialization failed." } newParser?.copy()?.of(blockParser) ?.invokeQuietly(resId) diff --git a/hikage-core/src/main/java/com/highcapable/hikage/core/Hikage.kt b/hikage-core/src/main/java/com/highcapable/hikage/core/Hikage.kt index ccb89e9..bcec0d0 100644 --- a/hikage-core/src/main/java/com/highcapable/hikage/core/Hikage.kt +++ b/hikage-core/src/main/java/com/highcapable/hikage/core/Hikage.kt @@ -167,7 +167,9 @@ class Hikage private constructor(private val factories: List) { if (parent != null && attachToParent) { val parentId = generateRandomViewId() provideView(parent, parentId) - }; newPerformer(lpClass, parent, attachToParent, context).apply(performer) + } + + newPerformer(lpClass, parent, attachToParent, context).apply(performer) } /** @@ -317,11 +319,13 @@ class Hikage private constructor(private val factories: List) { */ private fun createView(viewClass: Class, id: String?, context: Context): V { val attrs = createAttributeSet(context) + val view = createViewFromFactory(viewClass, id, context, attrs) ?: getViewConstructor(viewClass)?.build(context, attrs) if (view == null) throw PerformerException( "Create view of type ${viewClass.name} failed. " + "Please make sure the view class has a constructor with a single parameter of type Context." ) + provideView(view, id) return view } @@ -334,16 +338,20 @@ class Hikage private constructor(private val factories: List) { private fun getViewConstructor(viewClass: Class) = viewConstructors[viewClass.name] as? ViewConstructor? ?: run { var parameterCount = 0 + val twoParams = viewClass.resolve() .optional(silent = true) .firstConstructorOrNull { parameters(Context::class, AttributeSet::class) } val onceParam = viewClass.resolve() .optional(silent = true) .firstConstructorOrNull { parameters(Context::class) } + val constructor = onceParam?.apply { parameterCount = 1 } ?: twoParams?.apply { parameterCount = 2 } + val viewConstructor = constructor?.let { ViewConstructor(it, parameterCount) } if (viewConstructor != null) viewConstructors[viewClass.name] = viewConstructor + viewConstructor } @@ -358,15 +366,20 @@ class Hikage private constructor(private val factories: List) { private fun createViewFromFactory(viewClass: Class, id: String?, context: Context, attrs: AttributeSet): V? { val parent = performers.firstOrNull()?.parent var processed: V? = null + factories.forEach { factory -> val params = PerformerParams(id, attrs, viewClass as Class) + val view = factory(parent, processed, context, params) if (view != null && view.javaClass isNotSubclassOf viewClass) throw PerformerException( "HikageFactory cannot cast the created view type \"${view.javaClass}\" to \"${viewClass.name}\", " + "please confirm that the view type you created is correct." ) + if (view != null) processed = view as? V? - }; return processed + } + + return processed } /** @@ -379,6 +392,7 @@ class Hikage private constructor(private val factories: List) { val (requireId, viewId) = generateViewId(id) view.id = viewId views[requireId] = view + return requireId } @@ -395,12 +409,16 @@ class Hikage private constructor(private val factories: List) { */ fun doGenerate(id: String): Int { val generateId = View.generateViewId() + if (viewIds.contains(id)) throw PerformerException("View with id \"$id\" already exists.") viewIds[id] = generateId + return generateId } + val requireId = id ?: generateRandomViewId() val viewId = doGenerate(requireId) + return requireId to viewId } @@ -456,6 +474,7 @@ class Hikage private constructor(private val factories: List) { */ internal inline fun requireNoPerformers(name: String, block: () -> Unit) { val viewCount = views.size + block() if (views.size != viewCount) throw PerformerException( "Performers are not allowed to appear in $name DSL creation process." @@ -472,13 +491,16 @@ class Hikage private constructor(private val factories: List) { private fun include(delegate: Delegate<*>, context: Context, embedded: Boolean): Hikage { val hikage = delegate.create(context) if (!embedded) return hikage + val duplicateId = hikage.viewIds.toList().firstOrNull { (k, _) -> viewIds.containsKey(k) }?.first if (duplicateId != null) throw PerformerException( "Embedded layout view IDs conflict, the view id \"$duplicateId\" is already exists." ) + viewIds.putAll(hikage.viewIds) views.putAll(hikage.views) performers.addAll(hikage.performers) + return hikage } @@ -544,9 +566,11 @@ class Hikage private constructor(private val factories: List) { val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams) val view = createView(classOf(), id, context) view.layoutParams = lpDelegate.create() + requireNoPerformers(classOf().name) { view.init() } startProvide(id, classOf()) addToParentIfRequired(view) + return view } @@ -587,10 +611,12 @@ class Hikage private constructor(private val factories: List) { val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams) val view = createView(classOf(), id, context) view.layoutParams = lpDelegate.create() + requireNoPerformers(classOf().name) { view.init() } startProvide(id, classOf()) addToParentIfRequired(view) newPerformer(view).apply(performer) + return view } @@ -625,10 +651,12 @@ class Hikage private constructor(private val factories: List) { id: String? = null ): View { val view = context.layoutInflater.inflate(resId, parent, attachToRoot = false) + startProvide(id, view.javaClass, view) lparams?.create()?.let { view.layoutParams = it } provideView(view, id) addToParentIfRequired(view) + return view } @@ -645,14 +673,17 @@ class Hikage private constructor(private val factories: List) { ): VB { val viewBinding = ViewBinding().inflate(context.layoutInflater, parent, attachToParent = false) val view = viewBinding.root + startProvide(id, view.javaClass, view) if (view.parent != null) throw ProvideException( "The ViewBinding($view) already has a parent, " + "it may have been created using layout root node or , cannot be provided." ) + lparams?.create()?.let { view.layoutParams = it } provideView(view, id) addToParentIfRequired(view) + return viewBinding } @@ -670,11 +701,14 @@ class Hikage private constructor(private val factories: List) { id: String? = null ): View { if (view.parent != null) throw ProvideException("The view $view already has a parent, cannot be provided.") + startProvide(id, view.javaClass, view) val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams) view.layoutParams = lpDelegate.create() + provideView(view, id) addToParentIfRequired(view) + return view } @@ -708,13 +742,17 @@ class Hikage private constructor(private val factories: List) { ): Hikage { val view = hikage.root startProvide(id, view.javaClass, view) + val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams) if (view.parent != null) throw ProvideException( "The Hikage layout root view $view already has a parent, cannot be provided." ) + view.layoutParams = lpDelegate.create() + provideView(view, id) addToParentIfRequired(view) + return hikage } @@ -783,6 +821,7 @@ class Hikage private constructor(private val factories: List) { */ private fun startProvide(id: String?, viewClass: Class, view: V? = null) { provideCount++ + if (provideCount > 1 && (parent == null || !attachToParent)) throw ProvideException( "Provide view ${view?.javaClass ?: viewClass}(${id?.let { "\"$it\""} ?: ""}) failed. ${ if (parent == null) "No parent view group found" @@ -900,6 +939,7 @@ class Hikage private constructor(private val factories: List) { superclass() }?.invokeQuietly(it) } ?: lparams + return wrapped // Build a default. ?: lpClass.createInstanceOrNull(LayoutParamsWrapContent, LayoutParamsWrapContent) @@ -912,12 +952,15 @@ class Hikage private constructor(private val factories: List) { */ fun create(): ViewGroup.LayoutParams { if (bodyBuilder == null && wrapperBuilder == null) throw PerformerException("No layout params builder found.") + return bodyBuilder?.let { val lparams = ViewLayoutParams(lpClass, it.width, it.height, it.matchParent, it.widthMatchParent, it.heightMatchParent) current.requireNoPerformers(lparams.javaClass.name) { it.body(lparams) } + lparams } ?: wrapperBuilder?.let { val lparams = it.delegate?.create() ?: it.lparams + createDefaultLayoutParams(lparams) } ?: throw PerformerException("Internal error of build layout params.") } diff --git a/hikage-core/src/main/java/com/highcapable/hikage/core/preview/HikagePreview.kt b/hikage-core/src/main/java/com/highcapable/hikage/core/preview/HikagePreview.kt index 9a89bd1..bee18d3 100644 --- a/hikage-core/src/main/java/com/highcapable/hikage/core/preview/HikagePreview.kt +++ b/hikage-core/src/main/java/com/highcapable/hikage/core/preview/HikagePreview.kt @@ -47,6 +47,7 @@ abstract class HikagePreview(context: Context, attrs: AttributeSet? = null) : Fr @CallSuper override fun onAttachedToWindow() { super.onAttachedToWindow() + removeAllViews() build().create(context, parent = this) } diff --git a/hikage-extension-betterandroid/build.gradle.kts b/hikage-extension-betterandroid/build.gradle.kts index 0a4353d..0689dab 100644 --- a/hikage-extension-betterandroid/build.gradle.kts +++ b/hikage-extension-betterandroid/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(com.highcapable.kavaref.kavaref.core) implementation(com.highcapable.kavaref.kavaref.extension) implementation(com.highcapable.betterandroid.ui.component) + implementation(com.highcapable.betterandroid.ui.component.adapter) implementation(com.highcapable.betterandroid.ui.extension) implementation(com.highcapable.betterandroid.system.extension) implementation(androidx.core.core.ktx) diff --git a/hikage-extension-betterandroid/src/main/java/com/highcapable/hikage/extension/betterandroid/ui/component/adapter/CommonAdapterBuilder.kt b/hikage-extension-betterandroid/src/main/java/com/highcapable/hikage/extension/betterandroid/ui/component/adapter/CommonAdapterBuilder.kt index 41855c7..3360f48 100644 --- a/hikage-extension-betterandroid/src/main/java/com/highcapable/hikage/extension/betterandroid/ui/component/adapter/CommonAdapterBuilder.kt +++ b/hikage-extension-betterandroid/src/main/java/com/highcapable/hikage/extension/betterandroid/ui/component/adapter/CommonAdapterBuilder.kt @@ -25,7 +25,7 @@ package com.highcapable.hikage.extension.betterandroid.ui.component.adapter import android.view.ViewGroup -import com.highcapable.betterandroid.ui.component.adapter.CommonAdapterBuilder +import com.highcapable.betterandroid.ui.component.adapter.BaseAdapterBuilder import com.highcapable.hikage.core.Hikage import com.highcapable.hikage.core.base.HikagePerformer import com.highcapable.hikage.core.base.Hikageable @@ -51,26 +51,26 @@ import com.highcapable.hikage.extension.betterandroid.ui.component.adapter.viewh * hikage.get("text").text = "Item ${entity.name} of ${position + 1}" * } * ``` - * @see CommonAdapterBuilder.onBindItemView - * @receiver [CommonAdapterBuilder]<[E]> + * @see BaseAdapterBuilder.onBindItemView + * @receiver [BaseAdapterBuilder]<[E]> * @param Hikageable the performer body. - * @return [CommonAdapterBuilder]<[E]> + * @return [BaseAdapterBuilder]<[E]> */ @JvmOverloads -fun CommonAdapterBuilder.onBindItemView( +fun BaseAdapterBuilder.onBindItemView( Hikageable: HikagePerformer, viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> } ) = onBindItemView(Hikageable(performer = Hikageable), viewHolder) /** * Create and add view holder from [Hikage.Delegate]. - * @see CommonAdapterBuilder.onBindItemView - * @receiver [CommonAdapterBuilder]<[E]> + * @see BaseAdapterBuilder.onBindItemView + * @receiver [BaseAdapterBuilder]<[E]> * @param delegate the delegate. - * @return [CommonAdapterBuilder]<[E]> + * @return [BaseAdapterBuilder]<[E]> */ @JvmOverloads -fun CommonAdapterBuilder.onBindItemView( +fun BaseAdapterBuilder.onBindItemView( delegate: Hikage.Delegate<*>, viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> } ) = onBindItemView(HikageHolderDelegate(delegate), viewHolder) \ No newline at end of file diff --git a/samples/app/src/main/java/com/highcapable/hikage/demo/DemoApp.kt b/samples/app/src/main/java/com/highcapable/hikage/demo/DemoApp.kt index a58f088..18b2ac8 100644 --- a/samples/app/src/main/java/com/highcapable/hikage/demo/DemoApp.kt +++ b/samples/app/src/main/java/com/highcapable/hikage/demo/DemoApp.kt @@ -28,6 +28,7 @@ class DemoApp : Application() { override fun onCreate() { super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) } } \ No newline at end of file diff --git a/samples/app/src/main/java/com/highcapable/hikage/demo/ui/MainActivity.kt b/samples/app/src/main/java/com/highcapable/hikage/demo/ui/MainActivity.kt index 3941450..27650ba 100644 --- a/samples/app/src/main/java/com/highcapable/hikage/demo/ui/MainActivity.kt +++ b/samples/app/src/main/java/com/highcapable/hikage/demo/ui/MainActivity.kt @@ -52,9 +52,11 @@ class MainActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentView { var username = "" var password = "" + CoordinatorLayout( lparams = LayoutParams(matchParent = true) ) { diff --git a/samples/app/src/main/java/com/highcapable/hikage/demo/ui/base/BaseActivity.kt b/samples/app/src/main/java/com/highcapable/hikage/demo/ui/base/BaseActivity.kt index 630c926..389826d 100644 --- a/samples/app/src/main/java/com/highcapable/hikage/demo/ui/base/BaseActivity.kt +++ b/samples/app/src/main/java/com/highcapable/hikage/demo/ui/base/BaseActivity.kt @@ -31,6 +31,7 @@ abstract class BaseActivity : AppViewsActivity() { @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + PanguTextFactory2.inject(this) } } \ No newline at end of file