Custom Menu Bar (tabs) – How to hook the menu button to show/hide a custom tab bar

No Comments

I want to begin this tutorial by crediting Josh Clemm for the underlying Custom Tabs implementation found in this example. The way he accomplished customization is very clean and the best method I have seen to date.

The subject of this tutorial is how to create your own custom menu bar that will be shown and hidden when the user clicks the menu button on his/her device. There are many other uses for this example and it just isn’t limited to a menu or tab bar. For example, you can use this to show and hide a panel that contains whatever content you want to be shown. It could be used for contextual help or MediaPlayer controls, etc.

Custom Menu Panel
Custom Menu Bar

The basis of this example is the layout. I have found that the RelativeLayout is superior for creating great layouts that perform well across devices. If you want to learn more about what a RelativeLayout can do for you I highly recommend that you check out Working with Multiple Android Screens in the Motorola Android Technical Library. For the purpose of this tutorial… I haven’t followed everything that I should have as far as creating a good cross device application by defining my layouts using dip (Density Independent Pixels)… but this is just an example. Now let’s get to it.

The main layout for the application contains 2 RelativeLayouts, a TextView, a TabHost, a TabWidget and a View. In particular, I want to point out the layout_alignParentBottom attribute of the panel RelativeLayout. This attribute is one of 4 separate attributes that allow you to place a RelativeLayout on any side of the parent layout. I also want to bring your attentions to the visibility attribute which allows us to set the layout as hidden. Using “gone” makes the layout to be completely hidden, as if the view had not been added.

/res/layout/main.xml

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@drawable/background"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:background="#000000"
        android:id="@+id/content"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="">
    </TextView>    
    <RelativeLayout
        android:layout_alignParentBottom="true"
        android:id="@+id/menuPopup"
        android:background="@drawable/tl_bg"
        android:layout_width="fill_parent"
        android:layout_height="90px"
        android:visibility="gone">
            <TabHost
                android:id="@android:id/tabhost"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
            <LinearLayout
                android:orientation="vertical"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
                <View
                    android:layout_width="fill_parent"
                    android:layout_height="0.5dip"
                    android:background="#000">
                </View>
                <TabWidget
                    android:id="@android:id/tabs"
                    android:layout_width="fill_parent"
                    android:layout_height="fill_parent"
                    android:layout_marginLeft="0dip"
                    android:layout_marginRight="0dip">
                </TabWidget>
                <View
                    android:layout_width="fill_parent"
                    android:layout_height="2dip"
                    android:background="#696969">
                </View>
                <View
                    android:layout_width="fill_parent"
                    android:layout_height="2dip"
                    android:background="#000">
                </View>
                <FrameLayout
                    android:id="@android:id/tabcontent"
                    android:layout_width="fill_parent"
                    android:layout_height="fill_parent">
                </FrameLayout>
            </LinearLayout>
        </TabHost>
    </RelativeLayout>
</RelativeLayout>

The second layout is for each individual tab. I am not going to go into how to customize the tabs in this tutorial. You can check out Josh Clemm’s tutorial from the link above to learn more.
/res/layout/tabs_layout.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tabsLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/tab_bg_selector"
    android:padding="5px"
    android:gravity="center"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/tabsIcon"
        android:layout_width="36px"
        android:layout_height="36px">
    </ImageView>
    <TextView
        android:id="@+id/tabsText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5px"
        android:text=""
        android:textSize="12px"
        android:textColor="@drawable/tab_text_selector">
    </TextView>
</LinearLayout>

The first activity is called CustomMenuBar.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package com.androidworkz.example.custommenubar;

/* Proper credit where credit is due
 * The custom tabs implementation contained within is adapted from an example
 * by Josh Clemm @ http://joshclemm.com/blog/?p=136
 * His custom tabs example is also located at http://code.google.com/p/android-custom-tabs/
 * The icons are from an icon pack that I found some time ago and can't remember where
 */


import android.app.TabActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory;
import android.widget.TabHost.TabSpec;
import android.widget.TextView;

public class CustomMenuBar extends TabActivity {
   
    private TabHost mTabHost;
    private static RelativeLayout mMenuPanel;
    private TextView content;

    private final static String WEBSITE = "http://www.androidworkz.com";
   
    // This isn't necessary but it makes it nice to determine which tab I am on in the switch statement below
    private static class TabItem {
        public final static int SEARCH = 0;
        public final static int SHARE = 1;
        public final static int WEBSITE = 2;
        public final static int SETTINGS = 3;
        public final static int QUIT = 4;
    }
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        setupViews();
    }
   
    private void setupViews() {
       
        content = (TextView) findViewById(R.id.content);
        content.setText(getString(R.string.search));
       
        mMenuPanel = ((RelativeLayout) findViewById(R.id.menuPopup));
        mMenuPanel.setVisibility(View.GONE);
       
        mTabHost = (TabHost) findViewById(android.R.id.tabhost);
        mTabHost.setup();
       
        mTabHost.getTabWidget().setDividerDrawable(R.drawable.tab_divider);
       
        addActivityTab(new TextView(this), "Search", R.drawable.search, new Intent(CustomMenuBar.this, Search.class));
        addActivityTab(new TextView(this), "Share", R.drawable.heart, new Intent(CustomMenuBar.this, Share.class));
        addMethodTab(new TextView(this), "Website", R.drawable.globe);
        addActivityTab(new TextView(this), "Settings", R.drawable.tools, new Intent(CustomMenuBar.this, Settings.class));
        addMethodTab(new TextView(this), "Quit", R.drawable.power);

        mTabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
            @Override
            public void onTabChanged(String arg0) {
                if (mMenuPanel != null) {
                    if (mMenuPanel.getVisibility() == View.VISIBLE) {                  
                        toggleMenu();
                    }
                    switch (mTabHost.getCurrentTab()) {
                        case TabItem.SEARCH:
                            content.setText(getString(R.string.search));
                            break;
                        case TabItem.SHARE:
                            content.setText(getString(R.string.share));
                            break;
                        case TabItem.WEBSITE:
                            content.setText(getString(R.string.website));
                            final Intent visit = new Intent(Intent.ACTION_VIEW);
                            visit.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            visit.setData(android.net.Uri.parse(WEBSITE));
                           
                            // Use a thread so that the menu is responsive when clicked
                            new Thread(new Runnable() {
                                public void run() {
                                    startActivity(visit);
                                }
                            }).start();                
                            break;
                        case TabItem.SETTINGS:
                            content.setText(getString(R.string.settings));
                            break;
                        case TabItem.QUIT:
                            content.setText(getString(R.string.quit));
                            new Thread(new Runnable() {
                                public void run() {                        
                                    CustomMenuBar.this.finish();
                                   
                                    // The following makes the Android Gods frown upon me
                                    android.os.Process.killProcess(android.os.Process.myPid());
                                    System.exit(0);
                                }
                            }).start();
                            break;
                        default:
                            break;
                    }
                   
                    // Handle click on currently selected tab - hide menu bar
                    // IMPORTANT: This listener has to appear AFTER the tabs are added
                    // Unfortunately, This doesn't work when the current tab contains an activity (except for tab 0)
                    // If you only have method tabs then it works perfect
                    mTabHost.getTabWidget().getChildAt(mTabHost.getCurrentTab()).setOnClickListener(new View.OnClickListener() {
                       
                        @Override
                        public void onClick(View v) {
                            toggleMenu();
                        }
                    });
                   
                    //if you want to reset the current tab
                    // mTabHost.setCurrentTab(0);
                }
            }
        });
    }
   
    // Use this method to add an activity or intent to the tab bar
    private void addActivityTab(final View view, final String tag, int iconResource, Intent intent) {
        View tabview = createTabView(mTabHost.getContext(), tag, iconResource);
         
        TabSpec setContent = mTabHost.newTabSpec(tag).setIndicator(tabview)
                .setContent(intent);
        mTabHost.addTab(setContent);

    }
   
    // Use this method if you only want the tab to execute a method
    private void addMethodTab(final View view, final String tag, int iconResource) {
        View tabview = createTabView(mTabHost.getContext(), tag, iconResource);

        TabSpec setContent = mTabHost.newTabSpec(tag).setIndicator(tabview)
                .setContent(new TabContentFactory() {
                    public View createTabContent(String tag) {
                        return view;
                    }
                });
        mTabHost.addTab(setContent);

    }
   
    private static View createTabView(final Context context, final String text,
            int iconResource) {
        View view = LayoutInflater.from(context)
                .inflate(R.layout.tabs_layout, null);
        TextView tv = (TextView) view.findViewById(R.id.tabsText);
        tv.setText(text);

        ImageView icon = (ImageView) view.findViewById(R.id.tabsIcon);
        icon.setImageResource(iconResource);

        return view;
    }
   
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            toggleMenu();
            return true;
        } else {
            return super.onKeyDown(keyCode, event);
        }
       
    }
   
    public static void toggleMenu() {
        if (mMenuPanel.getVisibility() == View.GONE) {
            mMenuPanel.setVisibility(View.VISIBLE);
        } else {
            mMenuPanel.setVisibility(View.GONE);
        }      
    }
}

