Bump hikage-core, hikage-extension, hikage-extension-betterandroid, hikage-extension-compose, hikage-compiler, hikage-widget-androidx, hikage-widget-material version to 1.0.0

This commit is contained in:
2025-04-20 05:32:06 +08:00
parent 794c535789
commit 99abe3cd18
218 changed files with 13256 additions and 627 deletions

View File

@@ -0,0 +1,54 @@
plugins {
autowire(libs.plugins.android.library)
autowire(libs.plugins.kotlin.android)
autowire(libs.plugins.kotlin.dokka)
autowire(libs.plugins.maven.publish)
autowire(libs.plugins.kotlin.ksp)
}
group = property.project.groupName
version = property.project.hikage.core.version
android {
namespace = property.project.hikage.core.namespace
compileSdk = property.project.android.compileSdk
defaultConfig {
minSdk = property.project.android.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
)
}
}
dependencies {
lintPublish(projects.hikageCoreLint)
ksp(projects.hikageCompiler)
implementation(org.lsposed.hiddenapibypass.hiddenapibypass)
implementation(com.highcapable.yukireflection.api)
api(com.highcapable.betterandroid.ui.extension)
implementation(com.highcapable.betterandroid.system.extension)
implementation(androidx.core.core.ktx)
implementation(androidx.appcompat.appcompat)
testImplementation(junit.junit)
androidTestImplementation(androidx.test.ext.junit)
androidTestImplementation(androidx.test.espresso.espresso.core)
}

View File

21
hikage-core/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,25 @@
package com.highcapable.hikage
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.highcapable.hikage.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,57 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/23.
*/
package com.highcapable.hikage.annotation
import android.view.View
import android.view.ViewGroup
import com.highcapable.hikage.core.Hikage
import kotlin.reflect.KClass
/**
* Declare annotations to generate the [Hikage.Performer] function for the specified [View] at compile time.
*
* Usage:
*
* ```kotlin
* @HikageView(lparams = FrameLayout.LayoutParams::class, alias = "MyView")
* class MyView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
* // ...
* }
* ```
* @param lparams the layout params class, only [ViewGroup] can be specified,
* after specifying, the `performer` parameter will be generated for the function.
* The parameters must be a class inherited from [ViewGroup.LayoutParams],
* if the current [View] does not inherit from [ViewGroup], this parameter will be ignored and warned.
* @param alias the view's class name alias will naming the function, default is the class name.
* @param requireInit whether to force the `init` parameter to be called, default is false.
* @param requirePerformer whether to force the `performer` parameter to be called, default is false,
* this parameter will be ignored when no `performer` parameter is needed here.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
annotation class HikageView(
val lparams: KClass<*> = Any::class,
val alias: String = "",
val requireInit: Boolean = false,
val requirePerformer: Boolean = false
)

View File

@@ -0,0 +1,61 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/31.
*/
package com.highcapable.hikage.annotation
import android.view.View
import android.view.ViewGroup
import com.highcapable.hikage.core.Hikage
import kotlin.reflect.KClass
/**
* Declare annotations to generate the [Hikage.Performer] function for the specified [view] at compile time.
*
* You can declare a class for [View] that does not belong to your own [View] or third-party library.
*
* The class name you define can be as good as you like, and notice it must to use the `object` keyword to modify it.
*
* Usage:
*
* ```kotlin
* @HikageViewDeclaration(ThirdPartyView::class, FrameLayout.LayoutParams::class, alias = "ThirdPartyView")
* object ThirdPartyViewDeclaration
* ```
* @param view the view class, only [View] can be specified.
* @param lparams the layout params class, only [ViewGroup] can be specified,
* after specifying, the `performer` parameter will be generated for the function.
* The parameters must be a class inherited from [ViewGroup.LayoutParams],
* if the current [View] does not inherit from [ViewGroup], this parameter will be ignored and warned.
* @param alias the view's class name alias will naming the function, default is the [view] class name.
* @param requireInit whether to force the `init` parameter to be called, default is false.
* @param requirePerformer whether to force the `performer` parameter to be called, default is false,
* this parameter will be ignored when no `performer` parameter is needed here.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
annotation class HikageViewDeclaration(
val view: KClass<*>,
val lparams: KClass<*> = Any::class,
val alias: String = "",
val requireInit: Boolean = false,
val requirePerformer: Boolean = false
)

View File

@@ -0,0 +1,45 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/23.
*/
package com.highcapable.hikage.annotation
import com.highcapable.hikage.core.Hikage
/**
* Declare an implementation of the [Hikage.Performer] function that
* will be used for infectious creation of sub-layouts.
*
* Usage:
*
* ```kotlin
* @Hikageable
* inline fun <reified LP : ViewGroup.LayoutParams> Hikage.Performer<LP>.MyView(
* lparams: Hikage.LayoutParams? = null,
* id: String? = null,
* init: HikageView<MyView> = {}
* ) = View<MyView>(id, init, lparams)
* ```
* @see Hikage.Performer
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
@MustBeDocumented
annotation class Hikageable

View File

@@ -0,0 +1,35 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/28.
*/
package com.highcapable.hikage.bypass
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.Keep
/**
* Just a view for obtaining [AttributeSet].
*
* **DONT USE THIS VIEW IN YOUR LAYOUT.**
*/
@Keep
class HikageAttrsView internal constructor(context: Context, internal val attrs: AttributeSet?) : View(context, attrs)

View File

