Kotlin / anko

Pleasant Android application development
Apache License 2.0
15.89k stars 1.29k forks source link

Set style? #16

Closed Leland-Takamine closed 7 years ago

Leland-Takamine commented 9 years ago

I can't seem to find a good way of setting a style for a view. For example if I wanted a large ProgressBar in XML I would write:

<ProgressBar
        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Large"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

How do you do this in anko?

yanex commented 9 years ago

Anko has a style() function, though traditional Android XML styles are unsupported now. Android SDK does not provide a good way to set View styles in code out of the box, and we are trying to find a way to support it.

sfunke commented 8 years ago

Inspired by the default Anko textView extension methods, and after doing some research on how to set a theme to a view by code, I came up with the following extension methods, which work very well so far (can be applied to any view class):

public inline fun ViewManager.textView(text: CharSequence?, styleRes: Int = 0, init: TextView.() -> Unit): TextView {
    return ankoView({ if (styleRes == 0) TextView(it) else TextView(ContextThemeWrapper(it, styleRes), null, 0) }) {
        init()
        setText(text)
    }
}

public fun ViewManager.textView(text: CharSequence?, styleRes: Int = 0): TextView = textView(text, styleRes) {}

Key is, you have to use ContextThemeWrapper, and the 3 argument constructor of the passed view, see this link: http://stackoverflow.com/a/28613069/1128600

That allows to pass an XML style, and have it fully applied to the newly created view (not comparable to the crappy setTextAppearance method)

@yanex Would it make sense to consider this for potential adoption into the standard library?

dennislysenko commented 8 years ago

@sfunke, does this only work for styling the text in a TextView, or is it generalized to applying any style resource to any view (including things like padding, backgrounds, borders, etc.?)

dennislysenko commented 8 years ago

Nevermind. I created this shim:

public inline fun ViewManager.styledButton(text: CharSequence?, styleRes: Int = 0, init: Button.() -> Unit): Button {
    return ankoView({ if (styleRes == 0) Button(it) else Button(ContextThemeWrapper(it, styleRes), null, 0) }) {
        init()
        setText(text)
    }
}

public fun ViewManager.styledButton(text: CharSequence?, styleRes: Int = 0): Button = styledButton(text, styleRes) {}

Works perfectly, I just call styledButton("text", R.style.MyStyle). Thanks @sfunke!

BennyWang commented 8 years ago

@sfunke I think

public inline fun ViewManager.AnyView(theme: Int = 0, init: TextView.() -> Unit): AnyView

is enough.

fboldog commented 8 years ago

https://github.com/Kotlin/anko/issues/143 ;)

BennyWang commented 8 years ago

@fboldog +1

simophin commented 8 years ago

Is there any way to set style? So now you can set theme for each widget, like AppTheme, AppTheme.Light, but what if you need to set individual styles, eg. Widget.Button, Widget.Button.Colored?

gregschlom commented 8 years ago

@simophin depends on what version you're targeting. on API 21+, Google added a 4th parameter to all Views constructors allowing you to set the style resource: https://developer.android.com/reference/android/view/View.html#View(android.content.Context,%20android.util.AttributeSet,%20int,%20int)

So if you're only targeting devices running 21+, it's easy to make custom Anko views calling that constructor. If you want to target devices running below 21, you're out of luck. There's no (easy) way to set the style programmatically because it's all private APIs used by the XML layout inflater.

What we ended up doing was to create very small XML layout files containing just <Button style="@style/WhiteButton" /> and then instantiating that layout with Anko's include<Button>(R.layout.white_button)

mvysny commented 8 years ago

Works great, thanks! If you wish to resolve attr value from Activity theme to style resource, just use this code:

val View.contextThemeWrapper: ContextThemeWrapper
get() = context.contextThemeWrapper

val Context.contextThemeWrapper: ContextThemeWrapper
get() = when (this) {
    is ContextThemeWrapper -> this
    is ContextWrapper -> baseContext.contextThemeWrapper
    else -> throw IllegalStateException("Context is not an Activity, can't get theme: $this")
}

@StyleRes
fun View.attrStyle(@AttrRes attrColor: Int): Int = contextThemeWrapper.attrStyle(attrColor)
@StyleRes
private fun ContextThemeWrapper.attrStyle(@AttrRes attrRes: Int): Int =
        attr(attrRes) {
            it.getResourceId(0, 0)
        }

private fun <R> ContextThemeWrapper.attr(@AttrRes attrRes: Int, block: (TypedArray)->R): R {
    val typedValue = TypedValue();
    if (!theme.resolveAttribute(attrRes, typedValue, true)) throw IllegalArgumentException("$attrRes is not resolvable")
    val a = obtainStyledAttributes(typedValue.data, intArrayOf(attrRes));
    val result = block(a)
    a.recycle()
    return result
}

then,

textView(attrStyle(R.attr.my_title)) {
}
hannesstruss commented 7 years ago

Resolving the attr does not actually work in all cases. Example:

button("My Button", theme = attrStyle(R.attr.buttonBarButtonStyle))

will not apply the correct background, since the one-argument constructor of Button that Anko uses calls through to the three-argument constructor with a default value for int defStyleAttr which overrides the button background with the default.

While working in some cases, the theme support of Anko 0.9 is not a full replacement for the style XML attribute. Can we reopen this issue?

yanex commented 7 years ago

@hannesstruss I've created the new issue for this: #361.

I also mark this issue as "closed". If you have other problems with themed views, please feel free to create the new issues.

ayinmursalin commented 6 years ago

now you can use horizontalProgressBar { }

so no need to set style horizontal to progressBar