Here is one of the example activities that are set as content for the activity tabs. I will only publish this one because they are identical.
Search.java

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
package com.androidworkz.example.custommenubar;

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.widget.TextView;

public class Search extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupViews();      
    }
   
    private void setupViews() {
        /* Search Tab Content */
        TextView textView = new TextView(this);
        textView.setText(getString(R.string.search));
        setContentView(textView);
    }
   
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            CustomMenuBar.toggleMenu();
            return true;
        } else {
            return super.onKeyDown(keyCode, event);
        }      
    }
}

The code in CustomMenuBar.java is commented and you should be able to follow what it does… I am going to concentrate on the primary subject of this tutorial which is hooking the menu button and displaying and hiding the menu panel. First, it is very easy to hook the menu button. This is accomplished by overriding the onKeyDown method of the activity and catching the KeyEvent for the menu button. If you look at the KeyEvent page in the SDK documentation you can see that there are many KeyEvent constants that allow you to hook almost any key.

1
2
3
4
5
6
7
8
9
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_MENU) {
        toggleMenu(); // show or hide the menu as required
        return true;
    } else {
        return super.onKeyDown(keyCode, event);
    }  
}

The next important part is the toggleMenu()_ method which just sets the visibility of the panel. Notice that the modifier of the method is static. This allows us to call this method from another activity. I will go into more detail below.

1
2
3
4
5
6
7
public static void toggleMenu() {
    if (mMenuPanel.getVisibility() == View.GONE) {
        mMenuPanel.setVisibility(View.VISIBLE);
    } else {
        mMenuPanel.setVisibility(View.GONE);
    }      
}

Now look at Search.java above. You will notice that it also has the Overriden onKeyDown method. This is because only the currently active Activity can catch KeyEvents. So we have to Override that method in each of the activities that we are using. The difference in these activities is that they don’t have access to the panel… only CustomMenuBar has access to it… so the workaround to be able to toggle the menu is to call the method directly in CustomMenuBar. This is the static call I mentioned above. Notice that in Search.java I am calling toggleMenu() like CustomMenuBar.toggleMenu();.

1
2
3
4
5
6
7
8
9
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_MENU) {
        CustomMenuBar.toggleMenu();
        return true;
    } else {
        return super.onKeyDown(keyCode, event);
    }      
}

I hope that this tutorial helps you design a great app. You can download a complete Eclipse Project for this tutorial here: CustomMenuBar.zip

chronix Radio

No Comments

chronix Radio has been published on the Android Market and is officially affiliated with chroniX Radio. Go get it!

chronix Radio
chroniX Radio

Lock Screen
Lock Screen Replacement

FroShedYo! 2.2 Froyo CM/AOSP Rom Released

No Comments

I just found out a few minutes ago that one of my friends released a kickass mod based on Cyanogen ROM 6. Please check it out and show him some love. :)

http://forum.xda-developers.com/showthread.php?t=824160

Beyond Smart Lists – How Observable Singletons change the game

No Comments

I wrote a tutorial about Smart Lists yesterday that showed you how to make your Lists smarter and, in the Addendum, I gave you a short example of how this design pattern can make Smart Objects that inform your activities whenever they are changed. In this tutorial, I am not going to duplicate that exactly… however, I wanted to provide a working example of just how powerful this method can be. How many times have you wanted to pass complex objects between Activities and found yourself hating Parcelables and Bundles because of their type limitations? How many times have you struggled to save complex objects during the application life cycle (orientation changes, etc). Below is an example that shows exactly how these Observable Singleton Objects can save you code, complexity and giant headaches. I have extended my previous example by duplicating the SmartAdapterExample.java, adding a separate array, and placing these two activities inside of a TabHost… so that you can see exactly how you can use this method to share objects across Activities without all of the voodoo I have seen in examples all over the web. I hope this helps you as much as it has me.

/BeyondSmartListAdapter/res/layout/tabs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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">
    <LinearLayout
       android:orientation="vertical"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:padding="5dp">
        <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"
           android:padding="5dp" />
    </LinearLayout>
</TabHost>

/BeyondSmartListAdapter/res/layout/list1.xml

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
34
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/base"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:orientation="vertical">
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="center_horizontal">
        <Button
            android:id="@+id/button"
            android:text="Add Item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
        <Button
            android:id="@+id/clear"
            android:text="Clear List"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
    </LinearLayout>
    <ListView android:id="@android:id/list"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:layout_weight="1"
       android:drawSelectorOnTop="false" />
</LinearLayout>

/BeyondSmartListAdapter/res/layout/list2.xml (NOTE: I know I could have used the same layout as above and just set the color programmatically… it’s an example… what do you want?)

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
34
35
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/base"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:orientation="vertical"
   android:background="#2f2f4f">
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="center_horizontal">
        <Button
            android:id="@+id/button"
            android:text="Add Item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
        <Button
            android:id="@+id/clear"
            android:text="Clear List"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
    </LinearLayout>
    <ListView android:id="@android:id/list"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:layout_weight="1"
       android:drawSelectorOnTop="false" />
</LinearLayout>

/BeyondSmartListAdapter/res/layout/list_item.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/text"
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="wrap_content"
   android:layout_height="40dip"
   android:textSize="18dip"
   android:textStyle="bold"
   android:textColor="#FFFFE0" />

/BeyondSmartListAdapter/src/com/androidworkz/smartlistadapter/SmartList.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package com.androidworkz.smartlistadapter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Observable;

public class SmartList extends Observable implements List<String> {

    List<String> delegate;

    public SmartList(List<String> delegate) {
        this.delegate = delegate;
    }

    private SmartList() {
        this.delegate = new ArrayList<String>();
    }

    public static synchronized SmartList getInstance() {
        if (instance == null)
            instance = new SmartList();
        return instance;
    }

    private static SmartList instance;
   
    @Override
    public String toString() {
        return delegate.toString();
    }