@@ -0,0 +1,178 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/5.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.highcapable.hikage.bypass
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.res.XmlResourceParser
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.ui.extension.view.inflateOrNull
import com.highcapable.betterandroid.ui.extension.view.layoutInflater
import com.highcapable.hikage.core.R
import com.highcapable.yukireflection.factory.classOf
import com.highcapable.yukireflection.factory.lazyClass
import com.highcapable.yukireflection.type.android.AssetManagerClass
import com.highcapable.yukireflection.type.java.BooleanType
import com.highcapable.yukireflection.type.java.IntType
import com.highcapable.yukireflection.type.java.LongType
import com.highcapable.yukireflection.type.java.StringClass
import org.lsposed.hiddenapibypass.HiddenApiBypass
import android.R as Android_R
/**
* Create a new XmlBlock with system api bypass reflection magic.
*/
internal object XmlBlockBypass {
/** The path used to load the apk assets represents an APK file. */
private const val FORMAT_APK = 0
/** The path used to load the apk assets represents an idmap file. */
private const val FORMAT_IDMAP = 1
/** The path used to load the apk assets represents an resources.arsc file. */
private const val FORMAT_ARSC = 2
/** The path used to load the apk assets represents a directory. */
private const val FORMAT_DIR = 3
/**
* The apk assets contains framework resource values specified by the system.
* This allows some functions to filter out this package when computing what
* configurations/resources are available.
*/
private const val PROPERTY_SYSTEM = 1 shl 0
/**
* The apk assets is a shared library or was loaded as a shared library by force.
* The package ids of dynamic apk assets are assigned at runtime instead of compile time.
*/
private const val PROPERTY_DYNAMIC = 1 shl 1
/**
* The apk assets has been loaded dynamically using a [ResourcesProvider].
* Loader apk assets overlay resources like RROs except they are not backed by an idmap.
*/
private const val PROPERTY_LOADER = 1 shl 2
/**
* The apk assets is a RRO.
* An RRO overlays resource values of its target package.
*/
private const val PROPERTY_OVERLAY = 1 shl 3
/**
* The apk assets is owned by the application running in this process and incremental crash
* protections for this APK must be disabled.
*/
private const val PROPERTY_DISABLE_INCREMENTAL_HARDENING = 1 shl 4
/**
* The apk assets only contain the overlayable declarations information.
*/
private const val PROPERTY_ONLY_OVERLAYABLES = 1 shl 5
/** The apk assets class. */
private val ApkAssetsClass by lazyClass("android.content.res.ApkAssets")
/** The xml block class. */
private val XmlBlockClass by lazyClass("android.content.res.XmlBlock")
/** Global pointer references object. */
private var xmlBlock: Long? = null
/** Global pointer references object. */
private var blockParser: AutoCloseable? = null
/** Whether the initialization is done once. */
private var isInitOnce = false
/**
* Initialize.
* @param context the context.
*/
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)
}
/**
* Initialize.
* @param info the application info.
*/
private fun init(info: ApplicationInfo) {
if (SystemVersion.isLowOrEqualsTo(SystemVersion.P)) return
if (isInitOnce) return
val sourceDir = info.sourceDir
xmlBlock = when {
SystemVersion.isHighOrEqualsTo(SystemVersion.R) ->
// private static native long nativeLoad(@FormatType int format, @NonNull String path,
// @PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException;
HiddenApiBypass.getDeclaredMethod(
ApkAssetsClass, "nativeLoad",
IntType, StringClass, IntType, classOf<AssetsProvider>()
).apply { isAccessible = true }.invoke(null, FORMAT_APK, sourceDir, PROPERTY_SYSTEM, null)
SystemVersion.isHighOrEqualsTo(SystemVersion.P) ->
// private static native long nativeLoad(
// @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
// throws IOException;
HiddenApiBypass.getDeclaredMethod(
ApkAssetsClass, "nativeLoad",
StringClass, BooleanType, BooleanType, BooleanType
).apply { isAccessible = true }.invoke(null, sourceDir, false, false, false)
else -> error("Unsupported Android version.")
} as? Long? ?: error("Failed to create ApkAssets.")
blockParser = HiddenApiBypass.getDeclaredConstructor(XmlBlockClass, AssetManagerClass, LongType)
.apply { isAccessible = true }
.newInstance(null, xmlBlock) as? AutoCloseable? ?: error("Failed to create XmlBlock\$Parser.")
isInitOnce = true
}
/**
* Create a new parser.
* @param context the context.
* @param resId the style resource id, default is [Android_R.style.Widget].
* @return [XmlResourceParser]
*/
fun newParser(context: Context, @StyleRes resId: Int = Android_R.style.Widget): XmlResourceParser {
/**
* Create a view [AttributeSet].
* @return [XmlResourceParser]
*/
fun createViewAttrs() = context.layoutInflater.inflateOrNull<HikageAttrsView>(R.layout.layout_hikage_attrs_view)?.attrs
as? XmlResourceParser? ?: error("Failed to create AttributeSet.")
return if (SystemVersion.isHighOrEqualsTo(SystemVersion.P)) {
if (!isInitOnce) return createViewAttrs()
require(blockParser != null) { "Hikage initialization failed." }
HiddenApiBypass.getDeclaredMethod(XmlBlockClass, "newParser", IntType)
.apply { isAccessible = true }
.invoke(blockParser, resId) as? XmlResourceParser? ?: error("Failed to create parser.")
} else createViewAttrs()
}
}

View File

