Showing posts with label layout. Show all posts
Showing posts with label layout. Show all posts

04 August 2016

Stylish Dynamic Layouts

One of the things we are taught in Android is that we should gracefully handle different layouts based on screen sizes. With more and more things being not just screen size-specific but also OS version-specific, this is one thing I think a lot more devs need to pay attention to. Today was my turn to do just that.

I wanted to create a dialog with an image that resizes itself while maintaining aspect ratio but being constrained by the container. Okay.

So I went about creating my ImageView:
<ImageView
android:id="@+id/permission_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@{ContextCompat.getDrawable(context, permission.descriptionDrawable)}"/>

Loaded up the layout and it looks satisfactory:

But then, I tried rotating and what the hell!

The image does not fit, you cannot see "NOT NOW". It doesn't work. Then I tried it on a tablet:

OMG how could it possibly get worse. There is so much space and it's not filling it up properly! I am pretty sure the root layout has layout_height="wrap_content". Weird.

After some furious Googling, I found out that I have to set the LayoutParams on the dialog after it has been shown. Why do I have to do that? I said wrap it and you refuse and now you want me to do it myself? Why?

I gave in and did the thing that StackOverflow tells me to. But it didn't work. It still looked the same. I tried moving stuff around; calling setLayout instead of setAttributes. It still looked the same. :sad_panda:

And so the time has come to look for another approach. A couple of things need to happen:
a. We shouldn't make the user scroll just to click the buttons on a landscape phone
b. Make the image take up more space on tablets

To make (a) happen, we either: create a new layout for this orientation or just set the image to be invisible. The second option is more appealing for me. There really is not much difference in the two orientations aside from the image after all. We can set the visibility of the image at runtime, but then maybe we can afford to show the image if there is enough space for it.

This means that to make (b) happen, maybe we need to tweak our layout a little. And this is where the beauty of Android's alternative resources come in. To be as less repetitive as possible, I chose to harness styles too, since I only need to override one attribute -- the visibility.

First I created a style in the default /res/values folder that I would apply to the image:
<style name="DescriptionImage">
   <item name="android:visibility">gone</item>
</style>

Now it's time to make the variations. We want to hide the image if we are in landscape, so into /res/values-land this style goes:
<style name="DescriptionImage">
   <item name="android:visibility">gone</item>
</style>

But we want to show it if there is enough space, so into /res/values-h500dp this style goes:
<style name="DescriptionImage">
   <item name="android:visibility">visible</item>
</style>

And now it's just a matter of applying this style to our image (and I found out that setting maxHeight works a bit better):
<ImageView
android:id="@+id/permission_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="200dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
style="@style/DescriptionImage"
android:src="@{ContextCompat.getDrawable(context, permission.descriptionDrawable)}"/>

I applied this technique to another dialog and here's the end result on a phone:


Yay!

22 February 2016

LinearLayouts, TextViews and Drawables

I sent out a series of tweets today about LinearLayouts and unexpectedly, quite a few people like them. I decided to get off my lazy ass and actually write it down in a post for easy reference.

Let's start with the LinearLayout root view.  One of the most common things designers ask us to do is to put dividers in. Quick! Think! What should we do? Add a generic View for each divider? How about NO? Instead, we can delegate the task of displaying these dividers to the LinearLayout itself:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:orientation="vertical"
              android:layout_height="match_parent"
              android:divider="@drawable/divider_horizontal_dark"
              android:showDividers="middle">

Where divider_horizontal_dark can be anything you want to be the divider (I recommend using a shape):

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle" >
    <solid android:color="#24000000" />
    <size android:height="1dp" />
</shape>

Doing this prevents us from littering our view hierarchy with useless empty views. LinearLayout is actually extremely powerful, and a lot of the stuff most apps usually need is already built in. PS: For some reason I cannot find the showDividers attribute in the Android docs, but the available attributes are middle, beginning, endnone (or a combination of those). EDIT+Nick Butcher showed us the light: docs here!

EDIT AGAIN: Nick has also lovingly pointed out that setShowDividers() was added in API 11. If by some cruel twist of fate you need to support anything below that, use LinearLayoutCompat. Also, you poor, poor thing.

