Author: Wei, smart client team

background

UI componenzation has positive benefits to the project, which can not only improve the efficiency, but also ensure a high degree of visual restoration and reduce the cost of communication with UI designers, so it has been recognized by everyone. So every project start UI modular construction, but the UI view is strong and project related, the project can’t reuse, cause everybody to realize, repetition of rolling, delay time from work, then based on the above background, is there a better solution, the answer is yes, the following introduce UI component in the project implementation experience, The following sections are divided into goals, engineering architecture, component architecture, and component implementation.

The target

Container was carried out on the existing UI componentization abstraction, the underlying UI components to provide maximum functionality set, complete decoupling of business logic, business according to their own needs, based on the basis of component development, through property configuration or combination to achieve the effect of complex, so as long as the underlying components of abstract good enough, the capacity, can greatly improve the efficiency of development, Later adaptation will not involve core logic modification, to a certain extent to ensure the stability of the function

Engineering structure

The module partition

All UI components converge to UIKit, and the following moudle division is based on whether they are very general. If there are non-business attributes and particularly general module components, separate modules are extracted to facilitate decoupling and reuse. If not, they are unified under the same module, so UIKit modules are divided as follows:

  • app

The shell works independently

  • demo

Demo is available for all components, and functions in the demo can also be opened in the debug panel

  • uikit

Depending on widgets and module****, services can directly rely on UIKit when using UI components

  • widget

    • DivideLine
  • XRadioGroup

  • LoadingView

  • ShimmerLayout

  • .

  • uikit-module

    • flatButton
  • roundView

  • load

  • dialog

  • imageSelect

  • toast

  • .

Engineering layered

The engineering architecture can be divided into five layers, which are: basic control, composite control, business UI component, bridge, demo.

  • Basic controls: Provide atomic capabilities, single point controls such as cascading FlowLayout, skeleton control ShimmerLayout, and button Flatbutton

  • Composite controls: They rely on basic controls, such as Dialog and ImageSelector. The UI of these controls is more complex, so basic components such as FlatButton and ShimmerLayout are used to improve development efficiency

  • Business UI Component: This is the real UI component that we want to implement, custom-developed based on design requirements, configuring business preferences on base controls and composite controls, and combining them into business components with less development effort

  • Bridging: The business layer is not aware of the number and dependencies of UI components. The business layer only relies on UIKit, while UI component dependency management converges to UIKit. The benefit of this is that subsequent iterations only maintain dependencies in UIKit

  • Demo: A good component requires intuitive sample code in addition to documentation, which can be integrated directly into the debug panel

The architecture layers are as follows:

A good architecture should be hierarchical, low coupling, high extension, and friendly enough to support the addition and deletion of components, so that any component can accurately find the corresponding layer, and will not change to the existing code, so review the architecture just designed, basically meet the requirements, the architecture design is in line with expectations. After the architecture is designed, the steps are implemented. How to combine it with the existing project? UI components can be divided into phases: Development phase and stable phase, the ideal model of development for the development in the host project development and debugging, but put the host in engineering will be brought the problem of low compiled component development and business is decoupling, so want to code in reservoir engineering, demo and component development can be run separately, when the component development is complete, the stable phase, reduce the frequency of component code changes, At the same time, the compilation speed is accelerated. UIKit components are released to the remote Maven repository. Finally, THE UIKit project is independent and iterated separately

UI components and hosts are packaged and compiled

settings.gradle

includeIfAbsent ':uikit:uikit'
includeIfAbsent ':uikit:demo'
includeIfAbsent ':uikit:imgselector'
includeIfAbsent ':uikit:roundview'
includeIfAbsent ':uikit:widget'
includeIfAbsent ':uikit:photodraweeview'
includeIfAbsent ':uikit:flatbutton'
includeIfAbsent ':uikit:dialog'
includeIfAbsent ':uikit:widgetlayout'
includeIfAbsent ':uikit:statusbar'
includeIfAbsent ':uikit:toolbar'
Copy the code

Common_business. gradle key dependency

apply from: rootProject.file("library_base.gradle")

dependencies {
    .​..
    implementation project(":uikit:uikit")
}
Copy the code

UI components compile independently

uikit/shell/settings.gradle

include ':app' includeModule('widget','.. /') includeModule('demo','.. /') includeModule('flatbutton','.. /') includeModule('imgselector','.. /') includeModule('photodraweeview','.. /') includeModule('roundview','.. /') includeModule('uikit','.. /') includeModule('widgetlayout','.. /') includeModule('dialog','.. /') includeModule('statusbar','.. /') includeModule('toolbar','.. /') def includeModule(name, filePath = name) { def projectDir = new File(filePath+name) if (projectDir.exists()) { include ':uikit:' + name project(':uikit:' + name).projectDir = projectDir } else { print("settings:could not find module $name in path $filePath") } }Copy the code

UI component lib build.gradle

if (rootProject.ext.is_in_uikit_project) { apply from: rootProject.file('.. /uikit.gradle') } else { apply from: rootProject.file('uikit/uikit.gradle') }Copy the code