@@ -0,0 +1,953 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/25.
*/
@file:Suppress(
"unused", "FunctionName", "PropertyName", "ConstPropertyName", "UNCHECKED_CAST",
"MemberVisibilityCanBePrivate", "TOPLEVEL_TYPEALIASES_ONLY", "NON_PUBLIC_CALL_FROM_PUBLIC_INLINE"
)
package com.highcapable.hikage.core
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.FontRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap
import androidx.viewbinding.ViewBinding
import com.highcapable.betterandroid.ui.extension.binding.ViewBinding
import com.highcapable.betterandroid.ui.extension.component.base.DisplayDensity
import com.highcapable.betterandroid.ui.extension.component.base.getColorCompat
import com.highcapable.betterandroid.ui.extension.component.base.getColorStateListCompat
import com.highcapable.betterandroid.ui.extension.component.base.getDrawableCompat
import com.highcapable.betterandroid.ui.extension.component.base.getFontCompat
import com.highcapable.betterandroid.ui.extension.component.base.toDp
import com.highcapable.betterandroid.ui.extension.component.base.toPx
import com.highcapable.betterandroid.ui.extension.view.LayoutParamsWrapContent
import com.highcapable.betterandroid.ui.extension.view.ViewLayoutParams
import com.highcapable.betterandroid.ui.extension.view.inflate
import com.highcapable.betterandroid.ui.extension.view.layoutInflater
import com.highcapable.hikage.annotation.Hikageable
import com.highcapable.hikage.bypass.XmlBlockBypass
import com.highcapable.hikage.core.Hikage.LayoutParamsBody
import com.highcapable.hikage.core.base.HikageFactory
import com.highcapable.hikage.core.base.HikageFactoryBuilder
import com.highcapable.hikage.core.base.HikagePerformer
import com.highcapable.hikage.core.base.HikageView
import com.highcapable.hikage.core.base.PerformerException
import com.highcapable.hikage.core.base.ProvideException
import com.highcapable.hikage.core.extension.ResourcesScope
import com.highcapable.yukireflection.factory.buildOf
import com.highcapable.yukireflection.factory.classOf
import com.highcapable.yukireflection.factory.constructor
import com.highcapable.yukireflection.factory.current
import com.highcapable.yukireflection.factory.notExtends
import com.highcapable.yukireflection.type.android.AttributeSetClass
import com.highcapable.yukireflection.type.android.ContextClass
import com.highcapable.yukireflection.type.android.ViewGroup_LayoutParamsClass
import com.highcapable.yukireflection.type.java.IntType
import java.io.Serializable
import java.lang.reflect.Constructor
import java.util.concurrent.atomic.AtomicInteger
/**
* The Hikage core layout builder.
*
* A [Hikage] can have multiple levels of [Hikage.Performer].
* @param factories the factories to customize the custom view in the initialization.
*/
class Hikage private constructor(private val factories: List<HikageFactory>) {
companion object {
/** The Android widget class prefix. */
internal const val ANDROID_WIDGET_CLASS_PREFIX = "android.widget."
/** The unspecified layout params value. */
private const val LayoutParamsUnspecified = LayoutParamsWrapContent - 1
/** The view constructors map. */
private val viewConstructors = mutableMapOf<String, ViewConstructor>()
/** The view atomic id. */
private val viewAtomicId = AtomicInteger(0x7F00000)
/** Returns nothing. */
private fun noGetter(): Nothing = error("No getter available.")
/**
* Automatically add [HikageFactory] to handle [LayoutInflater.Factory2].
*
* This [LayoutInflater] will be retrieved from the [Context] you passed in.
*
* This option is enabled by default and will add this feature when creating any [Hikage].
* It can support the [View] autoboxing feature that supports libraries such as `androidx.appcompat`,
* which can be disabled if you don't need it.
*/
var isAutoProcessWithFactory2 = true
/**
* Create a new [Hikage].
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage]
*/
@JvmName("createTyped")
inline fun <reified LP : ViewGroup.LayoutParams> create(
context: Context,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<LP>
) = create(classOf<LP>(), context, parent, attachToParent, factory, performer)
/**
* Create a new [Hikage].
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage]
*/
inline fun create(
context: Context,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<ViewGroup.LayoutParams>
) = create(ViewGroup_LayoutParamsClass, context, parent, attachToParent, factory, performer)
/**
* Create a new [Hikage].
* @param lpClass the layout params type.
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage]
*/
inline fun <LP : ViewGroup.LayoutParams> create(
lpClass: Class<LP>,
context: Context,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<LP>
) = Hikage(HikageFactoryBuilder.create {
if (isAutoProcessWithFactory2) add(HikageFactory(context.layoutInflater))
factory()
}.build()).apply {
// If the parent view is specified and mark as attach to parent,
// add it directly to the first position.
if (parent != null && attachToParent) {
val parentId = generateRandomViewId()
provideView(parent, parentId)
}; newPerformer(lpClass, parent, attachToParent, context).apply(performer)
}
/**
* Create a new [Hikage.Delegate].
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage.Delegate]<[LP]>
*/
@JvmName("buildTyped")
inline fun <reified LP : ViewGroup.LayoutParams> build(
noinline factory: HikageFactoryBuilder.() -> Unit = {},
noinline performer: HikagePerformer<LP>
) = build(classOf<LP>(), factory, performer)
/**
* Create a new [Hikage.Delegate].
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage.Delegate]<[ViewGroup.LayoutParams]>
*/
fun build(
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<ViewGroup.LayoutParams>
) = build(ViewGroup_LayoutParamsClass, factory, performer)
/**
* Create a new [Hikage.Delegate].
* @param lpClass the layout params type.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage.Delegate]<[LP]>
*/
fun <LP : ViewGroup.LayoutParams> build(
lpClass: Class<LP>,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<LP>
) = Delegate(lpClass, factory, performer)
}
/** The [Hikage] layout params body type. */
private typealias LayoutParamsBody<LP> = LP.() -> Unit
/**
* The [Hikage.Performer] scope interface.
*/
private interface PerformerScope : DisplayDensity, ResourcesScope {
/**
* Get the actual view id by [id].
* @param id the view id.
* @return [Int] or -1.
*/
fun actualViewId(id: String): Int
}
/**
* The view constructor class.
* @param instance the constructor instance.
* @param parameterCount the parameter count.
*/
private inner class ViewConstructor(
private val instance: Constructor<*>,
private val parameterCount: Int
) {
/**
* Build the view.
* @param context the context.
* @param attrs the attribute set.
* @return [V] or null.
*/
fun <V : View> build(context: Context, attrs: AttributeSet) = when (parameterCount) {
2 -> instance.newInstance(context, attrs)
1 -> instance.newInstance(context)
else -> null
} as? V?
}
/** The performer set. */
private val performers = linkedSetOf<Performer<*>>()
/** The view map. */
private val views = linkedMapOf<String, View>()
/** The view id map. */
private val viewIds = mutableMapOf<String, Int>()
/**
* Get the root view.
* @return [View]
*/
val root get() = views.values.firstOrNull()
?: throw PerformerException("No root view found, are you sure you have provided any view?")
/**
* Get the root view [V].
* @return [V]
*/
inline fun <reified V : View> root() = root as? V?
?: throw PerformerException("Root view is not a type of ${classOf<V>().name}.")
/**
* Get the view by [id].
* @param id the view id.
* @return [View]
*/
operator fun get(id: String) = views[id]
?: throw PerformerException("View with id \"$id\" not found.")
/**
* Get the view by [id].
* @param id the view id.
* @return [View] or null.
*/
fun getOrNull(id: String) = views[id]
/**
* Get the view by [id] via [V].
* @param id the view id.
* @return [V]
*/
@JvmName("getTyped")
inline fun <reified V : View> get(id: String) = get(id) as? V
?: throw PerformerException("View with id \"$id\" is not a ${classOf<V>().name}.")
/**
* Get the view by [id] via [V].
* @param id the view id.
* @return [V] or null.
*/
@JvmName("getOrNullTyped")
inline fun <reified V : View> getOrNull(id: String) = getOrNull(id) as? V?
/**
* Get the actual view id by [id].
* @param id the view id.
* @return [Int] or -1.
*/
fun getActualViewId(id: String) = viewIds[id] ?: -1
/**
* Create a new [View] via [V].
* @param viewClass the view class.
* @param id the view id, generated by default.
* @param context the context.
* @return [V]
*/
private fun <V : View> createView(viewClass: Class<V>, 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
}
/**
* Get the view constructor.
* @param viewClass the view class.
* @return [ViewConstructor] or null.
*/
private fun <V : View> getViewConstructor(viewClass: Class<V>) =
viewConstructors[viewClass.name] ?: run {
var parameterCount = 0
val twoParams = viewClass.constructor {
param(ContextClass, AttributeSetClass)
}.ignored().give()
val onceParam = viewClass.constructor {
param(ContextClass)
}.ignored().give()
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
}
/**
* Create a new [View] from [HikageFactory].
* @param viewClass the view class.
* @param id the view id.
* @param context the context.
* @param attrs the attribute set.
* @return [V] or null.
*/
private fun <V : View> createViewFromFactory(viewClass: Class<V>, 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<View>)
val view = factory(parent, processed, context, params)
if (view != null && view.javaClass notExtends 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
}
/**
* Provide an exists [View].
* @param view the view instance.
* @param id the view id.
* @return [String]
*/
private fun provideView(view: View, id: String?): String {
val (requireId, viewId) = generateViewId(id)
view.id = viewId
views[requireId] = view
return requireId
}
/**
* Generate view id from [id].
* @param id the view id.
* @return [Pair]<[String], [Int]>
*/
private fun generateViewId(id: String?): Pair<String, Int> {
/**
* Generate a new view id.
* @param id the view id.
* @return [Int]
*/
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
}
/**
* Generate random view id.
* @return [String]
*/
private fun generateRandomViewId() = "anonymous@${viewAtomicId.getAndIncrement().toHexString()}"
/**
* We just need a [AttributeSet] instance.
* @param context the context.
* @return [AttributeSet]
*/
private fun createAttributeSet(context: Context): AttributeSet = XmlBlockBypass.newParser(context)
/**
* Start a new performer [LP].
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param context the context, priority is given to [parent]'s context.
* @return [Performer]
*/
private inline fun <reified LP : ViewGroup.LayoutParams> newPerformer(
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
context: Context? = null
) = newPerformer(classOf<LP>(), parent, attachToParent, context)
/**
* Start a new performer [LP].
* @param lpClass the layout params type.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param context the context, priority is given to [parent]'s context.
* @return [Performer]
*/
private fun <LP : ViewGroup.LayoutParams> newPerformer(
lpClass: Class<LP>,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
context: Context? = null
) = Performer(lpClass, parent, attachToParent, context).apply {
// Init [XmlBlockBypass] if the context is not null.
context?.let { XmlBlockBypass.init(it) }
performers.add(this)
}
/**
* Require no [Performer].
* @param name the process name.
* @param block the block.
*/
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."
)
}
/**
* Include a delegate.
* @param delegate the delegate.
* @param context the context.
* @param embedded the embedded flag.
* @return [Hikage]
*/
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
}
/**
* The core performer entity of layout build [LP].
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent.
* @param lpClass the layout params type.
* @param baseContext the context to create the layout, priority is given to [parent]'s context.
* if [parent] is null, it must be set manually.
*/
inner class Performer<LP : ViewGroup.LayoutParams> internal constructor(
private val lpClass: Class<LP>,
internal val parent: ViewGroup?,
private val attachToParent: Boolean,
private val baseContext: Context? = null
) : PerformerScope {
/** The current [Hikage]. */
private val current get() = this@Hikage
/**
* The context to create the layout.
* @return [Context]
*/
private val context get() = parent?.context
?: baseContext
?: throw PerformerException("Parent layout is null or broken, Hikage.Performer need a Context to create the layout.")
override fun actualViewId(id: String) = getActualViewId(id)
override fun stringResource(@StringRes resId: Int, vararg formatArgs: Any) =
if (formatArgs.isNotEmpty())
context.getString(resId, *formatArgs)
else context.getString(resId)
override fun colorResource(@ColorRes resId: Int) = context.getColorCompat(resId)
override fun stateColorResource(@ColorRes resId: Int) = context.getColorStateListCompat(resId)
override fun drawableResource(@DrawableRes resId: Int) = context.getDrawableCompat(resId)
override fun bitmapResource(@DrawableRes resId: Int) = context.getDrawableCompat(resId).toBitmap()
override fun dimenResource(@DimenRes resId: Int) = context.resources.getDimension(resId)
override fun fontResource(@FontRes resId: Int) = context.getFontCompat(resId)
override fun <N : Number> N.toPx() = toPx(context)
override fun <N : Number> N.toDp() = toDp(context)
/** The count of providing views. */
private var provideCount = 0
/**
* Provide a new [View] instance [V].
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @param init the view initialization body.
* @return [V]
*/
@Hikageable
@JvmName("ViewTyped")
inline fun <reified V : View> View(
lparams: LayoutParams? = null,
id: String? = null,
init: HikageView<V> = {}
): V {
val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams)
val view = createView(classOf<V>(), id, context)
view.layoutParams = lpDelegate.create()
requireNoPerformers(classOf<V>().name) { view.init() }
startProvide<V>(id)
addToParentIfRequired(view)
return view
}
/**
* Provide a new [View] instance.
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @param init the view initialization body.
* @return [View]
*/
@Hikageable
inline fun View(
lparams: LayoutParams? = null,
id: String? = null,
init: HikageView<View> = {}
) = View<View>(lparams, id, init)
/**
* Provide a new [ViewGroup] instance [VG].
*
* Provide the new type of [ViewGroup.LayoutParams] down via [LP].
*
* - Note: The [VG] must be inherited from [ViewGroup].
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @param init the view initialization body.
* @param performer the performer body.
* @return [VG]
*/
@Hikageable
@JvmName("ViewGroupLP")
inline fun <reified VG : ViewGroup, reified LP : ViewGroup.LayoutParams> ViewGroup(
lparams: LayoutParams? = null,
id: String? = null,
init: HikageView<VG> = {},
performer: HikagePerformer<LP> = {}
): VG {
val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams)
val view = createView(classOf<VG>(), id, context)
view.layoutParams = lpDelegate.create()
requireNoPerformers(classOf<VG>().name) { view.init() }
startProvide<VG>(id)
addToParentIfRequired(view)
newPerformer<LP>(view).apply(performer)
return view
}
/**
* Provide a new [ViewGroup] instance [VG].
*
* - Note: The [VG] must be inherited from [ViewGroup].
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @param init the view initialization body.
* @param performer the performer body.
* @return [VG]
*/
@Hikageable
inline fun <reified VG : ViewGroup> ViewGroup(
lparams: LayoutParams? = null,
id: String? = null,
init: HikageView<VG> = {},
performer: HikagePerformer<ViewGroup.LayoutParams> = {}
) = ViewGroup<VG, ViewGroup.LayoutParams>(lparams, id, init, performer)
/**
* Provide layout from [resId].
* @param resId the layout resource id.
* @param lparams the view layout params.
* @param id the view id, generated by default.
*/
@Hikageable
fun Layout(
@LayoutRes resId: Int,
lparams: LayoutParams? = null,
id: String? = null
): View {
val view = context.layoutInflater.inflate(resId, parent, attachToRoot = false)
startProvide<View>(id, view)
lparams?.create()?.let { view.layoutParams = it }
provideView(view, id)
addToParentIfRequired(view)
return view
}
/**
* Provide layout from [ViewBinding].
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @return [VB]
*/
@Hikageable
inline fun <reified VB : ViewBinding> Layout(
lparams: LayoutParams? = null,
id: String? = null
): VB {
val viewBinding = ViewBinding<VB>().inflate(context.layoutInflater, parent, attachToParent = false)
val view = viewBinding.root
startProvide<View>(id, view)
if (view.parent != null) throw ProvideException(
"The ViewBinding($view) already has a parent, " +
"it may have been created using layout root node <merge> or <include>, cannot be provided."
)
lparams?.create()?.let { view.layoutParams = it }
provideView(view, id)
addToParentIfRequired(view)
return viewBinding
}
/**
* Provide layout from exists [View].
* @param view the view instance.
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @return [View]
*/
@Hikageable
fun Layout(
view: View,
lparams: LayoutParams? = null,
id: String? = null
): View {
if (view.parent != null) throw ProvideException("The view $view already has a parent, cannot be provided.")
startProvide<View>(id, view)
val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams)
view.layoutParams = lpDelegate.create()
provideView(view, id)
addToParentIfRequired(view)
return view
}
/**
* Provide layout from another [Hikage].
*
* - Note: [Hikage] view IDs will not copied to this layout
* when provides a complete [Hikage] instead of [Delegate].
*
* ```kotlin
* val first: Hikage = Hikageable(context) {
* TextView(id = "textView")
* }
* val second: Hikage = Hikageable(context) {
* TextView(id = "textView")
* // The view ID "textView" will not copied to this layout,
* // so there will be no problem of ID overlap.
* Layout(first)
* }
* ```
* @param hikage the hikage instance.
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @return [Hikage]
*/
@Hikageable
fun Layout(
hikage: Hikage,
lparams: LayoutParams? = null,
id: String? = null
): Hikage {
val view = hikage.root
startProvide<View>(id, 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
}
/**
* Provide layout from another [Delegate].
*
* - Note: If you use [embedded], you must make sure that the introduced view IDs
* do not overlap with the view IDs in the current layout.
* @param delegate the delegate instance.
* @param lparams the view layout params.
* @param id the view id, generated by default.
* @param embedded whether to embed this layout (All view IDs and performers will be copied),
* default is true.
* @return [Hikage]
*/
@Hikageable
fun Layout(
delegate: Delegate<*>,
lparams: LayoutParams? = null,
id: String? = null,
embedded: Boolean = true
): Hikage {
val hikage = include(delegate, context, embedded)
return Layout(hikage, lparams, id)
}
/**
* Create a new layout params.
*
* Usage:
*
* ```kotlin
* View(
* lparams = LayoutParams {
* // You code here.
* }
* )
* ```
* @see ViewLayoutParams
* @param body the layout params body.
* @return [LayoutParams]
*/
fun LayoutParams(
width: Int = LayoutParamsUnspecified,
height: Int = LayoutParamsUnspecified,
matchParent: Boolean = false,
widthMatchParent: Boolean = false,
heightMatchParent: Boolean = false,
body: LayoutParamsBody<LP> = {}
) = LayoutParams.from(
current, lpClass, parent,
width, height, matchParent, widthMatchParent, heightMatchParent,
body = body
)
/** If required, add the [view] to the [parent]. */
private fun addToParentIfRequired(view: View) {
if (attachToParent) parent?.addView(view)
}
/**
* Call to start providing a new view.
* @param id the view id.
* @param view the view instance.
*/
private inline fun <reified V : View> startProvide(id: String?, view: V? = null) {
provideCount++
if (provideCount > 1 && (parent == null || !attachToParent)) throw ProvideException(
"Provide view ${view?.javaClass ?: classOf<V>()}(${id?.let { "\"$it\""} ?: "<anonymous>"}) failed. ${
if (parent == null) "No parent view group found"
else "Parent view group declares attachToParent = false"
}, you can only provide one view for the root view."
)
}
}
/**
* The parameters of the [Performer].
* @param id the view ID.
* @param attrs the attributes set.
* @param viewClass the view class.
*/
data class PerformerParams internal constructor(
val id: String?,
val attrs: AttributeSet,
val viewClass: Class<View>
) : Serializable
/**
* The [Hikage] layout params.
* @see ViewLayoutParams
* @param current the current [Hikage].
* @param lpClass the layout params type.
* @param parent the parent view group.
*/
class LayoutParams private constructor(
private val current: Hikage,
private val lpClass: Class<ViewGroup.LayoutParams>,
private val parent: ViewGroup?
) {
/**
* Builder params of body.
*/
private class BodyBuilder(
val width: Int,
val height: Int,
val matchParent: Boolean,
val widthMatchParent: Boolean,
val heightMatchParent: Boolean,
val body: LayoutParamsBody<ViewGroup.LayoutParams>
)
/**
* Builder params of wrapper.
*/
private class WrapperBuilder(
val delegate: LayoutParams?,
val lparams: ViewGroup.LayoutParams?
)
/** The layout params body. */
private var bodyBuilder: BodyBuilder? = null
/** The layout params wrapper. */
private var wrapperBuilder: WrapperBuilder? = null
internal companion object {
/**
* Create a new [LayoutParams]
* @see ViewLayoutParams
* @param current the current [Hikage].
* @param parent the parent view group.
* @return [LayoutParams]
*/
fun <LP : ViewGroup.LayoutParams> from(
current: Hikage,
lpClass: Class<LP>,
parent: ViewGroup?,
width: Int,
height: Int,
matchParent: Boolean,
widthMatchParent: Boolean,
heightMatchParent: Boolean,
body: LayoutParamsBody<LP>
) = LayoutParams(current, lpClass as Class<ViewGroup.LayoutParams>, parent).apply {
bodyBuilder = BodyBuilder(
width, height, matchParent, widthMatchParent, heightMatchParent,
body as LayoutParamsBody<ViewGroup.LayoutParams>
)
}
/**
* Create a new [LayoutParams].
* @param current the current [Hikage].
* @param parent the parent view group.
* @param delegate the delegate.
* @param lparams the another layout params.
* @return [LayoutParams]
*/
fun <LP : ViewGroup.LayoutParams> from(
current: Hikage,
lpClass: Class<LP>,
parent: ViewGroup?,
delegate: LayoutParams?,
lparams: ViewGroup.LayoutParams? = null
) = LayoutParams(current, lpClass as Class<ViewGroup.LayoutParams>, parent).apply {
wrapperBuilder = WrapperBuilder(delegate, lparams)
}
}
/**
* Create a default layout params.
* @return [ViewGroup.LayoutParams]
*/
private fun createDefaultLayoutParams(lparams: ViewGroup.LayoutParams? = null): ViewGroup.LayoutParams {
val wrapped = lparams?.let {
parent?.current(ignored = true)?.method {
name = "generateLayoutParams"
param(ViewGroup_LayoutParamsClass)
superClass()
}?.invoke<ViewGroup.LayoutParams?>(it)
} ?: lparams
return wrapped
// Build a default.
?: lpClass.buildOf<ViewGroup.LayoutParams>(LayoutParamsWrapContent, LayoutParamsWrapContent) {
param(IntType, IntType)
} ?: throw PerformerException("Create default layout params failed.")
}
/**
* Create the layout params.
* @return [ViewGroup.LayoutParams]
*/
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.")
}
}
/**
* The delegate for [Hikage].
* @param lpClass the layout params type.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
*/
class Delegate<LP : ViewGroup.LayoutParams> internal constructor(
private val lpClass: Class<LP>,
private val factory: HikageFactoryBuilder.() -> Unit = {},
private val performer: HikagePerformer<LP>
) {
/**
* Create a new [Hikage].
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @return [Hikage]
*/
fun create(context: Context, parent: ViewGroup? = null, attachToParent: Boolean = parent != null) =
create(lpClass, context, parent, attachToParent, factory, performer)
}
}

