Category: android

Building Responsive / Resizable Android UIs for ChromeOS 📐📏

Building Responsive / Resizable Android UIs for ChromeOS 📐📏

Learn how using ViewModels can help create great user experiences on ChromeOS

This post originally appeared here.

Supporting ChromeOS devices sounds like a large undertaking with many unknowns. If you didn’t know already, ChromeOS allows users to install Android Apps on their devices. This is great news for ChromeOS users since it unlocks a huge amount of apps that previously weren’t available to them.

Why should you build support for ChromeOS?

ChromeOS support seems like a big task that you may not think is important to implement. But if you do a bit of research into the Chromebook usage, you will see that there is a large portion of the US market that uses Chromebooks. ChromeOS made up 59.6% of mobile computing sales in the US in Q4 of 2017 (according to this article) and whilst in the rest of the world ChromeOS isn’t as popular, it is steadily gaining in popularity. Considering that you can pick up a Chromebook for about $150, you can understand why it might be appealing to purchase.

How to build support in your Android app for ChromeOS?

Spoiler Alert: You don’t need to do anything fancy to enable your Android apps to run on ChromeOS. If your app supports tablets, your app will run on ChromeOS. There are extra options you can consider for a ChromeOS device, such as support for a Stylus and possibly support for if a user doesn’t have a touch screen (If you want them to be able to use your app without one).

Supporting the resizing of layouts can be a bit tricky. If you know what tools to use from the start, building support for this kind of interaction in your apps is something you can do from the beginning of building any new layout or app.

Since releasing the Over App on Android, we’ve been working to optimise our app for ChromeOS devices. But it turns out that we didn’t need to do thatmuch more in order to support it because we were already following best practices (as mostly described in this post). There is obviously room for improvement for us (keyboard shortcut support and UI optimisations). In this article, we will cover how to ensure that your UI remains consistent during app window resizes.

Use ViewModels for storing UI State

If you’ve been out of action in Android for a while, you may have missed all the great libraries that have been released recently that help to solve some complex Android specific issues. ViewModels are a new class available from Android Jetpack.

This is the definition of a ViewModel from the Android Developer docs:

ViewModels help store and manage UI-related data in a lifecycle aware way. The ViewModelclass allows data to survive configuration changes such as screen rotations.

With this definition in mind, this is how ChromeOS handles the lifecycle when resizing an app: It notifies your app of a screen size change, which typically would trigger a new creation of your Activity class. Now if you aren’t using something like a ViewModel, when the new activity is created, you would losethe data that you have backing that view. Since ViewModels have a different lifecycle than an Activity, they outlive a recreation of it.

ViewModel Lifecycle from Android Developer Documentation

In our case, we have our layout state stored in the ViewModel and as such, when a user resizes the app window, the state outlives the Activity and the view automatically keeps the same information after it is being resized.

Our ViewModel looks something like this:

class ProjectEditorViewModel : ViewModel() {

    private val _state = MutableLiveData<EditorState>()
    val state: LiveData<EditorState>
        get() = _state

}

The usage of this ViewModel in our ProjectEditorFragment, looks like the code below:

class ProjectEditorFragment : Fragment {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var viewModel: ProjectEditorViewModel

 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_editor_initial, container, false)
        AndroidSupportInjection.inject(this)

        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
            .get(ProjectEditorViewModel::class.java)
        setupViewModel()
    }
    
    private fun setupViewModel() {
        viewModel.state.observe(this, Observer { editorState ->
            editorState?.let { state ->
                // Set the state of all controls based on the saved state in the ViewModel 
            }
        })
    }
}

In the sample above, you can see that we have the ProjectEditorViewModelstate being observed for changes. When a new state comes in from the ViewModel, we will then perform all the required changes in order to update the view’s state.

Resizing the Editor Experience keeps a user’s selected tool state and their project state due to using ViewModels.

If we stored our state in a class that wasn’t extending ViewModel or AndroidViewModel, when the activity is resized, the UI information would be lost (i.e. A user’s project changes, the state of the currently selected tool etc.).

With us using ViewModels by default for all our UI state, resizing an activity didn’t present any weird state loss issues. 🎉

Support multiple screen sizes using best practices

Now that we’ve covered how you can go about storing state across window resizing, we might want to transition our layouts in a way that communicates the change in UI. Of course, this is not the only thing you need to do in order to support ChromeOS devices.

