I'm an App and Game Developer - Check out my Work

Getting user input on Wear OS – Part 1 – Voice

Google Play's input screen

Google Play’s input screen

When I first began programming for Wear OS I was expecting an easy out of the box method for getting user input similar to how Google Play works. Giving the user the option to speak or type and also a list of canned inputs to choose from.

Unfortunately, this isn’t as easy as I thought and the developer page seemed pretty vague and unhelpful, but with a little work I was able to come up with a pretty small script to accomplish what I needed to do. I will explain the basics of this in the following post.

To begin, create a layout that looks something like this:

Component tree

 

 

 

 

 

 

I use a Dismiss Layout as the wrapper so the user can swipe the input away if they change there mind and a Box Inset layout to make sure it is readable on any screen inside that. A scroll view isn’t necessary if you aren’t going to include any canned responses. I also include a microphone button and a keyboard button.

I use a Fragment class to include all the code for the input. Here’s the basic code for getting the input from the mic:

// Create an intent that can start the Speech Recognizer activity
private void displaySpeechRecognizer() {
    Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
            RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    // Start the activity, the intent will be populated with the speech text
    startActivityForResult(intent, SPEECH_REQUEST_CODE);
}

this is the function to run when the user clicks on the voice input button. It starts the speech recognizer intent a private integer variable can be used for the SPEECH_REQUEST_CODE so we can get the result back later by overriding onActivityResult.

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    Cat.d("Got Request Code " + SPEECH_REQUEST_CODE + "  " + requestCode);
    Cat.d("Got Result Code " + resultCode + " " + RESULT_OK);
    if (requestCode == SPEECH_REQUEST_CODE) {
        try {
            List<String> results = data.getStringArrayListExtra(
                    RecognizerIntent.EXTRA_RESULTS);
            String spokenText = results.get(0);
            sendResult(spokenText);
        } catch (NullPointerException ex) {
            Cat.d("No result received");
            // If they are using android wear 1.x, back out now
            if(Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
                removeThis();
            }
        }
    }
    super.onActivityResult(requestCode, resultCode, data);
}

This function runs when a result comes back. We need to check to make sure the the code we sent matches then get the first result. There is also the possibility that there won’t be a result. If this is the case, we either do nothing or just remove the fragment if it is android wear 1 since there is not keyboard input.

In the next part, I will explain an easy way to get keyboard input without leaving the app.

Getting colors like Android’s media style notifications

Getting colors like Android’s media style notifications

Android's media style notification

Android’s media style notification

Update There is a new library by Mateusz Kaflowski who was kind enough to post it in the comments. The tutorial below gives you information on developing your own color fetching script and how to tweak it to your liking, but if you want to make sure the colors match the notification exactly, I recommend you use his. More info here.


I was recently writing an Audiobook app and wanted to get dynamic colors based on the book cover similar to Android’s media style notifications. Obviously the best method is to use the Palette API, but implementing it for the most aesthetically pleasing colors with good contrast wasn’t immediately clear.

Phonograph Screenshot

Phonograph’s colors with white buttons and text

Some popular apps like Pocketcasts for Phonograph seem to get the most vibrant color they can find from the art and then darken it so the white buttons and text will have enough contrast. I used this method for previous apps since it was easy but this method bothered me since it was possible that the primary color wouldn’t match very well and often clashed with the notification.

To better match Android’s notifications we need to also fetch a complementary color from the art that has enough contrast for good readability.

To start, let’s generate a palette:

public static Palette generatePalette(Bitmap bitmap) {
    if (bitmap == null) return null;
    Palette palette = new Palette.Builder(bitmap)
            .generate();
    if(palette.getSwatches().size() <= 1) {
        palette = new Palette.Builder(bitmap)
                .clearFilters()
                .generate();
    }
    return palette;
}

There are a couple things to note here. By default, the palette builder has filters that disable colors that are close to black or white or certain hues that might cause accessibility issues for color blind folks like me. The second part in green will clear all filters in case these are the only colors that can be found. You can even add your own filters using addFilter.

Next, we need to be able to easily figure out the distance between two LAB colors. We can do this easily with ColorUtils.

private static double calculateDistance(int color0, int color1) {
    double[] lab0 = new double[3];
    double[] lab1 = new double[3];

    ColorUtils.colorToLAB(color0, lab0);
    ColorUtils.colorToLAB(color1, lab1);

    return ColorUtils.distanceEuclidean(lab0, lab1);
}

Now we need a way to compare different swatches to find the swatch with the greatest distance.

private static class distanceComparator implements Comparator<Palette.Swatch> {
    private int color;
    private distanceComparator(int color) {
        this.color = color;
    }

    @Override
    public int compare(Palette.Swatch swatch1, Palette.Swatch swatch2) {
        return (int)(calculateDistance(swatch2.getRgb(), color)
                - calculateDistance(swatch1.getRgb(), color));
    }
}