View File

@@ -0,0 +1,36 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/26.
*/
@file:JvmName("ExceptionsUtils")
package com.highcapable.hikage.core.base
/**
* The exception of performing view.
* @param message the exception message.
*/
internal class PerformerException(message: String) : Exception(message)
/**
* The exception of providing view.
* @param message the exception message.
*/
internal class ProvideException(message: String) : Exception(message)

View File

@@ -0,0 +1,106 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/4.
*/
@file:Suppress("unused", "FunctionName")
@file:JvmName("HikageFactory")
package com.highcapable.hikage.core.base
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.highcapable.hikage.core.Hikage
/**
* The [Hikage] factory type.
*
* - `parent` the parent view group.
* - `base` the base view (from previous [HikageFactory] processed, if not will null).
* - `context` the view context.
* - `params` the parameters.
*/
typealias HikageFactory = (parent: ViewGroup?, base: View?, context: Context, params: Hikage.PerformerParams) -> View?
/**
* Create a [Hikage] factory.
* @param createView the view creator.
* @return [HikageFactory]
*/
@JvmSynthetic
fun HikageFactory(createView: HikageFactory) = createView
/**
* Create a [Hikage] factory from [LayoutInflater].
*
* This will proxy the function of [LayoutInflater.Factory2] to [Hikage].
* @param inflater the layout inflater.
* @return [HikageFactory]
*/
@JvmSynthetic
fun HikageFactory(inflater: LayoutInflater) = HikageFactory { parent, base, context, params ->
base ?: inflater.factory2?.onCreateView(parent, params.viewClass.name.let {
if (it.startsWith(Hikage.ANDROID_WIDGET_CLASS_PREFIX))
it.replace(Hikage.ANDROID_WIDGET_CLASS_PREFIX, "")
else it
}, context, params.attrs)
}
/**
* The [HikageFactory] builder.
*/
class HikageFactoryBuilder private constructor() {
internal companion object {
/**
* Create a [HikageFactoryBuilder].
* @param builder the builder.
* @return [HikageFactoryBuilder]
*/
inline fun create(builder: HikageFactoryBuilder.() -> Unit) = HikageFactoryBuilder().apply(builder)
}
/** Caches factories. */
private val factories = mutableListOf<HikageFactory>()
/**
* Add a factory.
* @param factory the factory.
*/
fun add(factory: HikageFactory) {
factories.add(factory)
}
/**
* Add factories.
* @param factories the factories.
*/
fun addAll(factories: List<HikageFactory>) {
this.factories.addAll(factories)
}
/**
* Build the factory.
* @return <[List]>[HikageFactory]
*/
internal fun build() = factories.toList()
}