    @Override
    public boolean add(String song) {
        boolean result = delegate.add(song);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public void add(int position, String song) {
        delegate.add(position, song);
        setChanged();
        notifyObservers();
    }

    @Override
    public boolean addAll(Collection<? extends String> c) {
        boolean result = delegate.addAll(c);
        if(result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean addAll(int position, Collection<? extends String> c) {
        boolean result = delegate.addAll(position, c);
        if(result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public void clear() {
        delegate.clear();
        setChanged();
        notifyObservers();
    }

    @Override
    public boolean contains(Object song) {
        return delegate.contains(song);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return delegate.containsAll(c);
    }

    @Override
    public String get(int position) {
        return delegate.get(position);
    }

    @Override
    public int indexOf(Object song) {
        return delegate.indexOf(song);
    }
   
    @Override
    public boolean equals(Object song) {
        return delegate.equals(song);
    }
   
    @Override
    public int hashCode() {
        return delegate.hashCode();
    }

    @Override
    public boolean isEmpty() {
        return delegate.isEmpty();
    }

    @Override
    public Iterator<String> iterator() {
        return delegate.iterator();
    }

    @Override
    public int lastIndexOf(Object song) {
        return delegate.lastIndexOf(song);
    }

    @Override
    public ListIterator<String> listIterator() {
        return delegate.listIterator();
    }

    @Override
    public ListIterator<String> listIterator(int position) {
        return delegate.listIterator(position);
    }

    @Override
    public String remove(int position) {
        String result = delegate.remove(position);
        setChanged();
        notifyObservers();
        return result;
    }

    @Override
    public boolean remove(Object song) {
        boolean result = delegate.remove(song);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        boolean result = delegate.removeAll(c);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        boolean result = delegate.retainAll(c);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public String set(int position, String song) {
        String result = delegate.set(position, song);
        setChanged();
        notifyObservers();
        return result;
    }

    @Override
    public int size() {
        return delegate.size();
    }

    @Override
    public List<String> subList(int fromPosition, int toPosition) {
        return delegate.subList(fromPosition, toPosition);
    }

    @Override
    public Object[] toArray() {
        return delegate.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return delegate.toArray(a);
    }
}

/BeyondSmartListAdapter/src/com/androidworkz/smartlistadapter/Tabs.java

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
34
35
36
37
38
package com.androidworkz.smartlistadapter;

import android.app.TabActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TabHost;

public class Tabs extends TabActivity {

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

        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, SmartListAdapterExample.class);

        // Initialize a TabSpec for each tab and add it to the TabHost
        spec = tabHost.newTabSpec("list1").setIndicator("List 1",
                          null)
                      .setContent(intent);
        tabHost.addTab(spec);

        // Do the same for the other tabs
        intent = new Intent().setClass(this, SmartListAdapterExample2.class);
        spec = tabHost.newTabSpec("list2").setIndicator("List 2",
                          null)
                      .setContent(intent);
        tabHost.addTab(spec);

        tabHost.setCurrentTab(0);

    }
}

/BeyondSmartListAdapter/src/com/androidworkz/smartlistadapter/SmartListAdapterExample.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.androidworkz.smartlistadapter;

import java.util.Observable;
import java.util.Observer;
import java.util.Random;

import android.app.ListActivity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

public class SmartListAdapterExample extends ListActivity {

    private static final int EXIT = 0;
    private LayoutInflater inflater;
    SmartList smartList1;
    String[] colors;
    String[] cars;
    Random random;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        Resources resources = getResources();
        colors = resources.getStringArray(R.array.colors);
        cars = resources.getStringArray(R.array.cars);
       
        random = new Random();
       
        smartList1 = SmartList.getInstance();
       
        int x = 0;
        while(x < 5) {
            int rndcolor = random.nextInt(colors.length - 1);
            int rndcar = random.nextInt(cars.length - 1);
            smartList1.add(colors[rndcolor]+" "+cars[rndcar]);
            x++;
        }
       
        SmartListAdapter smartAdapter = new SmartListAdapter(this);
       
        inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.list1, (ViewGroup) findViewById(R.id.base));

        ListView list = (ListView) layout.findViewById(android.R.id.list);     
        list.setAdapter(smartAdapter);
       
        Button addItem = (Button) layout.findViewById(R.id.button);
        Button clearList = (Button) layout.findViewById(R.id.clear);
       
        addItem.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                int rndcolor = random.nextInt(colors.length - 1);
                int rndcar = random.nextInt(cars.length - 1);
                smartList1.add(colors[rndcolor]+" "+cars[rndcar]);
                Log.v("SmartList", "Count is :"+smartList1.size());
            }
           
        });
       
        clearList.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                smartList1.clear();
            }
           
        });

        setContentView(layout);
       
    }
   
    private class SmartListAdapter extends BaseAdapter implements Observer {
        private LayoutInflater mInflater;
        private SmartList smartList2;
       
        class ViewHolder {
            TextView text;
        }

        public SmartListAdapter(Context context) {
            mInflater = LayoutInflater.from(context);                  
            smartList2 = SmartList.getInstance();
            smartList2.addObserver(this);
        }

        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.list_item, null);
                holder = new ViewHolder();
                holder.text = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.text.setText(smartList2.get(position));
            return holder.text;
        }        

        public int getCount() {
            return smartList2.size();
        }

        public Object getItem(int position) {
            return smartList2.get(position);
        }

        public long getItemId(int position) {
            return position;
        }

        @Override
        public void update(Observable observable, Object object) {
            if (observable.equals(SmartList.getInstance())) {
                Log.v("SmartList.update", "Yep... it's me!");
            }
            smartList2 = SmartList.getInstance();
            notifyDataSetChanged();
            Log.v("SmartListAdapter", "Count is :"+smartList2.size());
        }
    }
   
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
       
        int NONE = Menu.NONE;
        menu.add(NONE, EXIT, 1, "Exit");
        return true;
    }
   
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case EXIT:
            quit();
            break;
        }

        return super.onOptionsItemSelected(item);
    }
   
    public void quit() {
        int pid = android.os.Process.myPid();
        android.os.Process.killProcess(pid);
        System.exit(0);
    }
}

/BeyondSmartListAdapter/src/com/androidworkz/smartlistadapter/SmartListAdapterExample2.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package com.androidworkz.smartlistadapter;

import java.util.Observable;
import java.util.Observer;
import java.util.Random;

import android.app.ListActivity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

public class SmartListAdapterExample2 extends ListActivity {

    private static final int EXIT = 0;
    private LayoutInflater inflater;
    SmartList smartList1;
    String[] houses;
    String[] materials;
    Random random;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        Resources resources = getResources();
        houses = resources.getStringArray(R.array.houses);
        materials = resources.getStringArray(R.array.materials);
       
        random = new Random();
       
        smartList1 = SmartList.getInstance();
       
        SmartListAdapter smartAdapter = new SmartListAdapter(this);
       
        inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.list2, (ViewGroup) findViewById(R.id.base));

        ListView list = (ListView) layout.findViewById(android.R.id.list);     
        list.setAdapter(smartAdapter);
       
        Button addItem = (Button) layout.findViewById(R.id.button);
        Button clearList = (Button) layout.findViewById(R.id.clear);
       
        addItem.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                int rndcolor = random.nextInt(houses.length - 1);
                int rndcar = random.nextInt(materials.length - 1);
                smartList1.add(houses[rndcolor]+" "+materials[rndcar]);
                Log.v("SmartList", "Count is :"+smartList1.size());
            }
           
        });
       
        clearList.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                smartList1.clear();
            }
           
        });

        setContentView(layout);
       
    }
   
    private class SmartListAdapter extends BaseAdapter implements Observer {
        private LayoutInflater mInflater;
        private SmartList smartList2;
       
        class ViewHolder {
            TextView text;
        }

        public SmartListAdapter(Context context) {
            mInflater = LayoutInflater.from(context);                  
            smartList2 = SmartList.getInstance();
            smartList2.addObserver(this);
        }

        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.list_item, null);
                holder = new ViewHolder();
                holder.text = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.text.setText(smartList2.get(position));
            return holder.text;
        }        

        public int getCount() {
            return smartList2.size();
        }

        public Object getItem(int position) {
            return smartList2.get(position);
        }

        public long getItemId(int position) {
            return position;
        }

        @Override
        public void update(Observable observable, Object object) {
            if (observable.equals(SmartList.getInstance())) {
                Log.v("SmartList.update", "Yep... it's me!");
            }
            smartList2 = SmartList.getInstance();
            notifyDataSetChanged();
            Log.v("SmartListAdapter", "Count is :"+smartList2.size());
        }
    }
   
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
       
        int NONE = Menu.NONE;
        menu.add(NONE, EXIT, 1, "Exit");
        return true;
    }
   
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case EXIT:
            quit();
            break;
        }

        return super.onOptionsItemSelected(item);
    }
   
    public void quit() {
        int pid = android.os.Process.myPid();
        android.os.Process.killProcess(pid);
        System.exit(0);
    }
}

