torsdag 26 april 2012

Android : Custom Tab layouts just using XML

Switched to using a Tabhost layout in my application, the stock one felt a big large height-wise as an image is supposed to be embedded, and i just wanted to use a simple text phrase per tab. Felt a bit off a hassle to create 9-patch images for everything, found way to many in the SDK, so this is the way i did it!

So, first we need to create a really simple layout for each Tab.
layout/custom_tab.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/tabTitleText"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:gravity="center_horizontal"
          android:clickable="true"
          android:padding="5dp"
          android:textSize="15sp"
          android:textStyle="bold"
          android:ellipsize="marquee"
          android:singleLine="true"
          android:textColor="@color/tab_textcolor"
          android:background="@drawable/tab_selector"/>

That's it for my Tab view! Clickable set to true because i want to color to change to white when pressing the tab. Padding of course, otherwise it'll be to small and singleline so it won't wrap around. Next our text color selector.

color/tab_textcolor.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true" android:color="#FFFFFF" />
    <item android:state_pressed="true" android:color="#FFFFFF" />
    <item android:color="@android:color/darker_gray" />
</selector>

Nothing special here, text color will be white when tab is selected or pressed, otherwise gray.

drawable/tab_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true" android:state_pressed="false" 
          android:drawable="@drawable/tab_bg_selected" />
    <item android:state_selected="false" android:state_pressed="false" 
          android:drawable="@drawable/tab_bg_unselected" />
    <item android:state_pressed="true" 
          android:drawable="@drawable/tab_bg_pressed" />
</selector>

Three different backgrounds for our TextView, selected, unselected and pressed.

drawable/tab_bg_selected.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item >
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF" />
            <corners android:topLeftRadius="5dp" android:topRightRadius="5dp"/>
        </shape>
    </item>

    <item  android:top="1dp" android:bottom="2dp" android:left="1dp" android:right="1dp">
    <shape android:shape="rectangle">
    <gradient
            android:startColor="#ff1673"
            android:endColor="#e6acc3"
            android:angle="270"
            android:type="linear"
            />
        <corners android:topLeftRadius="5dp" android:topRightRadius="5dp"/>
    </shape>
</item>
</layer-list>

Link to <layer-list> at Google. First we create a white colored rectangle (just gonna use it as a border), then we create our gradient on top of it but set the offset so we'll get white borders. Can use <stroke> but then we cant control the border, i wanted different widths and sometimes no width. Like when it's unselected.

drawable/tab_bg_unselected.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:top="10dp">
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF" />
        </shape>
    </item>
    <item android:bottom="2dp">
    <shape  android:shape="rectangle">
    <gradient android:centerColor="#655e5e" android:startColor="#3e3e3e"
              android:endColor="#807c7c"
              android:angle="-90" />
        <corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
    </shape>
    </item>
</layer-list>


Our unselected tab background. First a white rectangle, i want a border at the bottom. Notice i placed android:top="10dp" , need to offset it so it wont be white at the top corners in the gradient.

drawable/tab_bg_pressed.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item >
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF" />
            <corners android:topLeftRadius="5dp" android:topRightRadius="5dp"/>
        </shape>
    </item>
    <item  android:top="1dp" android:bottom="2dp" android:left="1dp" android:right="1dp">
        <shape android:shape="rectangle">
            <solid android:color="#ff1673" />
            <corners android:topLeftRadius="5dp" android:topRightRadius="5dp"/>
        </shape>
    </item>
</layer-list>

Looks like the first one except i just use a solid color when it's pressed. Sneak peek of the layout :

Lets move along to the startup screen layout.
layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@android:id/tabhost"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
         android:layout_marginTop="2dp">
    <LinearLayout
            android:paddingTop="2dp"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            >
        <TabWidget
                android:id="@android:id/tabs"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />
        <FrameLayout
                android:id="@android:id/tabcontent"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                />
    </LinearLayout>
</TabHost>

Standard TabHost, taken from the Android Developers homepage. Tab Layout Example.

layout/view_testlayout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:layout_marginTop="5dp">
    <Button android:id="@+id/button_Test"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            />
</LinearLayout>


The layout for the activites we create in the tabs.


src/MyActivity.java

import android.app.TabActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TabHost;
import android.widget.TextView;

public class MyActivity extends TabActivity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        TabHost tabHost = getTabHost();  // The activity TabHost
        TabHost.TabSpec spec;  // Resusable TabSpec for each tab
        Intent intent;  // Reusable Intent for each tab
        // Create an Intent to launch an Activity for the tab (to be reused)
        intent = new Intent().setClass(this, TestActivity.class);
        // Create our custom view. 
        View tabView = createTabView(this, "Tab 1");
        // Initialize a TabSpec for each tab and add it to the TabHost
        spec = tabHost.newTabSpec("tab1").setIndicator(tabView)
                .setContent(intent);
        tabHost.addTab(spec);
        // Do the same for the other tabs
        tabView = createTabView(this, "Tab 2");
        intent = new Intent().setClass(this, TestActivity.class);
        spec = tabHost.newTabSpec("tab2").setIndicator(tabView)
                .setContent(intent);
        tabHost.addTab(spec);
    }

    private static View createTabView(Context context, String tabText) {
        View view = LayoutInflater.from(context).inflate(R.layout.tab_custom, null, false);
        TextView tv = (TextView) view.findViewById(R.id.tabTitleText);
        tv.setText(tabText);
        return view;
    }
}