View File

@@ -0,0 +1,101 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/25.
*/
@file:Suppress("unused", "FunctionName", "NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
@file:JvmName("HikageableUtils")
package com.highcapable.hikage.core.base
import android.content.Context
import android.view.ViewGroup
import com.highcapable.hikage.core.Hikage
/**
* The [Hikage.Performer] type.
*/
typealias HikagePerformer<LP> = Hikage.Performer<LP>.() -> Unit
/**
* The [Hikage] view scope type.
*/
typealias HikageView<V> = V.() -> Unit
/**
* Start performing a [Hikage] layout [LP].
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage]
*/
@JvmSynthetic
@JvmName("HikageableTyped")
inline fun <reified LP : ViewGroup.LayoutParams> Hikageable(
context: Context,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<LP>
) = Hikage.create(context, parent, attachToParent, factory, performer)
/**
* Start performing a [Hikage] layout.
* @param context the context to create the layout.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage]
*/
@JvmSynthetic
inline fun Hikageable(
context: Context,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null,
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<ViewGroup.LayoutParams>
) = Hikage.create(context, parent, attachToParent, factory, performer)
/**
* Start performing a [Hikage] layout [LP].
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage.Delegate]<[LP]>
*/
@JvmSynthetic
@JvmName("HikageableTyped")
inline fun <reified LP : ViewGroup.LayoutParams> Hikageable(
noinline factory: HikageFactoryBuilder.() -> Unit = {},
noinline performer: HikagePerformer<LP>
) = Hikage.build<LP>(factory, performer)
/**
* Start performing a [Hikage] layout.
* @param factory the [HikageFactory] builder.
* @param performer the performer body.
* @return [Hikage.Delegate]<[ViewGroup.LayoutParams]>
*/
@JvmSynthetic
fun Hikageable(
factory: HikageFactoryBuilder.() -> Unit = {},
performer: HikagePerformer<ViewGroup.LayoutParams>
) = Hikage.build(factory, performer)

View File

@@ -0,0 +1,106 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/6.
*/
@file:Suppress("unused")
@file:JvmName("HikageBuilderUtils")
package com.highcapable.hikage.core.builder
import android.content.Context
import android.view.ViewGroup
import com.highcapable.hikage.core.Hikage
/**
* Lazy initialize a [Hikage] layout.
*
* Usage:
*
* ```kotlin
* object MainLayout : HikageBuilder {
*
* override fun build() = Hikageable {
* LinearLayout(
* lparams = LayoutParams(matchParent = true),
* init = {
* orientation = LinearLayout.VERTICAL
* gravity = Gravity.CENTER
* }
* ) {
* TextView {
* text = "Hello, Hikage!"
* textSize = 20f
* }
* }
* }
* }
*
* class MainActivity : AppCompatActivity() {
*
* private val hikage by lazyHikage(MainLayout)
*
* override fun onCreate(savedInstanceState: Bundle?) {
* super.onCreate(savedInstanceState)
* setContentView(hikage)
* }
* }
* ```
* @receiver the context to create the layout.
* @param builder the [HikageBuilder] instance.
* @param parent the parent view group.
* @param attachToParent whether to attach the layout to the parent when the [parent] is filled.
* @return [Lazy]<[Hikage]>
*/
@JvmSynthetic
fun Context.lazyHikage(
builder: HikageBuilder,
parent: ViewGroup? = null,
attachToParent: Boolean = parent != null
) = lazy { builder.build().create(context = this, parent, attachToParent) }
/**
* A layout builder interface for [Hikage].
*
* Checking [build] for usage.
*/
interface HikageBuilder {
/**
* Provide a [Hikage] for builder.
*
* Usage:
*
* ```kotlin
* override fun build() = Hikageable {
* TextView(
* lparams = LayoutParams(
* width = 100.dp,
* height = 100.dp
* )
* ) {
* text = "Hello, Hikage!"
* textSize = 20f
* }
* }
* ```
* @return [Hikage.Delegate]
*/
fun build(): Hikage.Delegate<*>
}

View File

@@ -0,0 +1,89 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/3/12.
*/
package com.highcapable.hikage.core.extension
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.FontRes
import androidx.annotation.StringRes
import com.highcapable.hikage.core.Hikage
/**
* An extension for [Hikage] view scope to provide resources.
*/
interface ResourcesScope {
/**
* Get the string from [resId].
* @param resId the resource id.
* @param formatArgs the format arguments.
* @return [String]
*/
fun stringResource(@StringRes resId: Int, vararg formatArgs: Any): String
/**
* Get the color from [resId].
* @param resId the resource id.
* @return [Int]
*/
fun colorResource(@ColorRes resId: Int): Int
/**
* Get the [ColorStateList] from [resId].
* @param resId the resource id.
* @return [Int]
*/
fun stateColorResource(@ColorRes resId: Int): ColorStateList
/**
* Get the [Drawable] from [resId].
* @param resId the resource id.
* @return [Drawable]
*/
fun drawableResource(@DrawableRes resId: Int): Drawable
/**
* Get the [Bitmap] from [resId].
* @param resId the resource id.
* @return [Bitmap]
*/
fun bitmapResource(@DrawableRes resId: Int): Bitmap
/**
* Get the dimension from [resId].
* @param resId the resource id.
* @return [Float]
*/
fun dimenResource(@DimenRes resId: Int): Float
/**
* Get the font from [resId].
* @param resId the resource id.
* @return [Typeface] or null.
*/
fun fontResource(@FontRes resId: Int): Typeface?
}

View File

@@ -0,0 +1,53 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/27.
*/
@file:Suppress("unused", "FunctionName")
package com.highcapable.hikage.core.preview
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import com.highcapable.hikage.core.Hikage
import com.highcapable.hikage.core.builder.HikageBuilder
/**
* A preview layout for [Hikage].
*
* - Note: This class should only be used in the layout preview mode by [View.isInEditMode].
*/
abstract class HikagePreview(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs), HikageBuilder {
init {
require(isInEditMode) {
"HikagePreview should only be used in the layout preview mode."
}
}
@CallSuper
override fun onAttachedToWindow() {
super.onAttachedToWindow()
removeAllViews()
build().create(context, parent = this)
}
}

View File

@@ -0,0 +1,216 @@
/*
* Hikage - An Android responsive UI building tool.
* Copyright (C) 2019 HighCapable
* https://github.com/BetterAndroid/Hikage
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/2/26.
*/
@file:Suppress("unused", "DEPRECATION")
@file:JvmName("WidgetsDeclaration")
package com.highcapable.hikage.widget.android
import android.widget.AbsListView
import android.widget.ActionMenuView
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.CalendarView
import android.widget.CheckBox
import android.widget.CheckedTextView
import android.widget.Chronometer
import android.widget.DatePicker
import android.widget.EditText
import android.widget.ExpandableListView
import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.GridView
import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.ImageSwitcher
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.MediaController
import android.widget.NumberPicker
import android.widget.ProgressBar
import android.widget.QuickContactBadge
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.RatingBar
import android.widget.RelativeLayout
import android.widget.ScrollView
import android.widget.SearchView
import android.widget.SeekBar
import android.widget.Space
import android.widget.Spinner
import android.widget.Switch
import android.widget.TabHost
import android.widget.TabWidget
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextClock
import android.widget.TextSwitcher
import android.widget.TextView
import android.widget.TimePicker
import android.widget.ToggleButton
import android.widget.Toolbar
import android.widget.VideoView
import android.widget.ViewAnimator
import android.widget.ViewFlipper
import android.widget.ViewSwitcher
import com.highcapable.hikage.annotation.HikageViewDeclaration
@HikageViewDeclaration(SeekBar::class)
private object SeekBarDeclaration
@HikageViewDeclaration(LinearLayout::class, LinearLayout.LayoutParams::class)
private object LinearLayoutDeclaration
@HikageViewDeclaration(RelativeLayout::class, RelativeLayout.LayoutParams::class)
private object RelativeLayoutDeclaration
@HikageViewDeclaration(FrameLayout::class, FrameLayout.LayoutParams::class)
private object FrameLayoutDeclaration
@HikageViewDeclaration(ScrollView::class, FrameLayout.LayoutParams::class)
private object ScrollViewDeclaration
@HikageViewDeclaration(ProgressBar::class)
private object ProgressBarDeclaration
@HikageViewDeclaration(Chronometer::class)
private object ChronometerDeclaration
@HikageViewDeclaration(Space::class)
private object SpaceDeclaration
@HikageViewDeclaration(CheckedTextView::class)
private object CheckedTextViewDeclaration
@HikageViewDeclaration(ExpandableListView::class, AbsListView.LayoutParams::class)
private object ExpandableListViewDeclaration
@HikageViewDeclaration(Spinner::class)
private object SpinnerDeclaration
@HikageViewDeclaration(RadioGroup::class, RadioGroup.LayoutParams::class)
private object RadioGroupDeclaration
@HikageViewDeclaration(RadioButton::class)
private object RadioButtonDeclaration
@HikageViewDeclaration(ToggleButton::class)
private object ToggleButtonDeclaration
@HikageViewDeclaration(CheckBox::class)
private object CheckBoxDeclaration
@HikageViewDeclaration(EditText::class)
private object EditTextDeclaration
@HikageViewDeclaration(AutoCompleteTextView::class)
private object AutoCompleteTextViewDeclaration
@HikageViewDeclaration(Button::class)
private object ButtonDeclaration
@HikageViewDeclaration(ImageButton::class)
private object ImageButtonDeclaration
@HikageViewDeclaration(TextView::class)
private object TextViewDeclaration
@HikageViewDeclaration(TextClock::class)
private object TextClockDeclaration
@HikageViewDeclaration(TextSwitcher::class, FrameLayout.LayoutParams::class)
private object TextSwitcherDeclaration
@HikageViewDeclaration(ActionMenuView::class, ActionMenuView.LayoutParams::class)
private object ActionMenuViewDeclaration
@HikageViewDeclaration(CalendarView::class, FrameLayout.LayoutParams::class)
private object CalendarViewDeclaration
@HikageViewDeclaration(DatePicker::class, FrameLayout.LayoutParams::class)
private object DatePickerDeclaration
@HikageViewDeclaration(TimePicker::class, FrameLayout.LayoutParams::class)
private object TimePickerDeclaration
@HikageViewDeclaration(RatingBar::class)
private object RatingBarDeclaration
@HikageViewDeclaration(HorizontalScrollView::class, FrameLayout.LayoutParams::class)
private object HorizontalScrollViewDeclaration
@HikageViewDeclaration(QuickContactBadge::class)
private object QuickContactBadgeDeclaration
@HikageViewDeclaration(ImageSwitcher::class, FrameLayout.LayoutParams::class)
private object ImageSwitcherDeclaration
@HikageViewDeclaration(ViewSwitcher::class, FrameLayout.LayoutParams::class)
private object ViewSwitcherDeclaration
@HikageViewDeclaration(ViewFlipper::class, FrameLayout.LayoutParams::class)
private object ViewFlipperDeclaration
@HikageViewDeclaration(ViewAnimator::class, FrameLayout.LayoutParams::class)
private object ViewAnimatorDeclaration
@HikageViewDeclaration(VideoView::class)
private object VideoViewDeclaration
@HikageViewDeclaration(Toolbar::class, Toolbar.LayoutParams::class)
private object ToolbarDeclaration
@HikageViewDeclaration(GridLayout::class, GridLayout.LayoutParams::class)
private object GridLayoutDeclaration
@HikageViewDeclaration(GridView::class, AbsListView.LayoutParams::class)
private object GridViewDeclaration
@HikageViewDeclaration(ListView::class, AbsListView.LayoutParams::class)
private object ListViewDeclaration
@HikageViewDeclaration(ImageView::class)
private object ImageViewDeclaration
@HikageViewDeclaration(MediaController::class, FrameLayout.LayoutParams::class)
private object MediaControllerDeclaration
@HikageViewDeclaration(TableLayout::class, TableLayout.LayoutParams::class)
private object TableLayoutDeclaration
@HikageViewDeclaration(TableRow::class, TableRow.LayoutParams::class)
private object TableRowDeclaration
@HikageViewDeclaration(NumberPicker::class, LinearLayout.LayoutParams::class)
private object NumberPickerDeclaration
@HikageViewDeclaration(SearchView::class, FrameLayout.LayoutParams::class)
private object SearchViewDeclaration
@HikageViewDeclaration(Switch::class)
private object SwitchDeclaration
@HikageViewDeclaration(TabHost::class, FrameLayout.LayoutParams::class)
private object TabHostDeclaration
@HikageViewDeclaration(TabWidget::class, LinearLayout.LayoutParams::class)
private object TabWidgetDeclaration

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<com.highcapable.hikage.bypass.HikageAttrsView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

View File

@@ -0,0 +1,18 @@
package com.highcapable.hikage
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}