Download the complete Eclipse project here

Smart List Adapter Tutorial – Singleton Oberservable List Objects and how they can improve your app

2 Comments

This tutorial is all about observation (heh… funny nerd joke there). I come from a web development background (LAMP) and really just started coding with Java at the beginning of June… So this is a new adventure for me. As I have been working on andAMP, I found myself having to write alot of extra code to manage the current play queue of songs everywhere in my app and it really started getting on my nerves because I found myself duplicating alot of code and having multiple copies of the same array in memory… I tried setting the array as a static property of my main activity and accessing it from the other parts of my app statically (like using a class with getters and setters)… but that caused logic and stability issues due to the life cycle of the activity and just wasn’t very elegant… sloppy to say the least and when I attempted to add a widget to the app… well… let’s not go there.

So, anyway, I started looking for a method to create a smarter list. There were a few different issues that really bothered me about how I was managing the play queue. First, I had multiple copies of the ArrayList in my app… For example, I have 2 Activities and a Service that use this particular list and I found that it added a TON of complexity to my app with every new copy I made of the list. In addition, I have to make sure I have a fresh copy of the current list anytime I need to use it so that I don’t use an old list… so I had all of this code to double check that everyone had the same copy of the current list. On top of that, anytime I make a change to the list, I need to make sure that my ListAdapter has the correct version of the current list… or it will show an old list of songs to the user when they want to view the play queue…. so, anyway, I found myself writing alot of stupid code to manage a simple list of Song objects. I just knew there was a better way to do it and this tutorial will show you what I discovered.

First, I decided to make a custom list object that was a Singleton Class so that I didn’t have multiple copies of the same list in memory AND this would ensure that all copies of the list were the same. In addition, I wanted the list to be thread safe because I might have the Service updating the list at the same time as an activity… and that would cause problems… so I read up on synchronization. I am still not 100% sure that how I did it is the correct way or not… but it works so I am not complaining.

Secondly, I wanted to make the list intelligent enough to inform it’s consumers when and if a change had been made to the list. This would save me alot of extra headache in manually telling the ListAdapter that the list had changed or telling the Service that the list had changed, etc. At first, I thought maybe I could attach some kind of listener to the list… and I was close. In the end I discovered Observable and Observer. These 2 classes allow me to provide a method for the consumer objects to watch the list and be notified whenever there is a change… and I think they are the best thing to happen to Lists since add() (lol… another nerd joke). All jokes aside, these discoveries gave my lists super powers and are on the way to super charging my applications. Below is the source code for you to look at… let me know what you think. I haven’t seen any Android specific tutorials about how to do this… if you know of one, please let me know so I can link it.

/res/layout/main.xml

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
34
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/base"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:orientation="vertical">
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="center_horizontal">
        <Button
            android:id="@+id/button"
            android:text="Add Item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
        <Button
            android:id="@+id/clear"
            android:text="Clear List"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:gravity="center">
        </Button>
    </LinearLayout>
    <ListView android:id="@android:id/list"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:layout_weight="1"
       android:drawSelectorOnTop="false" />
</LinearLayout>

/res/layout/list_item.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/text"
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="wrap_content"
   android:layout_height="40dip"
   android:textSize="18dip"
   android:textStyle="bold"
   android:textColor="#FFFFE0" />

/res/values/array.xml

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="colors">
<item>red</item>
<item>blue</item>
<item>green</item>
<item>orange</item>
<item>purple</item>
<item>yellow</item>
<item>white</item>
<item>black</item>
<item>gold</item>
<item>silver</item>
</string-array>
<string-array name="cars">
<item>chevy</item>
<item>ford</item>
<item>toyota</item>
<item>nissan</item>
<item>volkswagon</item>
<item>bmw</item>
<item>buick</item>
<item>mercury</item>
<item>pontiac</item>
<item>honda</item>
</string-array>
</resources>

/SmartListAdapter/src/com/androidworkz/smartlistadapter/SmartListAdapterExample.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package com.androidworkz.smartlistadapter;

import java.util.Observable;
import java.util.Observer;
import java.util.Random;

import android.app.ListActivity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

public class SmartListAdapterExample extends ListActivity {

    private static final int EXIT = 0;
    private LayoutInflater inflater;
    SmartList smartList1;
    String[] colors;
    String[] cars;
    Random random;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        Resources resources = getResources();
        colors = resources.getStringArray(R.array.colors);
        cars = resources.getStringArray(R.array.cars);
       
        random = new Random();
       
        smartList1 = SmartList.getInstance();
       
        int x = 0;
        while(x < 5) {
            int rndcolor = random.nextInt(colors.length - 1);
            int rndcar = random.nextInt(cars.length - 1);
            smartList1.add(colors[rndcolor]+" "+cars[rndcar]);
            x++;
        }
       
        SmartListAdapter smartAdapter = new SmartListAdapter(this);
       
        inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.main, (ViewGroup) findViewById(R.id.base));

        ListView list = (ListView) layout.findViewById(android.R.id.list);     
        list.setAdapter(smartAdapter);
       
        Button addItem = (Button) layout.findViewById(R.id.button);
        Button clearList = (Button) layout.findViewById(R.id.clear);
       
        addItem.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                int rndcolor = random.nextInt(colors.length - 1);
                int rndcar = random.nextInt(cars.length - 1);
                smartList1.add(colors[rndcolor]+" "+cars[rndcar]);
                Log.v("SmartList", "Count is :"+smartList1.size());
            }
           
        });
       
        clearList.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                smartList1.clear();
            }
           
        });

        setContentView(layout);
       
    }
   
    private class SmartListAdapter extends BaseAdapter implements Observer {
        private LayoutInflater mInflater;
        private SmartList smartList2;
       
        class ViewHolder {
            TextView text;
        }

        public SmartListAdapter(Context context) {
            mInflater = LayoutInflater.from(context);                  
            smartList2 = SmartList.getInstance();
            smartList2.addObserver(this);
        }

        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.list_item, null);
                holder = new ViewHolder();
                holder.text = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.text.setText(smartList2.get(position));
            return holder.text;
        }        

        public int getCount() {
            return smartList2.size();
        }

        public Object getItem(int position) {
            return smartList2.get(position);
        }

        public long getItemId(int position) {
            return position;
        }

        @Override
        public void update(Observable arg0, Object arg1) {
            smartList2 = SmartList.getInstance();
            notifyDataSetChanged();
            Log.v("SmartListAdapter", "Count is :"+smartList2.size());
        }
    }
   
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
       
        int NONE = Menu.NONE;
        menu.add(NONE, EXIT, 1, "Exit");
        return true;
    }
   
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case EXIT:
            quit();
            break;
        }

        return super.onOptionsItemSelected(item);
    }
   
    public void quit() {
       // if the Android engineers see this their heads will explode
        int pid = android.os.Process.myPid();
        android.os.Process.killProcess(pid);
        System.exit(0);
        finish()
    }
}