Another common thing that we are asked to do is to have an image +  text displayed side by side. We use this quite a bit in the Domain app, most notably in the main navigation drawer:

Instead of having one huge RelativeLayout or (horror!) nested LinearLayouts, we can just have a bunch of TextViews in one LinearLayout.

You see, TextViews have this magical ability to add Drawables to themselves. You can position the Drawable anywhere you want, and even tint it from XML! :gasp:

Drawable placement can be in any of the cardinal directions (bottom, top, left, right) and the tint should be a defined colour in XML (not actually required, but encouraged. By me. I encourage it.). If the Drawable is too close to the text for your or your designer's taste, you can adjust the distance via the drawablePadding attribute.

So here's a screen of four TextViews in one LinearLayout:



And the full code (also in github):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:orientation="vertical"
              android:layout_height="match_parent"
              android:divider="@drawable/divider_horizontal_dark"
              android:showDividers="middle">

    <TextView
        android:id="@+id/text1"
        android:drawableTop="@drawable/ic_notifications_black_24dp"
        android:drawableTint="@color/lemongrab"
        android:drawablePadding="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:padding="16dp"
        android:text="Text 1"/>

    <TextView
        android:id="@+id/text2"
        android:textSize="20sp"
        android:drawableRight="@drawable/ic_notifications_black_24dp"
        android:drawablePadding="8dp"
        android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Text 2"/>

    <TextView
        android:id="@+id/text3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/ic_notifications_black_24dp"
        android:drawablePadding="8dp"
        android:padding="16dp"
        android:text="Text 3"
        android:textSize="20sp"/>


    <TextView
        android:id="@+id/text4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/ic_notifications_black_24dp"
        android:drawablePadding="8dp"
        android:padding="16dp"
        android:text="Text 4"
        android:textSize="20sp" />


</LinearLayout>

If you need to update the Drawable tint at runtime (if you are basing it on some status field, for example), you can do so via code:

// left, top, right, and bottom
DrawableCompat.setTint(mText3.getCompoundDrawables()[0].mutate(), ContextCompat.getColor(this, R.color.red));

Note that we need to call mutate() or else the tint will be everywhere and it's gonna be a mess!

So there you have it! Remember kids, #perfmatters!

08 June 2014

Quick Tip: Understanding Alternate Resources

Trying to support as many devices as possible the best way possible is a very daunting task indeed. You will usually need to provide a lot of different layouts, strings, or dimensions (among others) to make your app look great whatever the user's device is. And then you start chaining resource qualifiers and testing which resource is being loaded by the OS can become a nightmare very quickly.

Here's a trick I started using which seemed to work quite well. Create a string (the app name works well if you have an Action Bar) that you can display on-screen (or just throw into a Log or a Toast) that will quickly let you know from which of those very many /res folders your resources are being pulled out of.

For this demo, I have the following /res/values-xxxxx folders:

As you can see, there are strings.xml files in each of the configurations I want to support. Each of these files will contain whatever description we want to see on the device or in the Logs. In my case, I have custom strings for each form factor and orientation, which allows me to validate things easier. But this is particularly useful if the changes per form factor and orientation is subtle, such as dimensions.

Here's what it looks like for phones:


And here are the outputs for tablets:

Each of the strings.xml files contains just these two strings (sample taken from /res/values-sw800dp):


    MultiDeviceSupport - Tablet Portrait
    Hello world on a tablet!


Have fun debugging!

05 June 2014

Adding attributes to a custom view

There are times when using the default Android Views just doesn't cut it and you need to create your own version of a View. So how exactly do you do that? It's as simple as subclassing the View! But what if you want to add customizable attributes? Here's how.

Let's say I am creating a form-filling application and I want some of the EditTexts in the form to be required. However, I am so tired of having to implement the error checking for each and every one of those fields. What I will do is create my own EditText that will do the validation for me if that particular field is required. Let's do it.

Step 1: Create your custom view and create fields for the attributes you want. In this case, I want an EditText that will show the default EditText error display if the user has not put in any value.
public class RequiredEditText extends EditText {
   private boolean mRequired;
   private String mErrorMessage;

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