There are great guidelines that already exist on how to build for different screen/density size buckets. Follow those in order to support different layout sizes (i.e. Create different size buckets, layout params etc.).

Finally

If you start doing these few things you won’t have to retrospectively go back and change your app to support ChromeOS. The great part about using the suggestions above is that it doesn’t just apply to ChromeOS but also to tablets or phones when you use the split screen feature on any Android device.

There are a few more things you can do to support ChromeOS really well, including supporting a stylus and making sure your app has some delightful keyboard shortcuts. We are getting there!

If you have any questions, feel free to reach out to me on Twitter @riggaroo.

Thanks to Joshua Leibstein and Nick Rout for reviewing this post.

Android MotionLayout Tutorial – Collapsing View

Android MotionLayout Tutorial – Collapsing View

MotionLayout is a layout class that extends from ConstraintLayout. MotionLayout has all the features of ConstraintLayout. On top of that, it also provides the ability to easily animate changes to your UI, without needing to know much about UI interactions and the Android Animation Frameworks.

Take for example this subtle animation of a view being scrolled and the profile picture shrinking. Before MotionLayout, this would be a tedious task to complete. We may have needed a CollapsingToolbar and some other custom animation code to ensure the profile picture scales correctly. Now with MotionLayout, this is really easy to achieve with one extra XML file.

In this article, we will be looking at implementing a simple swipe action on a RecyclerView and how we can achieve the scaling animation with MotionLayout.

Add MotionLayout as a Gradle Dependency

To get started with MotionLayout, you need to make sure we have the latest version in your build.gradlefile. (Note: MotionLayout is still in alpha at the time of writing)

 implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'

Create your layout XML as per usual

The great part about MotionLayout is that it uses the same constructs as ConstraintLayout. Everything you’ve previously learnt about ConstraintLayout (ie barriers, chains etc) is applicable to layouts that we build with MotionLayout.

To get started, open up your editor and change the root element of your layout to use MotionLayout. Add a RecyclerView and an ImageView to your layout. Make sure the RecyclerView is constrained to the bottom of the ImageView.

RecyclerView with ImageView above

The XML behind the following layout should look similar to this:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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:orientation="vertical"                                android:layout_width="match_parent"                                     app:showPaths="false"                          app:layoutDescription="@xml/motion_layout_example"                                android:layout_height="match_parent">
    <View android:layout_width="0dp" android:layout_height="0dp"
          app:layout_constraintStart_toStartOf="parent"
          android:background="@color/colorPrimary"
          android:id="@+id/background"
          app:layout_constraintBottom_toBottomOf="@id/space"
          app:layout_constraintTop_toTopOf="parent"
          app:layout_constraintEnd_toEndOf="parent" android:alpha="0"
    />

    <ImageView
            android:layout_width="140dp"
            android:layout_height="0dp"
            android:scaleType="centerCrop"
            android:id="@+id/imageViewAvatar"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="16dp"
            app:layout_constraintDimensionRatio="h,1:1"
            app:srcCompat="@drawable/veruca"/>


    <TextView
            android:text="@string/veruca_salt_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/textViewName"
            android:fontFamily="@font/willywonka"
            app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
            android:layout_marginStart="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="16dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Display1"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="@+id/imageViewAvatar" 
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toTopOf="@+id/space"/>

    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            tools:listitem="@layout/list_item_status"
            android:id="@+id/recyclerViewStatus" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/imageViewAvatar">

    </androidx.recyclerview.widget.RecyclerView>
    <Space
            android:layout_width="0dp"
            android:layout_height="8dp"
            android:id="@+id/space"
            app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
            app:layout_constraintTop_toBottomOf="@id/imageViewAvatar"
            app:layout_constraintEnd_toEndOf="@+id/imageViewAvatar"
            app:layout_constraintStart_toStartOf="@+id/imageViewAvatar"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

Create the MotionScene XML

In order to animate this layout we need to describe how views should animate in the layout. To do this, create an XML file in the xml folder of your application. We will call it motion_scene.xml.

The first thing that we will do, is define the Transition for this MotionScene. We set the reference to the start and end ConstraintSets on the Transition object. We can also set the duration of the transition.

<?xml version="1.0" encoding="utf-8"?>
<MotionScene
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
            app:constraintSetStart="@id/start"
            app:constraintSetEnd="@id/end"
            app:duration="1000">
        <OnSwipe
                app:touchAnchorId="@+id/recyclerViewStatus"
                app:touchAnchorSide="top"
                app:dragDirection="dragUp" />
    </Transition>
    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    </ConstraintSet>