/SmartListAdapter/src/com/androidworkz/smartlistadapter/SmartList.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package com.androidworkz.smartlistadapter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Observable;

public class SmartList extends Observable implements List<String> {

    List<String> delegate;

    public SmartList(List<String> delegate) {
        this.delegate = delegate;
    }

    private SmartList() {
        this.delegate = new ArrayList<String>();
    }

    public static synchronized SmartList getInstance() {
        if (instance == null)
            instance = new SmartList();
        return instance;
    }

    private static SmartList instance;
   
    @Override
    public String toString() {
        return delegate.toString();
    }

    @Override
    public boolean add(String song) {
        boolean result = delegate.add(song);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public void add(int position, String song) {
        delegate.add(position, song);
        setChanged();
        notifyObservers();
    }

    @Override
    public boolean addAll(Collection<? extends String> c) {
        boolean result = delegate.addAll(c);
        if(result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean addAll(int position, Collection<? extends String> c) {
        boolean result = delegate.addAll(position, c);
        if(result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public void clear() {
        delegate.clear();
        setChanged();
        notifyObservers();
    }

    @Override
    public boolean contains(Object song) {
        return delegate.contains(song);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return delegate.containsAll(c);
    }

    @Override
    public String get(int position) {
        return delegate.get(position);
    }

    @Override
    public int indexOf(Object song) {
        return delegate.indexOf(song);
    }
   
    @Override
    public boolean equals(Object song) {
        return delegate.equals(song);
    }
   
    @Override
    public int hashCode() {
        return delegate.hashCode();
    }

    @Override
    public boolean isEmpty() {
        return delegate.isEmpty();
    }

    @Override
    public Iterator<String> iterator() {
        return delegate.iterator();
    }

    @Override
    public int lastIndexOf(Object song) {
        return delegate.lastIndexOf(song);
    }

    @Override
    public ListIterator<String> listIterator() {
        return delegate.listIterator();
    }

    @Override
    public ListIterator<String> listIterator(int position) {
        return delegate.listIterator(position);
    }

    @Override
    public String remove(int position) {
        String result = delegate.remove(position);
        setChanged();
        notifyObservers();
        return result;
    }

    @Override
    public boolean remove(Object song) {
        boolean result = delegate.remove(song);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        boolean result = delegate.removeAll(c);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        boolean result = delegate.retainAll(c);
        if (result) {
            setChanged();
        }
        notifyObservers();
        return result;
    }

    @Override
    public String set(int position, String song) {
        String result = delegate.set(position, song);
        setChanged();
        notifyObservers();
        return result;
    }

    @Override
    public int size() {
        return delegate.size();
    }

    @Override
    public List<String> subList(int fromPosition, int toPosition) {
        return delegate.subList(fromPosition, toPosition);
    }

    @Override
    public Object[] toArray() {
        return delegate.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return delegate.toArray(a);
    }
}

Addendum:

I wanted to note that your Activity can implement Observer as well so that you can use Observable with POJO (Plain Old Java Objects). For example, You can create a Singleton class that contains your application state and variables (I haven’t tried storing objects in one of these but it’s great for basic types (Boolean, Integer, String, Long, etc)). If your Activity is observing multiple objects… this is how you capture which object is sending the update notification:

Here is an actual work in progress for my ApplicationState object

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.androidworkz.example;

import java.util.Observable;

public class ApplicationState extends Observable {
   
    private boolean PartyShuffleMode = false;
    private boolean QueueMode = false;
    private boolean ShuffleMode = false;
    private boolean ContinuousMode = false;

    private ApplicationState() {}

    public static synchronized ApplicationState getInstance() {
        if (instance == null)
            instance = new ApplicationState();
        return instance;
    }

    public void setPartyShuffleMode(boolean partyShuffleMode) {
        PartyShuffleMode = partyShuffleMode;
        setChanged();
        notifyObservers();
    }

    public boolean isPartyShuffleMode() {
        return PartyShuffleMode;
    }

    public void setQueueMode(boolean queueMode) {
        QueueMode = queueMode;
        setChanged();
        notifyObservers();
    }

    public boolean isQueueMode() {
        return QueueMode;
    }

    public void setShuffleMode(boolean shuffleMode) {
        ShuffleMode = shuffleMode;
        setChanged();
        notifyObservers();
    }

    public boolean isShuffleMode() {
        return ShuffleMode;
    }

    public void setContinuousMode(boolean continuousMode) {
        ContinuousMode = continuousMode;
        setChanged();
        notifyObservers();
    }

    public boolean isContinuousMode() {
        return ContinuousMode;
    }

    private static ApplicationState instance;
}

Let’s say you are observing SmartList as well as your ApplicationState object. Below you can see how to instantiate the Singleton objects and add the Observer. in the update method for the Observer you compare the Observable argument (observable) with the Singleton to see if that was the object that was updated. I found that the Object argument was null everytime I tried to do something with it… so I am not sure what it’s purpose really is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyActivity extends Activity implements Observer {
    public void onCreate() {
        ApplicationState state = ApplcationState.getInstance();
        state.addObserver(this);
        SmartList smartList = SmartList.getInstance();
        smartList.addObserver(this);
        /* ....... the rest of your onCreate() */
    }

    @Override
    public void update(Observable observable, Object object) {
        if (observable.equals(SmartList.getInstance())) {
            smartList = SmartList.getInstance();
        }
        if (observable.equals(ApplicationState.getInstance())) {
            state = ApplicationState.getInstance();
        }
    }
}

Download Complete Eclipse Project

A couple of cool tutorial sites I have found

No Comments

Everyone needs tutorials… I don’t care who it is. Here are a few of the best tutorial sites I have found for Android. These sites have stood out among the rest for some of the best android development tutorials available and lots of them. I hope this list helps you.

Special Mention -
While this next site isn’t a free tutorial site… I highly recommend it. I have learned an incredible amount about Android Development from Mark Murphy at Commonsware. He is well known for being the author of Beginning Android 2 and other books. I bought Beginning Android 2 when I first got started and read the whole book in a day. That alone says something… who can read a development book in a day? Well… Mark makes the book so easy to understand that you can do it. In addition, you can get that same book under a different title at his website CommonsWare.com and 2 others for the same price you would pay for one.

three awesome android development books for the price of one

HIGHLY RECOMMENDED! Get a subscription at CommonsWare for only $40 and receive book updates and new titles for a full year. This means that whenever there is a new version of the book you get it for free. (2nd Edition, 3rd Edition, etc)

Get Help -
Lastly, I can’t say enough about StackOverflow as a resource for help. If you have a question or a problem with your application that is just driving you nuts… you can ask at StackOverflow and get an answer in a short period of time. There is a very active community of developers who answer questions. Just make sure you give credit where credit is due! Vote up and accept answers!

Backward compatible killbackgroundProcesses

No Comments

Here is the solution I came up with for Shouvik.

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
34
35
36
37
38
39
40
41
/* onCreate() */
final Button killbutton = (Button) findViewById(R.id.killbutton);
killbutton.setOnClickListener(new View.OnClickListener() {
     public void onClick(View v) {
         if (this.killMethod != null) {
            try {
                this.killMethod.invoke(this.activityManager, packageName);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
     }
 });
 /* snip */

 /* method in activity */
private void initializeKillMethod() {
    try {
            this.killMethod = ActivityManager.class.getMethod("killBackgroundProcesses", String.class);
    } catch (SecurityException e) {
            e.printStackTrace();
    } catch (NoSuchMethodException e) {
            e.printStackTrace();
    }

    if (this.killMethod != null) {
            return;
    }

    try {
            this.killMethod = ActivityManager.class.getMethod("restartPackage", String.class);
    } catch (SecurityException e) {
            e.printStackTrace();
    } catch (NoSuchMethodException e) {
            e.printStackTrace();
    }
}

Source Code: Find files by type + play media file

No Comments

I was perusing StackOverflow for Android items and came across this question Getting the Contents of a Specific Directory on the SDCard. I love StackOverflow… but for some reason I can’t post xml code examples and the code field doesn’t always work currently so you end up with code outside of the code box…. So, anyway, I decided to make an example for Edward and post it here so that everything would be formatted correctly.

This examples focus is the FileUtils class that I created for the purpose of finding files recursively on the SD Card. Here is the 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
34
35
36
37
38
public class FileUtils {

    public File[] listFilesAsArray(File directory, FilenameFilter[] filter,
            int recurse) {
        Collection<File> files = listFiles(directory, filter, recurse);

        File[] arr = new File[files.size()];
        return files.toArray(arr);
    }

    public Collection<File> listFiles(File directory,
            FilenameFilter[] filter, int recurse) {

        Vector<File> files = new Vector<File>();

        File[] entries = directory.listFiles();

        if (entries != null) {
            for (File entry : entries) {
                for (FilenameFilter filefilter : filter) {
                    if (filter == null
                            || filefilter
                                    .accept(directory, entry.getName())) {
                        files.add(entry);
                        Log.v("FileUtils", "Added: "
                                + entry.getName());
                    }
                }
                if ((recurse <= -1) || (recurse > 0 && entry.isDirectory())) {
                    recurse--;
                    files.addAll(listFiles(entry, filter, recurse));
                    recurse++;
                }
            }
        }
        return files;
    }
}

Edward needs to find files by type (specifically ringtones) from a path… either on the phone (as in the following example) or on the SD Card. He could use the MediaStore.Audio.Media Content Provider to accomplish this task… however, it would give him all of the audio media on the phone. It is sometimes necessary to list files by a particular mimetype without using the Content Providers. In my example, this is accomplished by using a list of file extensions and searching the desired path for files that match those file extensions. There are two ways you could go about it… you could make a String[] array of the extensions or you could use the Resources array as I have done. Below is the xml for the mimetypes we are looking for.

/res/values/mimetypes.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="audio">
<item>aif</item>
<item>aifc</item>
<item>aiff</item>
<item>au</item>
<item>mid</item>
<item>mp3</item>
<item>ogg</item>
<item>snd</item>
<item>wav</item>
</string-array>
</resources>

Using a Resources string array is very convenient and provides a way for you to store arrays in files so that you can use them on demand rather than keeping them in memory. They are very simple to use. In my example you can see how they are used in the FindFiles function as seen here:

1
2
3
4
Resources resources = getResources();
// array of valid audio file extensions
String[] audioTypes = resources.getStringArray(R.array.audio);
FilenameFilter[] filter = new FilenameFilter[audioTypes.length];

Of particular interest in the above example is the FilenameFilter object. This allows us to define an array of file extension filters that works very quickly and only returns the files we want.

Here is the example application that I created for Edward. It lists all of the audio files in /system/media/audio/ringtones/ and displays them in a ListView. When you click a particular file in the ListView it starts playing. There is a menu item to stop the ringtone from playing (there seems to be an issue with MediaPlayer.setLooping(false) when dealing with ogg files) and to quit the application. The complete Eclipse project is available for download at the bottom of the post.

/res/layout/main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">
    <ListView android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:drawSelectorOnTop="false" />
    <TextView android:id="@android:id/empty"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:text="No ringtones found." />
</LinearLayout>

/res/layout/list_item.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/ringtone"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="40dip"
    android:textSize="18dip"
    android:textStyle="bold"
    android:textColor="#FFFFE0" />


/src/com/androidworks/findfiles/FindFilesByType.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package com.androidworkz.findfiles;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.res.Resources;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener;

public class FindFilesByType extends ListActivity {

    private static final int EXIT = 0;
    private static final int STOP = 1;
    private static final String DIRECTORY = "/system/media/audio/ringtones/";
    private MediaPlayer mp = new MediaPlayer();
    List<String> Ringtones = new ArrayList<String>();
    Boolean hasErrors = false;
    int currentPosition = 0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ListView lv = getListView();
        File ringtones_directory = new File(DIRECTORY);
        if (!ringtones_directory.exists()) {
            AlertDialog.Builder ad = new AlertDialog.Builder(
                    FindFilesByType.this);
            ad.setTitle("Directory Not Found");
            ad.setMessage("Sorry! The ringtones directory doesn't exist.");
            ad.setPositiveButton("OK", null);
            ad.show();
            hasErrors = true;
        }
       
        if (!ringtones_directory.canRead()) {
            AlertDialog.Builder ad = new AlertDialog.Builder(
                    FindFilesByType.this);
            ad.setTitle("Permissions");
            ad.setMessage("Sorry! You don't have permission to list the files in that folder");
            ad.setPositiveButton("OK", null);
            ad.show();
            hasErrors = true;
        }
        else {
            Ringtones = FindFiles(false);

            if (Ringtones.size() < 1) {
                AlertDialog.Builder ad = new AlertDialog.Builder(
                        FindFilesByType.this);
                ad.setTitle("Permissions");
                ad.setMessage("Sorry! No ringtones exists in " + DIRECTORY + ".");
                ad.setPositiveButton("OK", null);
                ad.show();
                Log.e("Ringtones", "No ringtones were found.");
                hasErrors = true;
            }
        }      
       
        if (!hasErrors) {
            setListAdapter(new ArrayAdapter<String>(FindFilesByType.this, R.layout.list_item,
                    Ringtones));
           
            lv.setOnItemClickListener(new OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> aView, View v,
                        int position, long id) {
                    currentPosition = position;
                    playRingtone(DIRECTORY+Ringtones.get(position));
                }
            });
        }
    }
   
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
       
        int NONE = Menu.NONE;
        if (mp.isPlaying()) {
            menu.add(NONE, STOP, 0, "Stop");
        }
        menu.add(NONE, EXIT, 1, "Exit");
        return true;
    }
   
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case STOP:
            mp.stop();
            break;
        case EXIT:
            quit();
            break;
        }

        return super.onOptionsItemSelected(item);
    }

    private void playRingtone(String ringtone) {
        try {
            mp.reset();
            mp.setDataSource(ringtone);
            mp.prepare();
            mp.start();
        } catch (IOException e) {
            Log.v("Ringtone", e.getMessage());
        }
    }

    private List<String> FindFiles(Boolean fullPath) {
        final List<String> tFileList = new ArrayList<String>();
        Resources resources = getResources();
        // array of valid audio file extensions
        String[] audioTypes = resources.getStringArray(R.array.audio);
        FilenameFilter[] filter = new FilenameFilter[audioTypes.length];

        int i = 0;
        for (final String type : audioTypes) {
            filter[i] = new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    return name.endsWith("." + type);
                }
            };
            i++;
        }