This has the effect of running the host project UIKit code separately

Component architecture

Components can be divided into two categories: type tool, business types, two types of components iterative thinking difference is very big, instrumental components, as long as the single point perfectly with respect to ok, overall is simpler, reusability is strong, and YeWuXing components will be a bit complicated, should not only consider reusability, extensibility, which should be considered respectively introduce the two types below component implementation

Tool type

The idea of tool-based component iteration is to continuously improve the basic capabilities, make the functions as comprehensive as possible, and continuously support new functions on the existing capabilities. The most important thing is to be compatible with existing apis. Representative components include FlatButton, RoundView, and StatusBar. Please refer to the iteration process of FlatButton&RoundView:

YeWuXing

The implementation of a business component can be either concrete or abstract. A good component design should have both. The bottom implementation should be abstract enough while the top implementation should be concrete.

Component implementation

The following uses the FlatButton as an example to describe the implementation of the component. The implementation of other components is similar. Before we implement it, let’s look at the visuals

There are many styles of buttons, and there are many ways to implement them. The existing project also provides the implementation scheme, as follows:

Step 1: Define noraml’s shape and Pressed’s shape, and if enable = false, a dissable shape

normal (ui_standard_bg_btn_corner_28_ripple)

<? The XML version = "1.0" encoding = "utf-8"? > <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/button_pressed_cover"> <item android:drawable="@drawable/ui_standard_bg_btn_corner_28_enable"> </item> </ripple>Copy the code

pressed(ui_standard_bg_btn_corner_28_disable)

<? The XML version = "1.0" encoding = "utf-8"? > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:angle="0" android:endColor="@color/button_disable_end" android:startColor="@color/button_disable_start" android:useLevel="false" android:type="linear" /> <corners android:radius="28dp" /> </shape>Copy the code

Step 2: Define the selector

selector(ui_standard_bg_btn_corner_28)

<? The XML version = "1.0" encoding = "utf-8"? > <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="true" android:drawable="@drawable/ui_standard_bg_btn_corner_28_ripple" /> <item android:state_enabled="false" android:drawable="@drawable/ui_standard_bg_btn_corner_28_disable" /> </selector>Copy the code

Step 3: Use

<TextView
    ...
    android:background="@drawable/ui_standard_bg_btn_corner_28"
    android:textColor="@color/white"/>
Copy the code

If the text also needs to be pressed, then repeat the steps above and create another selector for the color. When the style defined in the UI above is implemented, the painting style in the project will look like this:

Who I am, where I am, how to play this, look the same, basically no development experience, reusability, scalability are very poor, if a major UI revision, and have to start again. How to solve the above problem, the answer is to define the button general ability, the business layer to achieve, according to this idea, you need to delete all the above shape, selector, and then customize the control, as we all know, the shape, selector XML file defined above, The Android system will eventually parse and generate the corresponding object, so we learn from the system code, the implementation is so easy

Look at this shape XML

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:angle="0"  android:endColor="@color/button_disable_end" android:startColor="@color/button_disable_start" android:useLevel="false" android:type="linear" /> <corners android:radius="28dp" /> </shape>Copy the code

The resolved object is a GradientDrawable

public void setOrientation(Orientation orientation)
public void setColors(@Nullable @ColorInt int[] colors)
public void setCornerRadii(@Nullable float[] radii)
public void setStroke(int width, @ColorInt int color)
...
Copy the code

In other words, all attributes defined in XML can be implemented in code. In addition to GradientDrawable, RippleDrawable is used to implement water ripples. Similarly, ColorStateList is used in the text color selector code.

Step 1: Define custom properties
<declare-styleable name="FlatButton"> <! <attr name="fb_colorNormal" format="color" /> <! <attr name="fb_colorPressed" format="color" /> <! <attr name="fb_colorDisable" format="color" /> <! <attr name="fb_colorNormalStart" format="color" /> <! <attr name="fb_colorNormalEnd" format="color" /> <! <attr name="fb_colorPressedStart" format="color" /> <! <attr name="fb_colorPressedEnd" format="color" /> <! <attr name="fb_colorDisableStart" format="color" /> <! --Disable End gradient color --> <attr name="fb_colorDisableEnd" format="color" /> <! <enum name="left_right" value="0" /> <enum name="right_left" value="1" /> <enum name="top_bottom" value="2" /> <enum name="bottom_top" value="3" /> <enum name="tr_bl" value="4" /> <enum name="bl_tr" value="5" /> <enum name="br_tl" value="6" /> <enum name="tl_br" value="7" /> </attr> <! <attr name="fb_colorNormalText" format="color" /> <! <attr name="fb_colorPressedText" format="color" /> <! --Disable text color --> <attr name="fb_colorDisableText" format="color" /> <! <attr name="fb_strokeColor" format="color" /> <! <attr name="fb_strokePressColor" format="color" /> <! --Disable border color --> <attr name="fb_strokeDisableColor" format="color" /> <! <attr name="fb_strokeWidth" format="dimension" /> <! <attr name="fb_isRippleEnable" format=" Boolean "/> <! <attr name="fb_colorRippleNormal" format="color" /> <! <attr name="fb_colorRipplePressed" format="color" /> <! <attr name="fb_cornerRadius" format=" Dimension "/> <! <attr name="fb_radius_TL" format="dimension" /> <! <attr name="fb_radius_TR" format="dimension" /> <! <attr name="fb_radius_BL" format="dimension" /> <! <attr name=" radius_br "format="dimension" /> <! <attr name="fb_antiShakeEnable" format=" Boolean "/> </declare-styleable>Copy the code
Step 2: Core implementation logic
private fun setBackgroundCompat() {
    val stateListDrawable = createStateListDrawable()
    val pL = paddingLeft
    val pT = paddingTop
    val pR = paddingRight
    val pB = paddingBottom
    background = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isRippleEnable) {
        val rippleDrawable = RippleDrawable(createRippleColorStateList(), stateListDrawable, null)
        rippleDrawable
    } else {
        stateListDrawable
    }
    setPadding(pL, pT, pR, pB)
}