Okay. Now everything is set up to get the colors.

public static int[] getMatchingColors(Palette palette, int[]fallback, boolean invert) {
    if (palette != null) {
        if(palette.getDominantSwatch() != null) {
            Palette.Swatch swatch = palette.getDominantSwatch();
            int swatchColor = swatch.getRgb();
            int matchingColor = -1;
            List<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
            
            Collections.sort(swatches, new distanceComparator(swatchColor));
            for(Palette.Swatch contrastingSwatch : swatches) {
                Cat.d("Testing contrast to " + ColorUtils.calculateContrast(contrastingSwatch.getRgb(), swatchColor));
                if(ColorUtils.calculateContrast(contrastingSwatch.getRgb(), swatchColor) > MIN_ALPHA_CONTRAST) {
                    matchingColor = swatches.get(0).getRgb();
                    break;
                }
            }
            if(matchingColor == -1)
                matchingColor = swatch.getBodyTextColor();

            if(invert)
                return new int[]{matchingColor, swatchColor};
            return new int[]{swatchColor, matchingColor};
        }
    }
    return fallback;
}

Some explanation here. getDominantSwatch() gets the swatch generated that appears the most in the bitmap and is close to what notifications use for the background.

Next we can get a list of all the swatches that were generated in the palette and use the comparator we made earlier to sort them by the greatest distance. We can then loop through them making sure the swatch also has a high enough contrast for readability. More information can be found in the Material guidelines for this.

If no matching color can be found, we can use getBodyTextColor() to get a guaranteed contrasting color. (this will either be white or black with some alpha)

I also added an invert option to my function in case a user wants to invert the colors for their audiobook to make it lighter or darker.

NavBooks Screenshot

The completed result in NavBooks.

The result gives you colors that are generally very close to the notification, but may still not match up perfectly. I think there are a couple things that may factor into the difference such as the bitmap being resized. I suspect the notification may also be getting the dominant color from the left side of the image for the  gradient it uses. You can try to get these to match better if you want, but I think the colors generated with this method will get as good results.

Do you have any other methods for generating colors that you use?

A simple way to get watch screen type from LinearLayout

A simple way to get watch screen type from LinearLayout

Recently I was trying to make some adjustments to a screen size for Android Wear so the watch would match the screen size. Most of the ways to do this always seem roundabout to me. I wanted a way to call a simple function like isRound()and get the information I need. I found a custom view someone made on the forms and modified it to return whether the screen is round or square as well.

Here is the custom view class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class WearLinearLayout extends LinearLayout {

    private int chin;
    private boolean isRound;
   
    public WearLinearLayout(Context context) {
        super(context);
    }

    public WearLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public WearLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public int getChinSize() {
        return chin;
    }

    public boolean isRound() {
        return this.isRound;
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        this.chin = insets.getSystemWindowInsetBottom();
        this.isRound = insets.isRound();

        return insets;
    }
}

It can then be implemented in the layout just like a LinearLayout.

Here is an example of how I used it to change the padding size of the bottom of the layout if the watch wasn’t round or had a chin:

1
2
3
if(!linearLayout.isRound() || linearLayout.getChinSize() > 0) {
                        int paddingBottom = getResources().getDimensionPixelSize(R.dimen.square_chin_padding_bottom);
                        controls.setPadding(0,0,0, paddingBottom);

Another Way…

Here’s a method I got from Sterling Udell on the Android Wear Developers Community.

Declare two resource values in bools.xml

1
2
3
4
5
6
7
8
9
values/bools.xml:
<resources>
    <bool name="is_round">false</bool>
</resources>

values-round/bools.xml:
<resources>
    <bool name="is_round">true</bool>
</resources>

and call getResources().getBoolean()

You can then check if the height is smaller than the width of the screen on a round screen to see if it has a chin. I made a class I use in my projects called DimenHelper to do just that:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DimenHelper {
    public static int GetScreenWidth(Context context) {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        return displayMetrics.widthPixels;
    }

    public static int GetScreenHeight(Context context) {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        return displayMetrics.heightPixels;
    }
}

What do you think? Does this seem like an acceptable method, or is there an easier way to do this?

How to crossfade drawer layouts in wearable:2.0.5

How to crossfade drawer layouts in wearable:2.0.5

I have a wearable drawer layout in my Android app. The behavior was that the peek view would display the peek fragment and when the user swiped up, the peek fragment would fade to the content fragment.

I recently attempted to update my android wear project from wearable 2.0.3 to wearable 2.0.5. A lot of components need to be changed from wearable.view to wear.widget.

After the update, both fragments display weather the drawer is open or closed. I tried fading these in and out manually, but there didn’t seem a way to do this smoothly or easily. Is the previous behavior possible anymore?

A side note: Using a view within the activity instead of a fragment for the peek view will work as before, but remove the chevron symbol. Another workaround is to add the symbol yourself, but the animation doesn’t work as before.

Previous Drawer Layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<android.support.wearable.view.drawer.WearableActionDrawer
   android:id="@+id/bottom_action_drawer"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="?android:attr/colorPrimary"
   app:drawer_content="@+id/nowPlayingFragment">
    <!-- Peek View -->
    <fragment
       android:id="@+id/peekFragment"
       android:name="com.turndapage.navmusic.ui.fragment.PeekFragment"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" />

    <fragment
       android:id="@+id/nowPlayingFragment"
       android:name="com.turndapage.navmusic.ui.fragment.NowPlayingFragment"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />

</android.support.wearable.view.drawer.WearableActionDrawer>

 

New Drawer Layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<android.support.wear.widget.drawer.WearableActionDrawerView
    android:id="@+id/bottom_action_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/colorPrimary"
    app:drawer_content="@+id/nowPlayingFragment"
    app:peekView="@+id/peek_view">


    <fragment
        android:id="@+id/nowPlayingFragment"
        android:name="com.turndapage.navmusic.ui.fragment.NowPlayingFragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <!-- Peek View -->
    <fragment
        android:id="@+id/peekFragment"
        android:name="com.turndapage.navmusic.ui.fragment.PeekFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.wear.widget.drawer.WearableActionDrawerView>

 

My manual solution was this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
wearableDrawerLayout.setDrawerStateCallback(new WearableDrawerLayout.DrawerStateCallback() {

        @Override
        public void onDrawerStateChanged(WearableDrawerLayout layout, int newState) {
            super.onDrawerStateChanged(layout, newState);
            if(nowPlayingUtil != null) {
                // If the drawer is settling into position and it was opened all the way.
                if (wearableActionDrawer.getDrawerState() == WearableDrawerView.STATE_SETTLING && nowPlayingUtil != null) {
                    // Transitioning from peek view
                    if (drawerState == "drawerPeeking")
                        nowPlayingUtil.fadeToOpen();
                    // Transitioning from open view
                    else if (drawerState == "drawerOpen")
                        nowPlayingUtil.fadeToPeek();
                    else if (drawerState == "drawerClosed")
                        nowPlayingUtil.fadeToPeek();
                }
                if (wearableActionDrawer.getDrawerState() == WearableDrawerView.STATE_IDLE) {
                    // update the previous State
                    if (wearableActionDrawer.isOpened()) {
                        drawerState = "drawerOpen";
                        nowPlayingUtil.fadeToOpen();
                    } else if (wearableActionDrawer.isPeeking()) {
                        drawerState = "drawerPeeking";
                        nowPlayingUtil.fadeToPeek();
                    } else if (wearableActionDrawer.isClosed()) {
                        drawerState = "drawerClosed";
                        nowPlayingUtil.fadeToPeek();
                    }
                }
            }
        }
    });

In my example, the fadeToPeek and fadeToOpen animate the opacity of the two views to create the desired effect. Hopefully there will be a better solution in the future, but I am satisfied with this one.

Next to Evil – 2010

Next to Evil – 2010

Duet is a woman with a strange talent. She is also a member of the most powerful family in the world. When she sees the destruction her family causes to the empire and a strange veiled woman gives her the opportunity to betray them, she must decide if starting a revolution is really what she wants.
  • Indie Games Freeware Game Pick
  • Unique mechanics
I have some expectations in a MAGS game- I expect a short and traditional game. NTE delivers and I didn’t feel disappointed after the ~20 minutes I spend on it. It was like picking up a pleasant “More Of The Same” paperback- presentation is very nice, and the story works out well enough, so well worth a look for anyone who prefers a serious tale to wacky fantasy.
-Ghost

Download

A Woman For All Seasons – 2010

A Woman For All Seasons – 2010

Occlude the wizard comes into existence when lightning strikes marble. His goal is to fight a seasonal curse on a princess. Using his natural magical talent with a wand.
  • Gesture System
  • Full Voice Acting
  • 2nd in the One Room One Week Competition
Lovely little game! Really well done – interesting and neat spell system, sweet fairy tale story, good graphics. And it made me remember the wonderful Loom. Thank you!
-Ilyich

Download

Raven – 2007

Raven – 2007

Raven is a fantasy adventure game about a Raven who is given human intelligence by the ogeress Eurybia so he can be her spy. He soon realizes that Eurybia is hatching an evil scheme to take over the Land of Hellenas! I really had fun with the art on this game.

Download

Lif and The Treasure of The Tanones – 2007

Lif and The Treasure of The Tanones – 2007

Lif the lizard has lived by himself in the Barren Desert of the West peacefully for a long time until he starts having trouble with money. He is forced to find some way to get 5,000 dollars by tomorrow. When he hears of the Treasure of the Tanones, he starts off on his adventure to find it.
  • Full Soundtrack by Mark Lovegrove
  • Download Here
The Graphics and music are very good… the puzzles are pretty good but some are pretty tricky.
-uvaagsfan

Download