</MotionScene>

The next step, that can be seen in the code snippet above, is to create the OnSwipe declaration. This indicates to the MotionLayout that it should monitor the layout for a swipe movement. When the user performs a dragUp gesture on the specified touchAnchorId, the MotionScene will start interpolating between the two states defined (start and end). In this case, it is the recyclerViewStatus view that it will be monitoring for the dragUp gesture.

In the code snippet above, you may notice that there are two ConstraintSet tags defined in the MotionScene. These tags are for the start and end constraints of the view. The beautiful part about MotionLayout is that it will automatically interpolate between these two states and produce some pretty magical animations. At the moment, we don’t have any constraint changes applied, so let’s change that.

The first view we want to animate is the ImageView. We want the size of the view to decrease as a user scrolls up on the RecyclerView. To do that, we will add an end Constraint to the ImageView to adjust the width and height to 40dp .

<?xml version="1.0" encoding="utf-8"?>
<MotionScene
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
            app:constraintSetStart="@id/start"
            app:constraintSetEnd="@id/end"
            app:duration="1000">
        <OnSwipe
                app:touchAnchorId="@+id/recyclerViewStatus"
                app:touchAnchorSide="top"
                app:dragDirection="dragUp" />
    </Transition>

    <ConstraintSet android:id="@+id/start">

    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        
        <Constraint android:id="@id/imageViewAvatar"
                    android:layout_width="40dp"
                    android:layout_height="40dp" android:layout_marginTop="16dp"
                    app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
                    android:layout_marginStart="16dp">
        </Constraint>

    </ConstraintSet>
</MotionScene>

Linking the MotionScene to the Layout

In order to link the MotionScene to the Layout, you will need to set the property app:layoutDescription="@xml/motion_layout_example" to point to your newly created XML file on the root MotionLayout element.

Running this on device, you will observe that as you scroll up on the RecyclerView , the ImageView decreases in size.

Animating Visibility of a View

Now if you wanted to animate the background to go from invisible to visible, we can just add a Property animation to the start and end constraints. The final XML will look as follows:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
            app:constraintSetStart="@id/start"
            app:constraintSetEnd="@id/end"
            app:duration="1000">
        <OnSwipe
                app:touchAnchorId="@+id/recyclerViewStatus"
                app:touchAnchorSide="top"
                app:dragDirection="dragUp" />
    </Transition>

    <ConstraintSet android:id="@+id/start">

        <Constraint android:id="@id/background">
            <PropertySet app:alpha="0"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">

        <Constraint android:id="@id/imageViewAvatar"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_marginTop="16dp"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    android:layout_marginStart="16dp">
        </Constraint>

        <Constraint android:id="@id/background">
            <PropertySet app:alpha="1"/>
        </Constraint>
    </ConstraintSet>
</MotionScene>

It’s a wrap!

As you can see from the short example above, using MotionLayout can decrease the amount of code you need to write in order to achieve delightful animations. In the next few posts, we will be covering more of the features of MotionLayout, as this article has just touched the surface. Till next time!

Follow me on Twitter – @riggaroo.

Android ConstraintLayout 2.0: ConstraintLayoutStates

Android ConstraintLayout 2.0: ConstraintLayoutStates

With the introduction of ConstraintLayout 2.0, there is an interesting new feature called ConstraintLayoutStates. ConstraintLayoutStates allow you to create a layout with different states and switch between them easily. Typically, most layouts contain a loading state, initial state, end state and error state. Using ConstraintLayoutStates, there is a clean way to switch between these different states.

How do you use this new feature? Glad you asked!

Step 1: Create your different states in separate XML files

Create the different state layout files that your layout needs. Each layout must include the same views, they can just have different properties such as their visibility, or their constraints.

ConstraintLayoutStates example layouts
3 layouts for the different layout states.

Step 2: Create a ConstraintLayoutStates XML file

In the xml resource folder, create an xml file containing your states. In this example, we will call it constraint_layout_states_example.xml. Inside this file place all the different representations of your layout. Give them meaningful ids such as startloading etc. and then link them to the relevant constraint files.