   /**
   * Set this EditText's requirement validation. The error message
   * will be set to null by default if not provided.
   * 
   * @param required
   * @param errorMessage (optional)
   */
   public void setRequired(boolean required, String errorMessage) {
      this.mRequired = required;
      this.mErrorMessage = errorMessage;
  
      invalidate();
      requestLayout();
   }
 
   public void setRequired(boolean required) {
      setRequired(required, null);
   }

   /**
   * Lets you know if this field is set as required or not
   * @return
   */
   public boolean isRequiredField() {
   return mRequired;
   }
}
Step 2: If a field is required, we want the default error message to appear (with our own error message, of course). If the user fills in the EditText, we want the error to disappear.
public class RequiredEditText extends EditText {
   private boolean mRequired;
   private String mErrorMessage;

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

   /**
   * Set this EditText's requirement validation. The error message
   * will be set to null by default if not provided.
   * 
   * @param required
   * @param errorMessage (optional)
   */
   public void setRequired(boolean required, String errorMessage) {
      this.mRequired = required;
      this.mErrorMessage = errorMessage;
  
      manageRequiredField(required);
  
      invalidate();
      requestLayout();
   }
 
   public void setRequired(boolean required) {
      setRequired(required, null);
   }

   private void manageRequiredField(boolean required) {
      // If we are required, set the listeners
      if(required) {
         setOnFocusChangeListener(mFocusChangeListener);
         addTextChangedListener(mTextWatcher);
      } else {
         // In case there is an error message already, clear it
         setError(null);
   
         // Remove the listeners
         setOnFocusChangeListener(null);
         removeTextChangedListener(mTextWatcher);
      }
   }

   /**
   * Lets you know if this field is set as required or not
   * @return
   */
   public boolean isRequiredField() {
      return mRequired;
   }

   OnFocusChangeListener mFocusChangeListener = new OnFocusChangeListener() {

      @Override
      public void onFocusChange(View v, boolean hasFocus) {
         // If the focus was removed from the field and it IS required,
         // check if the user has put in something
         if(!hasFocus && mRequired){
            isRequiredFieldFilled();
         }
      }
   };
 
   TextWatcher mTextWatcher = new TextWatcher() {

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
         // Once the user types in something, remove the error
         setError(null);
      }

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) { /* do nothing */ }

      @Override
      public void afterTextChanged(Editable s) { /* do nothing */ }   
   };

   private boolean isRequiredFieldFilled() {
      // If the EditText is empty, show the error message
      if(TextUtils.isEmpty(getText().toString().trim())){
         showRequiredErrorDrawable();
         return false;
      }
      return true;
   }

   private void showRequiredErrorDrawable() {
      setError(mErrorMessage);
   }
}
Step 3: Now it's time to add the fields to the Layout Editor. Create an attrs.xml file in /res if there isn't one already. Declare a styleable and include the attributes you want to appear in the Layout Editor.

   
       
       
   

The name of the styleable does not need to match your custom view class name, but doing so makes it more readable and maintainable.

Step 4: Go back to your custom view implementation and add constructors that take in an AttributeSet.
public RequiredEditText(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   init(attrs);
}

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

private void init(AttributeSet attrs) { 
   TypedArray a=getContext().obtainStyledAttributes(
      attrs,
      R.styleable.RequiredEditText);

   try {
      setRequired(a.getBoolean(R.styleable.RequiredEditText_required, false), 
            a.getString(R.styleable.RequiredEditText_errorMessage));
   } finally {
      //Don't forget this, we need to recycle
      a.recycle();
   }
}
Step 5: Go to the Layout Editor and look for "Custom & Library Views" in the Palette (you may have to click on "Refresh" several times before your custom view appears in the list). Add the custom view to your layout and check out the properties panel!
 

 As always, the code is in GitHub.

28 December 2012

Styling tab selectors for ActionBarSherlock

This post builds on the previous post for ABS + VPI.

I have gotten a lot of questions on styling the ABS, specifically the tab selected indicators. This task may seem daunting, but in reality it is relatively simple. I am also quite confused why I didn't find any straightforward tutorials when I tried googling this. Anyway, this post will hopefully help developers who aim to style their action bars.