        FileUtils fileUtils = new FileUtils();
        File[] allMatchingFiles = fileUtils.listFilesAsArray(
                new File(DIRECTORY), filter, -1);
        for (File f : allMatchingFiles) {
            if (fullPath) {
                tFileList.add(f.getAbsolutePath());
            }
            else {
                tFileList.add(f.getName());
            }
        }
        return tFileList;
    }

    public class FileUtils {

        public void saveArray(String filename, List<String> output_field) {
            try {
                FileOutputStream fos = new FileOutputStream(filename);
                GZIPOutputStream gzos = new GZIPOutputStream(fos);
                ObjectOutputStream out = new ObjectOutputStream(gzos);
                out.writeObject(output_field);
                out.flush();
                out.close();
            } catch (IOException e) {
                e.getStackTrace();
            }
        }

        @SuppressWarnings("unchecked")
        public List<String> loadArray(String filename) {
            try {
                FileInputStream fis = new FileInputStream(filename);
                GZIPInputStream gzis = new GZIPInputStream(fis);
                ObjectInputStream in = new ObjectInputStream(gzis);
                List<String> read_field = (List<String>) in.readObject();
                in.close();
                return read_field;
            } catch (Exception e) {
                e.getStackTrace();
            }
            return null;
        }

        public File[] listFilesAsArray(File directory, FilenameFilter[] filter,
                int recurse) {
            Collection<File> files = listFiles(directory, filter, recurse);

            File[] arr = new File[files.size()];
            return files.toArray(arr);
        }

        public Collection<File> listFiles(File directory,
                FilenameFilter[] filter, int recurse) {

            Vector<File> files = new Vector<File>();

            File[] entries = directory.listFiles();

            if (entries != null) {
                for (File entry : entries) {
                    for (FilenameFilter filefilter : filter) {
                        if (filter == null
                                || filefilter
                                        .accept(directory, entry.getName())) {
                            files.add(entry);
                            Log.v("FileUtils", "Added: "
                                    + entry.getName());
                        }
                    }
                    if ((recurse <= -1) || (recurse > 0 && entry.isDirectory())) {
                        recurse--;
                        files.addAll(listFiles(entry, filter, recurse));
                        recurse++;
                    }
                }
            }
            return files;
        }
    }
   
    public void quit() {
        int pid = android.os.Process.myPid();
        android.os.Process.killProcess(pid);
        System.exit(0);
    }
}

