Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scaling UI

jbarr21
July 27, 2018

Scaling UI

jbarr21

July 27, 2018
Tweet

More Decks by jbarr21

Other Decks in Technology

Transcript

  1. Scaling UI
    James Barr
    Uber

    View full-size slide

  2. UI Issues & Considerations

    View full-size slide

  3. Design System

    View full-size slide

  4. Before & After
    #000000
    #09091A
    #151525
    #222231
    #3A3A48
    #6B6B76
    #9D9DA3
    #FFFFFF
    #B2B2BA
    #C0C0C8
    #CDCDD3
    #D9D9DE
    #E6E6E9
    #F2F2F4
    #F9F9F9
    #1EACC7
    #1FBAD6
    #661FBAD6
    #4CC8DE
    #79D6E6
    #A5E3EF
    #D2F1F7
    ...

    View full-size slide

  5. Design System Guardrails

    View full-size slide

  6. Lint
    class ColorResourceUsageDetector : ResourceXmlDetector() {
    override fun visitAttribute(context: XmlContext, attr: Attr) {
    if (attr.name in COLOR_ATTRIBUTES {
    if (attr.value.startsWith(PREFIX_COLOR_RES) && attr.value !in WHITELISTED_COLORS) {
    when {
    isInResFolder(LAYOUT) && !isColorSelector(attr.value) -> reportError()
    isInVectorDrawable(attribute) -> reportError()
    isInResFolder(context, DRAWABLE) && !hasQualifiedVersion(QUALIFIER_V21)
    && !isColorSelector(context, attributeVal) -> reportError()
    }
    } else if (attr.value.startsWith(PREFIX_THEME_ATTR) && isInResFolder(DRAWABLE)
    && !isInVectorDrawable(attribute) && !isQualifiedVersion(context)) {
    reportError()
    }
    }
    }
    }

    View full-size slide

  7. Custom Fonts

    View full-size slide

  8. Font Updates
    1. Add new font asset
    2. Update font name
    -- fonts/Roboto.ttf
    ++ fonts/GoogleSans.ttf
    3.

    ?
    src/main/res/value/strings-fonts.xml

    View full-size slide

  9. Calligraphy Wrapped

    View full-size slide

  10. ViewPump
    View inflation with a pre/post-
    processing API
    github.com/InflationX/ViewPump

    View full-size slide

  11. ViewPump
    override fun onCreateView(
    name: String,
    context: Context,
    attrs: AttributeSet): View {
    return ViewPump.get().inflate(InflateRequest.builder()
    .name(name)
    .context(context)
    .attrs(attrs)
    .fallbackViewCreator(mViewCreator)
    .build()).view()
    }
    ViewPumpLayoutInflater.java

    View full-size slide

  12. ViewPump
    override fun onCreateView(
    name: String,
    context: Context,
    attrs: AttributeSet): View {
    return ViewPump.get().inflate(InflateRequest.builder()
    .name(name)
    .context(context)
    .attrs(attrs)
    .fallbackViewCreator(mViewCreator)
    .build()).view()
    }
    ViewPumpLayoutInflater.java

    View full-size slide

  13. ViewPump Interceptors
    class TextUpdatingInterceptor() : Interceptor {
    override fun intercept(chain: Chain): InflateResult {
    val result: InflateResult = chain.proceed(chain.request())
    val view: View = result.view()
    if (view is TextView) {
    // You also have access to result.context and result.attrs
    view.text = "[Prefix] ${view.text}"
    }
    return result
    }
    }

    View full-size slide

  14. Calligraphy
    class CalligraphyInterceptor(
    val calligraphy: Calligraphy) : Interceptor {
    override fun intercept(chain: Chain): InflateResult {
    val result: InflateResult = chain.proceed(chain.request())
    return result.toBuilder()
    .view(
    calligraphy.onViewCreated(
    result.view(), result.context(), result.attrs()))
    .build()
    }
    }

    View full-size slide

  15. Configuring ViewPump
    ViewPump.init(ViewPump.builder()
    .addInterceptor(CalligraphyInterceptor(CalligraphyConfig.Builder().build()))
    .addInterceptor(AccessibilityEncouragingInterceptor())
    .addInterceptor(TextUpdatingInterceptor())
    .addInterceptor(AppCompatInterceptor())
    .build())

    View full-size slide

  16. Reimplement AppCompat
    class AppCompatInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): InflateResult {
    val request = chain.request()
    val viewNameToInflate = viewNameToInflate(request.name())
    return chain.proceed(request.toBuilder().name(viewNameToInflate).build())
    }
    fun viewNameToInflate(requestedViewName: String) = when (requestedViewName) {
    "Button" -> AppCompatButton::class.java.name
    "EditText" -> AppCompatEditText::class.java.name
    "ImageView" -> AppCompatImageView::class.java.name
    "TextView" -> AppCompatTextView::class.java.name
    else -> requestedViewName
    }
    }

    View full-size slide

  17. Reimplement AppCompat
    class AppCompatInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): InflateResult {
    val request = chain.request()
    val viewNameToInflate = viewNameToInflate(request.name())
    return chain.proceed(request.toBuilder().name(viewNameToInflate).build())
    }
    fun viewNameToInflate(requestedViewName: String) = when (requestedViewName) {
    "Button" -> AppCompatButton::class.java.name
    "EditText" -> AppCompatEditText::class.java.name
    "ImageView" -> AppCompatImageView::class.java.name
    "TextView" -> AppCompatTextView::class.java.name
    else -> requestedViewName
    }
    }

    View full-size slide

  18. Hide Unaccessible Views
    class AccessibilityEncouragingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): InflateResult {
    val result = chain.proceed(chain.request())
    result.view()?.let {
    if (it.isClickable && it.contentDescription.isNullOrEmpty()) {
    it.visibility = View.INVISIBLE
    }
    }
    return result
    }
    }

    View full-size slide

  19. Other Example Usages
    • Geocities-ify an app
    • Draw over views under some
    conditions
    • Backport functionality for new attrs
    • Dynamic string reloading
    • Track recently inflated views & add
    data to crash reports

    View full-size slide

  20. Platform UI Widgets

    View full-size slide

  21. Artist
    A build tool that codegens a base
    set of Android Views
    github.com/uber/artist

    View full-size slide

  22. Artist
    Traits (functionality)
    +
    Stencils (definitions)

    Views

    View full-size slide

  23. Artist Trait
    @AutoService(Trait::class)
    class VisibilityTrait : Trait {
    override fun generateFor(
    type: TypeSpec.Builder,
    initMethod: MethodSpec.Builder,
    rClass: ClassName,
    baseType: String) {
    arrayOf("visible", "invisible", "gone").forEach {
    type.addMethod(createVisibilityConvenienceMethod(it))
    }
    }
    ...
    }

    View full-size slide

  24. Artist Trait
    @AutoService(Trait::class)
    class VisibilityTrait : Trait {
    ...
    fun createVisibilityConvenienceMethod(type: String): MethodSpec {
    return MethodSpec.methodBuilder("is${type.capitalize()}")
    .addModifiers(Modifier.PUBLIC)
    .returns(TypeName.BOOLEAN)
    .addStatement(
    "return getVisibility() == \$T.${type.toUpperCase()}",
    TypeNames.Android.View)
    .build()
    }
    }

    View full-size slide

  25. Artist Stencil
    class TextViewStencil : ViewStencil(
    extendedType = "android.support.v7.widget.AppCompatTextView",
    constructorCount = 3,
    defaultAttrRes = "textViewStyle",
    addedTraits = VisibilityTrait::class.java
    )

    View full-size slide

  26. Artist Generated View
    public class MyTextView extends AppCompatTextView {
    public MyTextView(Context context) { this(context, null); }
    public MyTextView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.textViewStyle); }
    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs, defStyleAttr, 0);
    }
    // protected init method - provided in every stencil
    public boolean isVisible() { return getVisibility() == View.VISIBLE; }
    public boolean isGone() { return getVisibility() == View.GONE; }
    public boolean isInvisible() { return getVisibility() == View.INVISIBLE; }
    }

    View full-size slide

  27. @AutoService(ViewStencilProvider.class)
    class SampleViewStencilProvider : ViewStencilProvider {
    override fun stencils() = setOf(
    ViewStencil("android.support.v7.widget.AppCompatButton", 3, "buttonStyle"),
    ViewStencil("android.support.v7.widget.AppCompatEditText", 3,
    "android.R.attr.editTextStyle", TextInputTrait.class),
    ViewStencil("android.widget.LinearLayout", 3),
    ViewStencil("android.support.v7.widget.AppCompatImageView", 3),
    ViewStencil("android.support.v4.widget.NestedScrollView", 3, ScrollableTrait.class),
    ViewStencil("android.support.v7.widget.AppCompatTextView", 3, "android.R.attr.textViewStyle")
    )
    override fun globalTraits() = setOf(
    VisibilityTrait.class::java,
    ForegroundTrait.class::java,
    ViewTrait.class::java
    )
    }

    View full-size slide

  28. Artist Use Cases
    • Make Rx APIs 1st class citizens: myView.clicks()
    • Add built-in analytics to taps and impressions
    • Make a bugfix to MyTextView and all other usages will receive
    it
    • Easily change all of your parent classes from
    AppCompatWidget to MaterialWidget

    View full-size slide

  29. Wrapping Up
    • Collaborate with Design to create a system that works
    • Use semantic names for colors & styles flexibility across apps
    • Enforce your design system with Lint & ErrorProne
    • Use ViewPump to post-process inflated views
    • Use Artist to easily maintain your base set of views

    View full-size slide

  30. Scaling UI
    James Barr
    • uber.github.io
    • We're Hiring
    @jbarr21

    View full-size slide