We will need to use Jeff Gilfelt's awesome styling tool. To use it, just input in your app's color scheme for the highlights and what-not then download the generated zip file. If you want to just change the tab underline color, change the value under "Accent color". To make the change obvious from the default settings, I have used a shade of orange for the tab selected indicators. Here are the settings I used for this demo.

Jeff's tool conveniently generates all the XML files and drawables and organizes them into the correct /res/drawable folders. Unzip the file and just drag the generated files into your project, or merge the contents if you already have such existing files.

See how easy it was? Seriously, we should all lie prostrate at Jeff Gilfelt's and Jake Wharton's feet. These guys are AWESOME.

The two photos below show the difference between the old app and the styled app. As you can see, I only changed the tab selected color. You can explore what happens if you use the other styles you can get from Jeff's tool.
DefaultStyled
The hard work is done, let's now try to understand the automagically generated configurations we did.

First, we have to configure the theme to use custom tab indicators. We do this by changing the tab styles in themes.xml. In my sample app, these lines customize the tabs we are using.
<!-- Styling the tabs -->
<style name="CustomTabPageIndicator" parent="Widget.TabPageIndicator">
   <!-- Tab text -->
   <item name="android:textSize">15dip</item>
   <item name="android:textColor">#FFB33E3E</item>
        
   <!--  Lines under tabs -->
   <item name="background">@drawable/tab_indicator_ab_styling_abs</item>
   <item name="android:background">@drawable/tab_indicator_ab_styling_abs</item>
</style>

The "drawable" tab_indicator_ab_styling_abs is actually a state list drawable that provides different images for each state of a tab -- focused and non-focused, pressed and not pressed.

You can see part of this in action in the image above. Try long pressing on a tab and you can see the tab's background change to use the unselected-pressed drawable.

Again, HUGE thanks to Jeff Gilfelt and Jake Wharton for creating tools that make our lives easier. :)

I have pushed a new branch in github that uses this style. The master branch of that repo still uses the default tab selectors.

31 March 2011

My EditText is cut off by the on-screen keyboard!

With clients demanding left and right that my app should look like an iPhone app, I tend to be unappreciative of the way Android natively handles UI interactions and such. Notice how the screen automagically scrolls up when you click on an EditText? It turns out that in iPhone development, the developer does this manually (indicate how much the view should scroll when the on-screen keyboard appears, then scroll it back down afterwards). HA!

But even magic fails sometimes. Has this ever happened to you?

The bottommost EditText is cut off. And we don't want that!

So what do we do? Do we programmatically scroll the view up? I don't want to do that! It turns out that we can just wrap the whole view in a ScrollView and it will scroll up properly!


The only downside to this is that you might want to hide the scrollbar when the view moves up to accommodate the on-screen keyboard. And to do that, I was trying to set the android:scrollbars="none" attribute but for one reason or another the scrollbar is still being drawn. To make the scrollbar disappear, we can do it from code as such:
((ScrollView)findViewById(R.id.my_scrollview))
.setVerticalScrollBarEnabled(false);
And we're done!

10 December 2010

What happened to my layout editor?

There you are, happily creating your layout files in the Eclipse plug-in's layout editor. Dragging and dropping is a breeze. But then one day, you open a layout XML file and boom! No UI! All you see is the XML tree with all the nodes and attributes. What happened?

This happened to me and I was in a panic for a few seconds. Why do things like these have to happen to me? I tried opening a layout file from another project in the same workspace, and it has the UI! What happened?

It probably has something to do with the interpreter you used to open the XML file, a voice in my head said. So I tried right-clicking on the XML file, and lo and behold, I found it. I may have accidentally clicked on some file in one of my mad-clicking moments and changed the setting.

So anyway, to bring back the Layout UI Editor, right click on an XML file > Choose Open With > Android Layout Editor.

I would say that everything is handy dandy, but apparently, the engineers at Google decided that we developers need a little less help and removed the very useful up and down arrow keys in the Outline View when editing XML layouts. Why do they hurt us like this?

I WANT MY UP/DOWN KEYS BACK!