Here is a screenshot:

You can download the Eclipse Project Here: Download FindFilesByType.zip

Source Code: ImageView Flipper + SD Card Scanner

No Comments

This is a little project I have been working on that is based on code that I found at codeshogun.com for working with the ViewFlipper. If you are looking for a sample project that incorporates the ViewFlipper to scroll through images or if you are looking for a sample project for how to recursively search the SD Card for files based on type (or any other type of file searching for that matter) than this project might help. This is open source with no license whatsoever… so you can use it however you like. This project also features something I haven’t seen in other projects and that is how to save an array object to a file and read it back in. It works very quickly and it is a fast way to save data instances without messing with a database or serialization. The reason I used it was to save the data so that the application wouldn’t attempt to rescan the SD Card whenever the screen orientation changed (something I hate about Android). This also shows you how to save variable state using SharedPreferences. You can download a zip of the Eclipse project at the bottom of the post.

The layout – /res/layout/main.xml

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
34
35
36
37
38
39
40
41
42
43
44
45
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/flipper"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
       
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center">      
        <ImageView
            android:id="@+id/zero"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerInside"
            android:src="@drawable/wait"
        />
    </LinearLayout>
   
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center">
        <ImageView
            android:id="@+id/one"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerInside"
            android:src="@drawable/wait"
        />
    </LinearLayout>
   
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center">
        <ImageView
            android:id="@+id/two"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerInside"
            android:src="@drawable/wait"
        />
    </LinearLayout>
   
</ViewFlipper>

A string array /res/values/mimetypes.xml

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="image">
<item>bmp</item>
<item>cmx</item>
<item>cod</item>
<item>gif</item>
<item>ico</item>
<item>ief</item>
<item>jpe</item>
<item>jpeg</item>
<item>jpg</item>
<item>jfif</item>
<item>pbm</item>
<item>pgm</item>
<item>png</item>
<item>pnm</item>
<item>ppm</item>
<item>ras</item>
<item>rgb</item>
<item>svg</item>
<item>tif</item>
<item>tiff</item>
<item>xbm</item>
<item>xpm</item>
<item>xwd</item>
</string-array>
</resources>
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
package com.androidworkz.imageviewflipper;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.Toast;
import android.widget.ViewFlipper;

public class ImageViewFlipper extends Activity {
   
    private static final int EXIT = 0;
    private static final int SWIPE_MIN_DISTANCE = 120;
    private static final int SWIPE_MAX_OFF_PATH = 250;
    private static final int SWIPE_THRESHOLD_VELOCITY = 200;
    private static final String DIRECTORY = "/sdcard/";
    private static final String DATA_DIRECTORY = "/sdcard/.ImageViewFlipper/";
    private static final String DATA_FILE = "/sdcard/.ImageViewFlipper/imagelist.dat";
    private GestureDetector gestureDetector;
    View.OnTouchListener gestureListener;
    private Animation slideLeftIn;
    private Animation slideLeftOut;
    private Animation slideRightIn;
    private Animation slideRightOut;
    private ViewFlipper viewFlipper;
    private int currentView = 0;
    List<String> ImageList;
    private int currentIndex = 0;
    private int maxIndex = 0;

    FileOutputStream output = null;
    OutputStreamWriter writer = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
       
        setContentView(R.layout.main);
        ImageView iv = (ImageView) findViewById(R.id.zero);

        File data_directory = new File(DATA_DIRECTORY);
        if (!data_directory.exists()) {
            if (data_directory.mkdir()) {
                FileUtils savedata = new FileUtils();
                Toast toast = Toast.makeText(ImageViewFlipper.this,
                        "Please wait while we search your SD Card for images...", Toast.LENGTH_SHORT);
                toast.show();
                SystemClock.sleep(100);
                ImageList = FindFiles();
                savedata.saveArray(DATA_FILE, ImageList);
               
            } else {
                ImageList = FindFiles();
            }

        }
        else {
            File data_file= new File(DATA_FILE);
            if (!data_file.exists()) {
                FileUtils savedata = new FileUtils();
                Toast toast = Toast.makeText(ImageViewFlipper.this,
                        "Please wait while we search your SD Card for images...", Toast.LENGTH_SHORT);
                toast.show();
                SystemClock.sleep(100);
                ImageList = FindFiles();
                savedata.saveArray(DATA_FILE, ImageList);
            } else {
                FileUtils readdata = new FileUtils();
                ImageList = readdata.loadArray(DATA_FILE);
            }
        }
       
        if (ImageList == null) {
            quit();
        }
       
        SharedPreferences indexPrefs = getSharedPreferences("currentIndex",
                MODE_PRIVATE);
        if (indexPrefs.contains("currentIndex")) {
            currentIndex = indexPrefs.getInt("currentIndex", 0);
        }
       
        maxIndex = ImageList.size() - 1;
       
        Log.v("currentIndex", "Index: "+currentIndex);

        viewFlipper = (ViewFlipper) findViewById(R.id.flipper);

        slideLeftIn = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
        slideLeftOut = AnimationUtils
                .loadAnimation(this, R.anim.slide_left_out);
        slideRightIn = AnimationUtils
                .loadAnimation(this, R.anim.slide_right_in);
        slideRightOut = AnimationUtils.loadAnimation(this,
                R.anim.slide_right_out);

        viewFlipper.setInAnimation(AnimationUtils.loadAnimation(this,
                android.R.anim.fade_in));
        viewFlipper.setOutAnimation(AnimationUtils.loadAnimation(this,
                android.R.anim.fade_out));
       
        iv.setImageDrawable(Drawable.createFromPath(ImageList
                .get(currentIndex)));
        System.gc();
       