src/TestActivity.java

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.view_testlayout);

        MyActivity myActivity = (MyActivity) this.getParent();
        String currentTab = myActivity.getTabHost().getCurrentTabTag();
        ((Button)findViewById(R.id.button_Test)).setText(currentTab);

    }
}


Nothing strange here, i really like the drawables through XML, works fine for basic layout! So many different .PNG's to keep track of when customizing a Tab layout. Final screenshot :


I have a small problem though, did you notice ? I did smaller corners in the unselected tab. The tab_bg_selected drawable still seems to be there even though it's not selected. If i do a larger corner radius, like 10dp it will look like this.

I'll update if i'll found out what's wrong, feel free to leave a comment if you have any idea!

Update!
Didnt really found a solution why the selected drawable still is drawn when unselected so i just updated the tab_bg_unselected.xml layer-list with a rectangle that's the same color as the background.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/background_color" />
        </shape>
    </item>
    <item android:top="20dp">
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF" />
        </shape>
    </item>
    <item android:bottom="2dp">
    <shape  android:shape="rectangle">
    <gradient android:centerColor="#655e5e" android:startColor="#3e3e3e"
              android:endColor="#807c7c"
              android:angle="-90" />
        <corners android:topLeftRadius="10dp" android:topRightRadius="10dp"/>
    </shape>
    </item>
</layer-list>

No more visible corners ! : )



måndag 9 april 2012

Android : Multiple Selection ListView with Custom Layout

This is one solution for the troublesome ListView when not using the internal layouts. This blogpost helped me alot as toggling the checkbox in the adapter or in the ListView's setOnItemClickListener() gave me strange results, based on the visibility of the row. He implemented the Checkable interface in the layout container, i use the same extended RelativeLayout.

Lets begin with the CheckableRelativeLayout.
src/CheckableRelativeLayout.java
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Checkable;
import android.widget.RelativeLayout;

/**
 * Extension of a relative layout to provide a checkable behaviour
 *
 * @author marvinlabs
 */
public class CheckableRelativeLayout extends RelativeLayout implements
        Checkable {

    private boolean isChecked;
    private List<Checkable> checkableViews;

    public CheckableRelativeLayout(Context context, AttributeSet attrs,
                                   int defStyle) {
        super(context, attrs, defStyle);
        initialise(attrs);
    }

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

    public CheckableRelativeLayout(Context context, int checkableId) {
        super(context);
        initialise(null);
    }

    /*
      * @see android.widget.Checkable#isChecked()
      */
    public boolean isChecked() {
        return isChecked;
    }

    /*
      * @see android.widget.Checkable#setChecked(boolean)
      */
    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
        for (Checkable c : checkableViews) {
            c.setChecked(isChecked);
        }
    }

    /*
      * @see android.widget.Checkable#toggle()
      */
    public void toggle() {
        this.isChecked = !this.isChecked;
        for (Checkable c : checkableViews) {
            c.toggle();
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; ++i) {
            findCheckableChildren(this.getChildAt(i));
        }
    }

    /**
     * Read the custom XML attributes
     */
    private void initialise(AttributeSet attrs) {
        this.isChecked = false;
        this.checkableViews = new ArrayList<Checkable>(5);
    }

    /**
     * Add to our checkable list all the children of the view that implement the
     * interface Checkable
     */
    private void findCheckableChildren(View v) {
        if (v instanceof Checkable) {
            this.checkableViews.add((Checkable) v);
        }

        if (v instanceof ViewGroup) {
            final ViewGroup vg = (ViewGroup) v;
            final int childCount = vg.getChildCount();
            for (int i = 0; i < childCount; ++i) {
                findCheckableChildren(vg.getChildAt(i));
            }
        }
    }
}

After that just use it in your XML layout files, make it part of your package and use syntax <org.mypackage.myapp.CheckableRelativeLayout>

Our testclass Team, which we want to populate the ListView with.
src/Team.java
public class Team {
    private String teamName;
    private int teamWins;
    public  Team(String name, int wins)
    {
        teamName = name;
        teamWins = wins;
    }

    public String getTeamName() {
        return teamName;
    }

    public int getTeamWins() {
        return teamWins;
    }
}