<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
                        xmlns:app="http://schemas.android.com/apk/res-auto">
    <State
            android:id="@+id/start"
            app:constraints="@layout/activity_cl_states_start"/>
    <State
            android:id="@+id/loading"
            app:constraints="@layout/activity_cl_states_loading"/>
    <State
            android:id="@+id/end"
            app:constraints="@layout/activity_cl_states_end"/>
</ConstraintLayoutStates>

 

Step 3: Switch between the states

In your activity/fragment, you can now easily switch between these states based on different conditions. You will first need to load the state description using loadLayoutDescription() on your ConstraintLayout object. Once you have done that, you can call constraintLayout.setState() with any of the states that you have defined in the previous states file.

In the example below, we are setting the state to the loading state. Then after some time (in this example I am just posting a delayed runnable, but this could easily be a network call that returns at a later stage), we are setting it to the end state.

class ConstraintLayoutStatesExampleActivity : AppCompatActivity() {

    val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cl_states_start)
        stateConstraintLayout.loadLayoutDescription(R.xml.constraint_layout_states_example)
        var changed = false
        buttonBakeCake.setOnClickListener {
            stateConstraintLayout.setState(R.id.loading, 0, 0)
            postDelayed(handler,  {
                stateConstraintLayout.setState(if (changed) R.id.start else R.id.end,0, 0)
                changed = !changed
            }, null, 3000L)
        }
    }
}

The above example running on a device:ConstraintLayoutStates example

That’s it! You should now be able to use ConstraintLayoutStates in your own apps. This example can be found on Github here.

Enjoyed this post? Let me know your thoughts on Twitter!

ConstraintLayout 2.0: ImageFilterView

ConstraintLayout 2.0: ImageFilterView

Whilst browsing through the various examples online with the new ConstraintLayout 2.0, I stumbled upon ImageFilterView. This got my attention immediately and I decided to investigate further.

An ImageFilterView allows you to perform some common image filtering techniques on an ImageView, including saturation, contrast, warmth and crossfade. 

If you have tried to implement these image filters before, you may have run into ColorMatrix. If you look at the source of ImageFilterView you will see that these methods have just been nicely wrapped up with simpler API usage.

For example, if you would like to adjust the warmth, contrast or saturation, all you need to do is set a property on the ImageFilterView:

<androidx.constraintlayout.utils.widget.ImageFilterView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:id="@+id/imageView"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        app:warmth="1.2"
        app:contrast="1.0"
        app:saturation="2.0"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintDimensionRatio="16:9"
        tools:srcCompat="@tools:sample/avatars"
        />

You can also access this programmatically, so you could add a SeekBar to control these values.

seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekbar: SeekBar?, progress: Int, p2: Boolean) {
        val percentage = (progress / 100.0f)
        imageView.saturation = (percentage) + 1
    }

    override fun onStartTrackingTouch(seekbar: SeekBar?) {

    }

    override fun onStopTrackingTouch(seekbar: SeekBar?) {

    }
})

There is also the ability to crossfade between two different images using the crossfade method defined on ImageFilterView. This allows you to merge two images together.

If you are looking for a quick way to add some basic image effects, ImageFilterView is definitely something to consider. It is fast to use and execute since it is backed by ColorMatrix which uses the GPU (and not the CPU) to process the resultant image.

Here is an example of ImageFilterView in action:

Realtime Image Processing with ImageFilterView
Realtime Image Processing with ImageFilterView

 

The downside to using this approach is that you are not in full control of the exact pixel values that are going to be used, which could be problematic if you are developing an image editing application.

Overall, I’m really excited about the ImageFilterView class! I hope it is the start of some awesome Image effects offered by the Android Team.

Check out the ConstraintLayout demo repository for the code used in the above example.

Follow me on Twitter for more.

 

Variable Fonts in Android O 🖍

Variable Fonts in Android O 🖍

This post initially appeared here.

After attending DroidCon Italy 2018 last week, I was excited by the presentation from Nick Butcher and Florina Muntenescu. They spoke about many different aspects related to Text on Android including Spans, Colours, and AutoSizing TextViews, but the one thing that caught my eye was Variable Fonts.

This was the first time I had heard about variable fonts and I wanted to know more. Since working at Over, we have had to package many different font variations with our (work-in-progress) Android App. Users of the app are able to select the variation of a font family (i.e. the bold version) to overlay text onto their photos. This has increased the file size of the app, so I thought variable fonts could be a good alternative.

Read More Read More