Code Monkey home page Code Monkey logo

paris's Introduction

Paris

Paris lets you define and apply styles programmatically to Android views, including custom attributes.

  • Apply styles programmatically at any time.
  • Combine multiple styles together.
  • Create styles programmatically (as opposed to using XML).
  • Use annotations to easily support custom attributes (inspired by Barber).
  • Declare explicitly supported styles for your custom views.
  • And much more...

Installation

In your project's build.gradle:

dependencies {
    implementation 'com.airbnb.android:paris:2.0.0'
    // Apply the Paris processor if you're using Paris annotations for code gen.
    kapt 'com.airbnb.android:paris-processor:2.0.0'
    // or if you are using Kotlin Symbol Processing
    ksp 'com.airbnb.android:paris-processor:2.0.0'
}

To use Paris in a library module see Library Modules.

Quick Start

Applying an XML-Defined Style

myView.style(R.style.MyStyle)
Click to see the example in Java.
Paris.style(myView).apply(R.style.MyStyle);

Where myView is an arbitrary view instance, MyStyle an XML-defined style, and style an extension function provided by Paris. Many but not all attributes are supported, for more see Supported View Types and Attributes.

Combining 2 or More Styles

myView.style {
    add(R.style.StyleA)
    add(R.style.StyleB)
    …
}
Click to see the example in Java.
Paris.styleBuilder(myView)
        .add(R.style.StyleA)
        .add(R.style.StyleB)
        …
        .apply();

In cases where there's some overlap the attribute value from the last style added prevails. For more see Combining Styles.

Defining Styles Programmatically

textView.style {
    // Using an actual value.
    textColor(Color.GREEN)
    // Or a resource.
    textSizeRes(R.dimen.my_text_size_small)
}
Click to see the example in Java.
Paris.styleBuilder(textView)
        // Using an actual value.
        .textColor(Color.GREEN)
        // Or a resource.
        .textSizeRes(R.dimen.my_text_size_small)
        .apply();

Can be combined with style resources as well:

textView.style {
    // Adds all the attributes defined in the MyGreenTextView style.
    add(R.style.MyGreenTextView)
    textSizeRes(R.dimen.my_text_size_small)
}
Click to see the example in Java.
Paris.styleBuilder(textView)
        // Adds all the attributes defined in the MyGreenTextView style.
        .add(R.style.MyGreenTextView)
        .textSizeRes(R.dimen.my_text_size_small)
        .apply();

For more see Defining Styles Programmatically.

Custom View Attributes

Attributes are declared as followed:

<declare-styleable name="MyView">
    <attr name="title" format="string" />
    <attr name="image" format="reference" />
    <attr name="imageSize" format="dimension" />
</declare-styleable>

The custom view is annotated with @Styleable and @Attr:

// The value here corresponds to the name chosen in declare-styleable.
@Styleable("MyView")
class MyView(…) : ViewGroup(…) {

    init {
        // This call enables the custom attributes when used in XML layouts. It
        // extracts styling information from AttributeSet like it would a StyleRes.
        style(attrs)
    }