Simple class for testing purposes, next up we build our ListView layout, what each row will look like.
I decided on a CheckBox (the new one as the old one felt big), and two TextViews to display the teamname & team wins. (Remember to look at the CheckableRelativeLayout class above as this isn't working with the normal containers.)

layout/row_team_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<se.adanware.listviewexample.CheckableRelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

   <!-- We dont want to be able to click the CheckBox -
        android:clickable="false" added.
        CheckableRelativeLayout takes care of the toggle when clicking the row -->
    <CheckBox
              android:id="@+id/myCheckBox"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:focusable="false"
              android:clickable="false"
              android:layout_alignParentLeft="true"
              android:background="@drawable/customcheckbox_background"
              android:button="@drawable/customcheckbox"
            />
    <TextView
            android:id="@+id/listview_TeamDescription"
            android:focusable="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="16sp"
            android:layout_toRightOf="@id/myCheckBox"
            android:layout_centerVertical="true"
            />
    <TextView
            android:id="@+id/listview_TeamWins"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="16sp"
            android:paddingRight="5dp"
            android:layout_centerVertical="true"
            android:focusable="false"
            android:layout_alignParentRight="true"
            />
</se.adanware.listviewexample.CheckableRelativeLayout>

I'm still targeting  the earlier Android version so i've taken the new checkbox from the SDK.

drawable-hdpi/customcheckbox.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="false"
          android:drawable="@drawable/btn_check_off_holo_dark" />
    <item android:state_checked="true"
          android:drawable="@drawable/btn_check_on_holo_dark" />
</selector>
drawable-hdp/customcheckbox_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/btn_check_label_background" />
</selector>
btn_check_label_background.9.png    


btn_check_on_holo_dark.png
btn_check_off_holo_dark.png
Hopefully the images can be seen, i'll change the background color later, now it's turn for our custom adapter that we'll populate the list with.
src/TeamListViewAdapter.java
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;

public class TeamListViewAdapter extends ArrayAdapter<Team>
        {
        View row;
        ArrayList<Team> myTeams;
        int resLayout;
        Context context;

        public TeamListViewAdapter(Context context, int textViewResourceId, ArrayList<Team> myTeams) {
            super(context, textViewResourceId, myTeams);
            this.myTeams = myTeams;
            resLayout = textViewResourceId;
            this.context = context;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {
            row = convertView;
            if(row == null)
            {   // inflate our custom layout. resLayout == R.layout.row_team_layout.xml
                LayoutInflater ll = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                row = ll.inflate(resLayout, parent, false);
            }

            Team item = myTeams.get(position); // Produce a row for each Team.

            if(item != null)
            {   // Find our widgets and populate them with the Team data.
                TextView myTeamDescription = (TextView) row.findViewById(R.id.listview_TeamDe                                                                       scription);
                TextView myTeamWins = (TextView) row.findViewById(R.id.listview_TeamWins);
                if(myTeamDescription != null)
                    myTeamDescription.setText(item.getTeamName());
                if(myTeamWins != null)
                    myTeamWins.setText("Wins: " + String.valueOf(item.getTeamWins()));
            }
            return row;
        }
}

Okay! Now we just need to make the root layout and our start activity! First a simple layout:

res/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Checkable Listview Example"
    />
    <Button android:id="@+id/buttonStart"
            android:layout_height="wrap_content"
            android:layout_width="fill_parent"
            android:text="Start tournament with selected teams"
            />
<ListView android:id="@+id/myListView"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:choiceMode="multipleChoice"/>
</LinearLayout>

Nothing unusual here, android:choiceMode="multipleChoice" cant be omitted.
It can also be set from code with myListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
There's also android:choiceMode="singleChoice" if you just want a single item checked.
If not set the ListView.getCheckedItemPositions() method will return a null SparseBooleanArray.

Finally we come to our startup activity!


src/ListViewExample.java
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.View;
import android.widget.*;
import java.util.ArrayList;

public class ListViewExample extends Activity
{
    ArrayList<Team> myTeams;
    TeamListViewAdapter myAdapter;
    ListView myListView;
    Button myButton;
    
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        myTeams = new ArrayList<Team>();
        // Add a few teams to display.
        myTeams.add(new Team("Winners", 10));
        myTeams.add(new Team("Philidelphia Flyers", 5));
        myTeams.add(new Team("Detroit Red Wings", 1));
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        myListView = (ListView) findViewById(R.id.myListView);
        myButton = (Button) findViewById(R.id.buttonStart);
        // Construct our adapter, using our own layout and myTeams
        myAdapter = new TeamListViewAdapter(this, R.layout.row_team_layout, myTeams );
        myListView.setAdapter(myAdapter);
        myListView.setItemsCanFocus(false);

        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ArrayList<Team> selectedTeams = new ArrayList<Team>();
                final SparseBooleanArray checkedItems = myListView.getCheckedItemPositions();
                int checkedItemsCount = checkedItems.size();
                for (int i = 0; i < checkedItemsCount; ++i) {
                   // Item position in adapter
                   int position = checkedItems.keyAt(i);
                   // Add team if item is checked == TRUE!
                   if(checkedItems.valueAt(i))
                      selectedTeams.add(myAdapter.getItem(position));
                }
                if(selectedTeams.size() < 2)
                  Toast.makeText(getBaseContext(), "Need to select two or more teams.", Toast                                                       .LENGTH_SHORT).show();
                else
                {
                   // Just logging the output.
                   for(Team t : selectedTeams)
                      Log.d("SELECTED TEAMS: ", t.getTeamName());
                }
            }
        });
    }
}


Thanks goes to http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/ for the elegant solution!



... Family time, nightie!