private fun createStateListDrawable(): StateListDrawable {
    var normalDrawable = StateListDrawable()
    normalDrawable.addState(
            intArrayOf(android.R.attr.state_pressed),
            createPressedDrawable()
    )
    normalDrawable.addState(
            intArrayOf(android.R.attr.state_focused),
            createPressedDrawable()
    )
    normalDrawable.addState(
            intArrayOf(-android.R.attr.state_enabled),
            createDisableDrawable()
    )
    normalDrawable.addState(
            intArrayOf(android.R.attr.state_selected),
            createPressedDrawable()
    )
    normalDrawable.addState(intArrayOf(), createNormalDrawable())
    return normalDrawable
}


private fun createRippleColorStateList(): ColorStateList {
    val stateList = arrayOf(intArrayOf(android.R.attr.state_pressed), intArrayOf(android.R.attr.state_focused), intArrayOf(android.R.attr.state_activated), intArrayOf())
    val normalColor = backgroundStyle.getColorRippleNormalFallback()
    val pressedColor = backgroundStyle.getColorRipplePressedFallback()
    val stateColorList = intArrayOf(
            pressedColor,
            pressedColor,
            pressedColor,
            normalColor
    )
    return ColorStateList(stateList, stateColorList)
}
Copy the code
Step 3: UI component implementation

XML is used in

<com.snapsolve.uikit.flatbutton.FlatButton
    app:fb_colorNormalText="@color/uikit_color_white"
    app:fb_colorPressedText="@color/uikit_color_white"
    app:fb_colorNormalEnd="#FF9800"
    app:fb_colorNormalStart="#FF0000"
    app:fb_colorPressedEnd="#4CAF50"
    app:fb_colorPressedStart="#009688"
    app:fb_colorRippleNormal="#303F9F"
    app:fb_colorRipplePressed="#FF4081"
    app:fb_cornerRadius="24dp"
    app:fb_gradientOrientation="left_right"
    app:fb_isRippleEnable="true" 
    ...
    />
Copy the code

Use in code

fb_radius_in_code.setBackgroundStyle {
    this.colorNormal = resources.getColor(R.color.uikit_color_FF4081)
    this.colorPressed = resources.getColor(R.color.uikit_color_9C27B0)
    this.colorRippleNormal = resources.getColor(R.color.uikit_color_FF4081)
    this.colorRipplePressed = resources.getColor(R.color.uikit_color_9C27B0)
}.setRadiusStyle {
    this.radiusTL = dp2px(24F)
    this.radius_BR = dp2px(24F)
}
Copy the code

Here, the underlying Button capability is defined, and the next step is the componentization implementation. The specific implementation is as follows:

The button UI in the project can be implemented based on the FlatButton as required by the UI component. The properties of the button can be configured for each type, and the button name can be aligned with the design

Step 4: Business usage

1, 2, and 3 buttons can be implemented by inheriting the FlatButton and setting the default style. When using the button, there is no need to define any attributes in XML, just remember the component name and dependency, so that it can be truly out of the box

For example, define a wireframe button

class StrokeButton : FlatButton { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet? , defStyleAttr: Int) : super(context, attrs, defStyleAttr) { config(context, attrs) } private fun config(context: Context, attrs: AttributeSet?) { .setBackgroundStyle { this.colorNormal = resources.getColor(R.color.uikit_color_FF4081) this.colorPressed = resources.getColor(R.color.uikit_color_9C27B0) this.colorRippleNormal = resources.getColor(R.color.uikit_color_FF4081) this.colorRipplePressed = resources.getColor(R.color.uikit_color_9C27B0) }.setRadiusStyle { this.radiusTL = dp2px(28F) this.radius_BR = dp2px(28F) } } private fun dp2px(dp: Float): Float { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) } }Copy the code

Business use

<com.snapsolve.uikit.demo.flatbutton.StrokeButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
Copy the code