    @Attr(R.styleable.MyView_title)
    fun setTitle(title: String) {
        // Automatically called with the title value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_image)
    fun setImage(image: Drawable?) {
        // Automatically called with the image value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_imageSize)
    fun setImageSize(@Px imageSize: Int) {
        // Automatically called with the imageSize value (if any) when an
        // AttributeSet or StyleRes is applied to the MyView instance.
    }
}
Click to see the example in Java.
// The value here corresponds to the name chosen in declare-styleable.
@Styleable("MyView")
public class MyView extends ViewGroup {

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyle) {
        this(context, attrs, defStyle);
        // This call enables the custom attributes when used in XML layouts. It
        // extracts styling information from AttributeSet like it would a StyleRes.
        Paris.style(this).apply(attrs);
    }

    @Attr(R.styleable.MyView_title)
    public void setTitle(String title) {
        // Automatically called with the title value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_image)
    public void setImage(Drawable image) {
        // Automatically called with the image value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_imageSize)
    public void setImageSize(@Px int imageSize) {
        // Automatically called with the imageSize value (if any) when an
        // AttributeSet or StyleRes is applied to the MyView instance.
    }
}

The @Attr-annotated methods will be called by Paris when the view is inflated with an AttributeSet or when a style is applied.

For more see Custom View Attributes.

Styling Subviews

Attributes are declared as followed for the 2 subviews we'd like to be able to style:

<declare-styleable name="MyHeader">
    <attr name="titleStyle" format="reference" />
    <attr name="subtitleStyle" format="reference" />
    ...
</declare-styleable>

The subview fields are annotated with @StyleableChild:

@Styleable("MyHeader")
class MyHeader(…) : ViewGroup(…) {

    @StyleableChild(R.styleable.MyHeader_titleStyle)
    internal val title: TextView …
    
    @StyleableChild(R.styleable.MyHeader_subtitleStyle)
    internal val subtitle: TextViewinit {
        style(attrs)
    }
}
Click to see the example in Java.
@Styleable("MyHeader")
public class MyHeader extends ViewGroup {

    @StyleableChild(R.styleable.MyHeader_titleStyle)
    TextView title;
    
    @StyleableChild(R.styleable.MyHeader_subtitleStyle)
    TextView subtitle;
    
    …
    // Make sure to call Paris.style(this).apply(attrs) during initialization.
}

The title and subtitle styles can now be part of MyHeader styles:

<MyHeader
    ...
    app:titleStyle="@style/Title2"
    app:subtitleStyle="@style/Regular" />
myHeader.style {
    // Defined in XML.
    titleStyle(R.style.Title2)
    // Defined programmatically.
    subtitleStyle {
        textColorRes(R.color.text_color_regular)
        textSizeRes(R.dimen.text_size_regular)
    }
}
Click to see the example in Java.
Paris.styleBuilder(myHeader)
        // Defined in XML.
        .titleStyle(R.style.Title2)
        // Defined programmatically.
        .subtitleStyle((builder) -> builder
                .textColorRes(R.color.text_color_regular)
                .textSizeRes(R.dimen.text_size_regular))
        .apply();

Attention: Extension functions like titleStyle and subtitleStyle are generated during compilation by the Paris annotation processor. When new @StyleableChild annotations are added, the project must be (re)compiled once for the related functions to become available.

For more see Styling Subviews.

Linking Styles to Views

@Styleable
class MyView(…) : View(…) {

    companion object {
        // For styles defined in XML.
        @Style
        val RED_STYLE = R.style.MyView_Red

        // For styles defined programmatically.
        @Style
        val GREEN_STYLE = myViewStyle {
            background(R.color.green)
        }
    }
}
Click to see the example in Java.
@Styleable
public class MyView extends View {

    // For styles defined in XML.
    @Style
    static final int RED_STYLE = R.style.MyView_Red;

    // For styles defined programmatically.
    @Style
    static void greenStyle(MyViewStyleApplier.StyleBuilder builder) {
        builder.background(R.color.green);
    }
}

Helper methods are generated for each linked style:

myView.style { addRed() } // Equivalent to style(R.style.MyView_Red)
myView.style { addGreen() } // Equivalent to add(MyView.GREEN_STYLE)

myView.style {
    addRed() // Equivalent to add(R.style.MyView_Red)
    addGreen() // Equivalent to add(MyView.GREEN_STYLE)
    …
}
Click to see the example in Java.
Paris.style(myView).applyRed(); // Equivalent to apply(R.style.MyView_Red)
Paris.style(myView).applyGreen(); // No equivalent.

Paris.styleBuilder(myView)
        .addRed() // Equivalent to add(R.style.MyView_Red)
        .addGreen() // No equivalent.
        …
        .apply();

Attention: Extension functions like addRed and addGreen are generated during compilation by the Paris annotation processor. When new @Style annotations are added, the project must be (re)compiled once for the related functions to become available.

For more see Linking Styles to Custom Views.

Documentation

See examples and browse complete documentation at the Paris Wiki.

If you still have questions, feel free to create a new issue.

Contributing

We love contributions! Check out our contributing guidelines and be sure to follow our code of conduct.

License

Copyright 2018 Airbnb, Inc.

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

   http://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.

paris's People

Contributors

brettbukowski avatar charlesx2013 avatar elihart avatar felipecsl avatar jenda avatar josetotirodriguez avatar lisherwin avatar mmdango avatar ngsilverman avatar olegkan avatar plnice avatar podarsmarty avatar pt2121 avatar rossbacher avatar rz-robsn avatar shaishavgandhi avatar vinaygaba avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

paris's Issues

Building styles using attributes from other styles

This would make it easier to reuse styles, especially in a context where styles have to declare the same attributes which makes combining them useless.

val dangerTextStyle = viewStyle {
    textColor(Color.RED)
}

val bigDangerTextStyle = viewStyle {
    // This uses the text color from the given style
    textColor(dangerTextStyle)
    textSizeDp(36)
    ...
}

Generate Kotlin extensions for @Styleable views

Rather than going through the Paris class (Paris.style(view)...) extensions could be generated for each view type:

fun View.style(style: Style) {
    ViewStyleApplier(this).apply(style)
}

fun View.style(init: ViewStyleApplier.StyleBuilder.() -> Unit) {
    ViewStyleApplier.StyleBuilder().let {
        it.init()
        it.applyTo(this)
    }
}

// Linked style
fun View.styleGreen() {
    ViewStyleApplier(this).apply(R.style.Green)
}

In action:

Paris.style(view).apply(R.style.Green)
// Could be replaced by:
view.style(R.style.Green)

Paris.styleBuilder(view)
        .add(R.style.MyGreenTextView)
        .textSizeRes(R.dimen.my_text_size_small)
        .apply();
// Could be replaced by:
view.style {
    add(R.style.MyGreenTextView)
    textSizeRes(R.dimen.my_text_size_small)
}

Paris.style(view).applyGreen()
// Could be replaced by:
view.styleGreen()

cc @elihart

Find a way to remove unit test resources from main

Certain Robolectric unit tests require custom resources (styles notably) but the only way to have access to those is to put them among regular resources which means they ship with the library. The number of such resources is currently tiny but it'd be nice to find a way to exclude them (instrumentation tests for example can have their own test-specific resources).

(I could also move those tests to paris-test.)

Enable @Style annotation for Style fields

For example:

Java

class MyView ... {

    @Style
    static final Style MY_STYLE = new MyViewStyleApplier.StyleBuilder()
            .background(...)
            .padding(...)
            .build()

    ...
}

Kotlin

class MyView ... {

    companion object {

        @Style
        val MY_STYLE = myViewStyle {
            background(...)
            padding(...)
        }
    }

    ...
}

Applying Paris to Epoxy-enabled custom view clears default layout styling

Paris 1.0.0
Epoxy 2.12.0
Android library module (though it shouldn't make a difference)

I'm using Paris to take advantage of @Attr annotation. Following example should produce view with red background (which is specified in the default layout). However, it looks like the styling is cleared due to some Paris magic.

Component.kt
------------
@ModelView(defaultLayout = R2.layout.default_component_layout)
@Styleable("Component")
class Component @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
        ConstraintLayout(context, attrs, defStyleAttr) {

    init {
        inflate(R.layout.component_inner_layout)
        Paris.style(this).apply(attrs)
    }

    @Attr(R2.styleable.Component_title)
    @TextProp
    fun setTitle(text: CharSequence) {
        title.text = text // kotlinx.android.syntethic
    }

}
default_component_layout.xml
----------------------------
<my.package.Component xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="56dp"
    android:background="@color/red"/>
component_inner_layout.xml
-----------------------
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:parentTag="android.support.constraint.ConstraintLayout">

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Test" />

</merge>
attrs.xml
---------
<declare-styleable name="Component">
    <attr name="title" format="string" />
</declare-styleable>

Removing Paris from the view makes the default layout styling work correctly:

Component.kt
------------
@ModelView(defaultLayout = R2.layout.default_component_layout)
// @Styleable("Component")
class Component @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
        ConstraintLayout(context, attrs, defStyleAttr) {

    init {
        inflate(R.layout.component_inner_layout)
        // Paris.style(this).apply(attrs)
    }

    // @Attr(R2.styleable.Component_title)
    @TextProp
    fun setTitle(text: CharSequence) {
        title.text = text // kotlinx.android.syntethic
    }

}

@Style name formatting

Having to always specify a name for styles as part of @Style is rather verbose, especially for views with lots of styles. Could formatting options be provided instead to make the explicit name optional?

  • By default if the name isn't specified it could remove the @Styleable's name if it is a prefix. Eg if the styleable name is "MyView" and the style name is "MyView_Red" then it would automatically be set to "Red". This also implies that the "_" character is removed automatically.
  • For more complex cases we could provide "removeStylePrefix" and "removeStyleSuffix" parameters to @Styleable. Eg the styleable name is "MyTextView" and the style names are as follow "TextTitle1", "TextTitle2", and "TextRegular". By setting removeStylePrefix = "Text" the resulting style names would be "Title1", "Title2", and "Regular".

[TextView] inputType=textPassword + singleLine=true misbehaves

Currently i'm using

com.airbnb.android:paris:1.1.0-SNAPSHOT

And i'm getting error value when styling my EditText programmatically, it give me textVisiblePassword instead of textPassword

Here is my code in my fragment

        EditText passEt = new EditText(getContext());
        passEt.setId(new ViewUtil().setViewId());
        Paris.style(passEt).apply(R.style.custom_password_et);
        RelativeLayout.LayoutParams testP = new 
        RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, dimensionToDp(50));
        passEt.setLayoutParams(testP);

Style that i used in styles.xml

    <style name="custom.password_et" parent="@android:style/Widget.EditText">
        <item name="android:layout_marginTop">@dimen/margin_normal</item>
        <item name="android:hint">@string/hint_password</item>
        <item name="android:background">@drawable/et_rounded</item>
        <item name="android:inputType">textPassword</item>
        <item name="android:maxLines">1</item>
        <item name="android:padding">@dimen/margin_small</item>
        <item name="android:paddingStart">@dimen/padding_start</item>
        <item name="android:paddingEnd">@dimen/view_grid_item_close_size</item>
        <item name="android:singleLine">true</item>
        <item name="android:textColor">@color/colorMaterialBlack</item>
        <item name="android:textColorHint">@color/colorMaterialBlackBlur</item>
    </style>

Please fix this bug, thanks

@Style annotations outside of view classes

This could be applicable to Java but especially makes sense with Kotlin where styles can be declared outside of classes:

// Can be in any file, in any module

@Style
val MY_STYLE_1 = R.style.MyStyle

@Style
val MY_STYLE_2 = myViewStyle {
    background(...)
    padding(...)
    ...
}

Can't implement style that has name with "." in the name field

I want to implement Style from my .xml, unfortunatelly it can't implement style that has "." in their name.
Example : I can't implement "AppTheme.CustomStyle", but when i remove the ".", my style name become AppThemeCustomStyle, then it can implement it.

Kotlin substyle builders

Current substyle builder in Kotlin:

Paris.styleBuilder(myView)
        .titleStyle {
            it.textColor(...)
            it.textSize(...)
        }

Ideally we'd be able to write:

Paris.styleBuilder(myView)
        .titleStyle {
            textColor(...)
            textSize(...)
        }

It's currently unclear to me how to achieve this while maintaining good Java interop.

cc @elihart

Enable @StyleableChild for Kotlin fields

Right now Kotlin fields are problematic because the annotation processor generates code that tries to access the backing field (which is private) rather than the getter.

Functional attribute values

Wondering if something like this would be useful:

val style = myViewStyle {
    backgroundRes { myView ->
        if (myView.something()) {
            R.drawable.something_background
        } else {
            R.drawable.another_background
        }
    }
}

Could this become a reactive pattern? For example a single style could differ depending on whether the view is enabled or not, all we'd need is a way to know when to refresh/re-apply the style.

Note: we wouldn't want to whole style to have access to the view instance to avoid differing attribute sets depending on conditions.

Paris style not work for styling AppBarLayout and Toolbar

I try to styling my AppBarLayout and Toolbar programmatically using Paris.
But, it's not work, may be because Paris not support it yet?
Really need help for styling that AppBarLayout and Toolbar programmatically.
And i wonder, in xml i use android:theme for styling AppBarLayout and Toolbar, is it same with styling right?

ColorDrawable style builder functions

For Drawable attributes we could generate a style builder method/function with a parameter of type @ColorInt int which would automatically create a ColorDrawable.

Could Paris provide a way to retheme an arbitrary view hierarchy?

One use case brought up by avipars and bernaferrari in this Reddit thread would be to easily toggle between a light and dark theme. (It would be a cool thing to showcase in the sample app.) This has also been requested internally at Airbnb by multiple teams.

The interface could look something like:

Paris.theme(activity).apply(R.style.Theme_Dark);

This would need to traverse the view hierarchy to change the style of all the views (or views grouped instead a @Styleable custom view). One issue becomes obvious: Paris would need to know which style to apply to each view type. Two solutions come to mind:

  1. Views could store the last style applied to them in a tag (with programmatic styles this could be costly memory-wise as styles might include drawables and the likes -- presumably they would also be used by the view but then again, maybe this would prevent them from being garbage collected).
  2. Linked styles could be leveraged to create groups of styles, ie. Paris' own version of themes. For example:
// Each component view would need to declare styles belonging to these groups
@Styleable
public class MyView ... {
    @Style("Dark") static final int DARK_STYLE = R.style.MyView_Dark;
    @Style("Light") static final int LIGHT_STYLE = R.style.MyView_Light;

    ...
}

(Instead of a string parameter it could also be a separate annotation: @Dark @Style)

Then the following methods could be generated:

Paris.theme(activity).applyDark();
Paris.theme(activity).applyLight();

Here we would traverse the view hierarchy and apply the appropriate style to @Styleable views that support it. It might be a little more cumbersome to setup but one of the benefit is each component view is still declaring explicitly the styles it supports rather than relying on arbitrary theme attributes. One caveat, especially for the dark/light case is all view components would need to support it.

Other considerations:

  • How would this work in a context where views are recycled?
    Perhaps applying a theme could alter the behavior of the applyDefault() methods where the default style becomes the ones set by the theme.
  • Could we make it so that styles could be customized in addition to the theme style being applied (for example in the context of an Epoxy controller)?
    This works out of the box for partial styles applied directly to models as long as we want the customization to override the theme and not the other way around.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.