        gestureDetector = new GestureDetector(new MyGestureDetector());
        gestureListener = new View.OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
                if (gestureDetector.onTouchEvent(event)) {
                    return true;
                }
                return false;
            }
        };

    }
   
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
       
        int NONE = Menu.NONE;
        menu.add(NONE, EXIT, NONE, "Exit");
        return true;
    }
   
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case EXIT:
            quit();
            break;
        }

        return super.onOptionsItemSelected(item);
    }
   
    protected void onPause() {
        super.onPause();

        SharedPreferences indexPrefs = getSharedPreferences("currentIndex",
                MODE_PRIVATE);
       
        SharedPreferences.Editor indexEditor = indexPrefs.edit();
        indexEditor.putInt("currentIndex", currentIndex);
        indexEditor.commit();
    }
   
    protected void onResume() {
        super.onResume();
        SharedPreferences indexPrefs = getSharedPreferences("currentIndex",
                MODE_PRIVATE);
        if (indexPrefs.contains("currentIndex")) {
            currentIndex = indexPrefs.getInt("currentIndex", 0);
        }  
    }

    private List<String> FindFiles() {
        final List<String> tFileList = new ArrayList<String>();
        Resources resources = getResources();
        // array of valid image file extensions
        String[] imageTypes = resources.getStringArray(R.array.image);
        FilenameFilter[] filter = new FilenameFilter[imageTypes.length];

        int i = 0;
        for (final String type : imageTypes) {
            filter[i] = new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    return name.endsWith("." + type);
                }
            };
            i++;
        }

        FileUtils fileUtils = new FileUtils();
        File[] allMatchingFiles = fileUtils.listFilesAsArray(
                new File(DIRECTORY), filter, -1);
        for (File f : allMatchingFiles) {
            tFileList.add(f.getAbsolutePath());
        }
        return tFileList;
    }

    public class FileUtils {
       
        public void saveArray(String filename, List<String> output_field) {
             try {
                FileOutputStream fos = new FileOutputStream(filename);
                GZIPOutputStream gzos = new GZIPOutputStream(fos);
                ObjectOutputStream out = new ObjectOutputStream(gzos);
                out.writeObject(output_field);
                out.flush();
                out.close();
             }
             catch (IOException e) {
                 e.getStackTrace();
             }
          }

          @SuppressWarnings("unchecked")
        public List<String> loadArray(String filename) {
              try {
                FileInputStream fis = new FileInputStream(filename);
                GZIPInputStream gzis = new GZIPInputStream(fis);
                ObjectInputStream in = new ObjectInputStream(gzis);
                List<String> read_field = (List<String>)in.readObject();
                in.close();
                return read_field;
              }
              catch (Exception e) {
                  e.getStackTrace();
              }
              return null;
          }
         
        public File[] listFilesAsArray(File directory, FilenameFilter[] filter,
                int recurse) {
            Collection<File> files = listFiles(directory, filter, recurse);

            File[] arr = new File[files.size()];
            return files.toArray(arr);
        }

        public Collection<File> listFiles(File directory,
                FilenameFilter[] filter, int recurse) {

            Vector<File> files = new Vector<File>();

            File[] entries = directory.listFiles();

            if (entries != null) {
                for (File entry : entries) {
                    for (FilenameFilter filefilter : filter) {
                        if (filter == null
                                || filefilter
                                        .accept(directory, entry.getName())) {
                            files.add(entry);
                            Log.v("ImageViewFlipper", "Added: "
                                    + entry.getName());
                        }
                    }
                    if ((recurse <= -1) || (recurse > 0 && entry.isDirectory())) {
                        recurse--;
                        files.addAll(listFiles(entry, filter, recurse));
                        recurse++;
                    }
                }
            }
            return files;
        }
    }

    class MyGestureDetector extends SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            try {
                if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)
                    return false;
                // right to left swipe
                if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE
                        && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    viewFlipper.setInAnimation(slideLeftIn);
                    viewFlipper.setOutAnimation(slideLeftOut);

                    if (currentIndex == maxIndex) {
                        currentIndex = 0;
                    } else {
                        currentIndex = currentIndex + 1;
                    }
                    if (currentView == 0) {
                        currentView = 1;
                        ImageView iv = (ImageView) findViewById(R.id.one);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                        System.gc();
                    } else if (currentView == 1) {
                        currentView = 2;
                        ImageView iv = (ImageView) findViewById(R.id.two);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                        System.gc();
                    } else {
                        currentView = 0;
                        ImageView iv = (ImageView) findViewById(R.id.zero);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                        System.gc();
                    }
                    Log.v("ImageViewFlipper", "Current View: " + currentView);
                    viewFlipper.showNext();
                } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE
                        && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    viewFlipper.setInAnimation(slideRightIn);
                    viewFlipper.setOutAnimation(slideRightOut);
                    if (currentIndex == 0) {
                        currentIndex = maxIndex;
                    } else {
                        currentIndex = currentIndex - 1;
                    }
                    if (currentView == 0) {
                        currentView = 2;
                        ImageView iv = (ImageView) findViewById(R.id.two);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                    } else if (currentView == 2) {
                        currentView = 1;
                        ImageView iv = (ImageView) findViewById(R.id.one);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                    } else {
                        currentView = 0;
                        ImageView iv = (ImageView) findViewById(R.id.zero);
                        iv.setImageDrawable(Drawable.createFromPath(ImageList
                                .get(currentIndex)));
                    }
                    Log.v("ImageViewFlipper", "Current View: " + currentView);
                    viewFlipper.showPrevious();
                }
            } catch (Exception e) {
                // nothing
            }
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (gestureDetector.onTouchEvent(event))
            return true;
        else
            return false;
    }
   
    public void quit() {
        SharedPreferences indexPrefs = getSharedPreferences("currentIndex",
                MODE_PRIVATE);
       
        SharedPreferences.Editor indexEditor = indexPrefs.edit();
        indexEditor.putInt("currentIndex", 0);
        indexEditor.commit();
       
        File settings = new File(DATA_FILE);
        settings.delete();
        finish();
        int pid = android.os.Process.myPid();
        android.os.Process.killProcess(pid);
        System.exit(0);
    }
}

You can download the Eclipse Project Here: Download ImageViewFlipper.zip

Developers: Use System Explorer to select directories or files

No Comments

I have added PICK_FILE and PICK_DIRECTORY intents to System Explorer so that developers can use it to select files or directories. You can also pass the file name or directory to open and System Explorer will start there.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 /**
 * Opens System Explorer to open a file.
 */

private static final int REQUEST_RESULT = 1;

private void openFile(String fileName) {   
    Intent intent = new Intent("com.androidworkz.action.PICK_FILE");
   
    // Construct URI from file name.
    intent.setData(Uri.parse("file://" + fileName));
    intent.putExtra("com.androidworkz.extra.TITLE", "Select a file to open");
    intent.putExtra("com.androidworkz.extra.BUTTON_TEXT", "Open");
   
    try {
        startActivityForResult(intent, REQUEST_RESULT);
    } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        Toast.makeText(this, "System Explorer is not installed", Toast.LENGTH_SHORT).show();
    }
}

/**
 * Opens System Explorer to open a directory.
 */

private void openDirectory(String directoryName) { 
    Intent intent = new Intent("com.androidworkz.action.PICK_DIRECTORY");
   
    // Construct URI from file name.
    intent.setData(Uri.parse("file://" + directoryName));
   
    // Set fancy title and button (optional)
    intent.putExtra("com.androidworkz.extra.TITLE", getString(R.string.pick_directory_title));
    intent.putExtra("com.androidworkz.extra.BUTTON_TEXT", getString(R.string.pick_directory_button));
   
    try {
        startActivityForResult(intent, REQUEST_RESULT);
    } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        Toast.makeText(this, "System Explorer is not installed", Toast.LENGTH_SHORT).show();
    }
}

/**
 * This is called when the user has selected the file or directory and clicked a button to return to your application.
 */

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    super.onActivityResult(requestCode, resultCode, intent);

    switch (requestCode) {
    case REQUEST_RESULT:
        if (resultCode == RESULT_OK && intent != null) {
            // obtain the filename
            String filename = intent.getDataString();
            if (filename != null) {
                // Get rid of URI prefix:
                if (filename.startsWith("file://")) {
                    filename = filename.substring(7);
                }
                // Do something with the file here
            }      
        }
        break;
    }
}