fredag 21 december 2012

Android : IntelliJ IDEA 12 released!

Wow ! Finally a built-in dark UI for my favorite IDE and also a pretty extensive GUI editor for the XML layout.


Still prefer to do it the 'manual way' through XML, more control and feels kinda clunky to drag them to the correct place with the layout attributes i want, but it's a nice addition and you can quickly see the properties of each layout item. It auto-completes in xml editing mode but sometimes my mind doesn't work as i want it to, i.e i forget what i'm looking for :) Preview mode has alot of options where you can configure screensizes, Android version and themes, it's also available when typing manually.
Easily compile directly from the IDE, including signing and obfuscation with Proguard, Logcat is incorporated for easy access. Rest of the external tools is available from the toolbar.

Run configurations:


The IDE feels alot more intelligent than Eclipse. Codecompletion, imports, refactoring and the UI.
Best thing for me is their new sleek 'Darcula' theme as they aptly name it. Soothing for the eyes!

Check out http://intellij.com/idea/whatsnew/ for more information, and best of all they have a free Community Edition to play with! Eclipse users, give it a shot! : )

torsdag 22 november 2012

Android : Creating a custom View (a Circle!)

I've played around with building custom views that can be embedded and styled through XML.
Google has a pretty good tutorial on making them here.

First the requirements, i didn't want to create the View by code and then attach it to a view. Not reusable. I wanted to just be able to type the classname in my layout file and style it as necessary.

First our custom styleable attributes.

values/attrs.xml
<resources>
    <declare-styleable name="circleview">
        <attr name="cRadius" format="integer" />
        <attr name="cFillColor" format="color" />
        <attr name="cStrokeColor" format="color" />
        <attr name="cAngleStart" format="integer" />
        <attr name="cAngleEnd" format="integer" />
    </declare-styleable>
</resources>

As you can probably guess we will have a nice little circle which we can display in a few ways, not so usuable. ( Except to display a Pac-Man ! : )

Lets look at the main layout that incorparate my CircleView class.

layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:circleview="http://schemas.android.com/apk/res/se.adanware.canvasplaying"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
    <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Hello World, CanvasActivity"
            />
    <se.adanware.canvasplaying.CircleView
            android:id="@+id/pieCircle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            circleview:cFillColor="#DDaa99"
            circleview:cStrokeColor="@android:color/white"
            circleview:cRadius="80"
            circleview:cAngleStart="30"
            circleview:cAngleEnd="290"
            />
    <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Hello World, CanvasActivity"
            />
</LinearLayout>

Notice the xmlns:circleview namespace that's called the same as the packagename. The prefix 'circleview' can be whatever we want. I added the two TextViews just to see the span of the height at the beginning.
As you can see we can shape our circle and draw it as we like either put in a radius (in pixels) or we can make use of the height or width. It'll calculate the circle radius so it fits depending on the height or width if the cRadius attribute is omitted. Screenshot of above settings :

Lets move on to the CircleView class. All views that wants to be emedded in xml need to have, quote
'To allow the Android Developer Tools to interact with your view, at a minimum you must provide a constructor that takes a Context and an AttributeSet object as parameters. This constructor allows the layout editor to create and edit an instance of your view.' 
Make sure your constructor has public access, otherwise it's locked to your own package.

src/CircleView.java
package se.adanware.canvasplaying;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CircleView extends View {

        private Paint circlePaint;
        private Paint circleStrokePaint;
        private RectF circleArc;

        // Attrs
        private int circleRadius;
        private int circleFillColor;
        private int circleStrokeColor;
        private int circleStartAngle;
        private int circleEndAngle;

    public CircleView(Context context, AttributeSet attrs) {

        super(context, attrs);
        init(attrs); // Read all attributes

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStyle(Paint.Style.FILL);
        circleStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circleStrokePaint.setStyle(Paint.Style.STROKE);
        circleStrokePaint.setStrokeWidth(2);
        circleStrokePaint.setColor(circleStrokeColor);
    }

    public void init(AttributeSet attrs)
    {
        // Go through all custom attrs.
        TypedArray attrsArray = getContext().obtainStyledAttributes(attrs, R.styleable.circleview);
        circleRadius = attrsArray.getInteger(R.styleable.circleview_cRadius, 0);
        circleFillColor = attrsArray.getColor(R.styleable.circleview_cFillColor, 16777215);
        circleStrokeColor = attrsArray.getColor(R.styleable.circleview_cStrokeColor, -1);
        circleStartAngle = attrsArray.getInteger(R.styleable.circleview_cAngleStart, 0);
        circleEndAngle = attrsArray.getInteger(R.styleable.circleview_cAngleEnd, 360);
        // Google tells us to call recycle.
        attrsArray.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Move canvas down and right 1 pixel.
        // Otherwise the stroke gets cut off.
        canvas.translate(1,1);
        circlePaint.setColor(circleFillColor);
        canvas.drawArc(circleArc, circleStartAngle, circleEndAngle, true, circlePaint);
        canvas.drawArc(circleArc, circleStartAngle, circleEndAngle, true, circleStrokePaint);
    }

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {

        int measuredWidth = measureWidth(widthMeasureSpec);
        if(circleRadius == 0) // No radius specified.
        {                     // Lets see what we can make.
            // Check width size. Make radius half of available.
            circleRadius = measuredWidth / 2;
            int tempRadiusHeight = measureHeight(heightMeasureSpec) / 2;
            if(tempRadiusHeight < circleRadius)
                // Check height, if height is smaller than
                // width, then go half height as radius.
                circleRadius = tempRadiusHeight;
        }
        // Remove 2 pixels for the stroke.
        int circleDiameter = circleRadius * 2 - 2;
        // RectF(float left, float top, float right, float bottom)
        circleArc = new RectF(0, 0, circleDiameter, circleDiameter);
        int measuredHeight = measureHeight(heightMeasureSpec);
        setMeasuredDimension(measuredWidth, measuredHeight);
        Log.d("onMeasure() ::", "measuredHeight =>" + String.valueOf(measuredHeight) + "px measuredWidth => " + String.valueOf(measuredWidth) + "px");
    }

    private int measureHeight(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int result = 0;
        if (specMode == MeasureSpec.AT_MOST) {
            result = circleRadius * 2;
        } else if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        }
        return result;
    }

    private int measureWidth(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int result = 0;
        if (specMode == MeasureSpec.AT_MOST) {
            result = specSize;
        } else if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        }
         return result;
    }
}

As you can see it's really easy. Measure the view and draw accordingly, no failsafe if you specify the radius to large but it's easy to implement though, just check the radius with the width and scale it down. This was just a small example on constructing custom views.

src/CanvasActivity.java
package se.adanware.canvasplaying;

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

public class CanvasActivity extends Activity {
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

I'll make another post about adding data to the custom view when i have the time!


tisdag 18 september 2012

Misc : Been moving, renovating.

So me and my fiancee have finally moved to our own place. Been kinda hectic so haven't had much time doing any coding or playing with Android or Java. Living room is finally ready, i did a little media wall to hide all the cables to the speakers and television.


After wallpaper and everything mounted.


Lousy picture, now i just need to choose a media streamer / extender. Before we had the computer in the living room so we just used a simple hdmi cable to the reciever and started everything that way. Alas no more. Been doing some research and havent gotten any wiser.

Requirements:

  • ISO support. DVD & Bluray with menu support.
  • HD audio formats.
  • Preferrebly 3D support (Used once a year : )
  • Fast GUI
  • All regular media containers, MKV w/ embedded subtitles. External subtitles, DTS, etc.
Looked at building a small HTPC but would be simpler with a little box that can stream everything.
I've eyed the Mede8er MED1000X3D , fairly recent product with hopefully some good support. The design doesnt really impress me but hopefully the inside does. I'll update the post with the solution i take and a mini review.

MED1000X3D Mini Review :

A MED1000X3D have been bought and tried out in the living room. First time i started it, remote control functions never got any response or lagged really bad. I was nervous.

A quick restart and it behaves better. This review is based mostly on it’s capabilities on movie playback of different formats. I’ve never used the drivebay inside the machine, i just stream from my computer and it’s connected to the home network by wire.

First of, it handles all the usual movie containers perfect, MKV, AVI, and DVD ISO files, etc. HD Audio tracks which was a buying point for me works nicely and gets decoded by the reciever.

Bluray can be played either from an ISO file or from the correct directory structure (i.e a BDMV folder) it will play it as a disc. However the menu on most of my new ones i’ve tried doesn’t work and will probably never get implemented. Quote from their forum :
‘No media streamer will offer this as a Blu-ray stack is necessary, along with a full license, to support full menus. Plus, doing so means you must implement Cinavia copy protection, which renders Blu-ray backups (ISO's) useless.’

Subtitles which i use is also nice, can be resized and moved around. Need ‘em, otherwise i need to crank the volume up so high. Last function i played around with is their custom software for building a ‘Movie Jukebox’, just place a file with a NFO file extension  with the correct IMDB url in the movie folder and the program will do the rest. Cover, artwork, etc.

There are a few Internet related apps included in their software. Youtube, Internet Radio, Weather and some video feeds.  All in all i’m happy.

Minus-
Remote Control, kinda plastic and needs accurate pointing to the device.
Price, well, based on the chipset other manufacturers use for their Media Extenders  it’s a bit hefty. They do include a HDMI cable though. : )  

My score: 8/10



torsdag 5 juli 2012

Android : Simple HTML parsing & image downloader using AsyncTask

A barebone html parser and a simple image downloader for a certain comic. This one may please your girlfriend or wife. I'll show a simple DefaultHttpClient and ResponseHandle in conjunction with a AsyncTask class.(Although with almost no error checking) The comic we want is Love Is, i've stripped it so it's just a ImageView for the image, a button for Previous/Next and a simple TextView for the date.

Lets start with the simple helper class that will connect to the homepage, parse the html and download the image.

Update, GoComics.com have removed their Love Is... comic strip. Updated it with another page.

(note only updated LoveIsParser.java, they use strange dates for their pictures, reused ones ? So need to store url to previous and next picture before closing the AsyncTask... just did a quick hack to get it working again.)

src/LoveIsParser.java
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class LoveIsParser {

    final String loveIsUrl = "http://loveiscomix.com/";
    private String urlImage;
    private Bitmap loveIsBitmap;
    private Pattern patternForImage = Pattern.compile("static/loveisbnk/.........gif");
    public LoveIsParser()
    { }

    public Bitmap getLoveIsBitmap()
    {
        return loveIsBitmap;
    }

    public boolean downloadImage(Calendar c)
    {
        //String urlExtension = c.get(Calendar.YEAR) + "/" + padString(c.get(Calendar.MONTH)+1) + "/" + padString(c.get(Calendar.DAY_OF_MONTH));
        //Log.d("LoveIS Url:", loveIsUrl + urlExtension);
        DefaultHttpClient httpClient = new DefaultHttpClient();
        BasicResponseHandler responseHandler = new BasicResponseHandler();
        HttpGet request = new HttpGet(loveIsUrl);
        try
        {
            String htmlBody = httpClient.execute(request, responseHandler);
            Matcher m = patternForImage.matcher(htmlBody);
            if(m.find())
            {
                urlImage = m.group();
                urlImage = loveIsUrl + urlImage;
                Log.d("Image Url:", urlImage);
                request = new HttpGet(urlImage);
                HttpResponse response = httpClient.execute(request);
                InputStream in = response.getEntity().getContent();
                BufferedInputStream bis = new BufferedInputStream(in, 8192);
                loveIsBitmap = BitmapFactory.decodeStream(bis);
                bis.close();
                in.close();
                return true;
            }
        }
        catch (Exception e)
        {
            Log.d("Exception", e.toString());
        }
        return  false;
    }

    public String padString(int number)
    {
        return String.format("%02d", number);
    }
}

Most is self explanatory, downloadImage function takes a Calendar, parses the date and completes the url.
Adding 1 to the month as it's zero-based and using padString to pad with a 0 if it's singledigit.

Lets move on to the layout.

layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <ImageView  android:id="@+id/ivLove"
                android:layout_height="wrap_content"
                android:layout_width="fill_parent"/>

    <TextView android:id="@+id/tvDate"
              android:layout_height="wrap_content"
              android:layout_width="wrap_content"/>
    <Button android:layout_height="wrap_content"
            android:layout_width="fill_parent"
            android:id="@+id/btnPrevious"
            android:text="Previous"/>
    <Button android:layout_height="wrap_content"
            android:layout_width="fill_parent"
            android:id="@+id/btnNext"
            android:text="Next"/>
</LinearLayout>

And lastly our launcher activity.

src/MainActivity.java

import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.Calendar;

public class MainActivity extends FragmentActivity {

    Calendar c;
    TextView tvDate;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.loveis);
        c = Calendar.getInstance();
        ImageView imageView = (ImageView) findViewById(R.id.ivLove);
        tvDate = (TextView) findViewById(R.id.tvDate);
        tvDate.setText(c.getTime().toLocaleString());
        Button btnPrevious = (Button) findViewById(R.id.btnPrevious);
        Button btnNext = (Button) findViewById(R.id.btnNext);
        if(isOnline())
            new GetAndSetImage().execute(c);
        else
            Toast.makeText(this, "No Internet connection found.", Toast.LENGTH_LONG).show();

        btnPrevious.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                c.add(Calendar.DATE, -1);
                tvDate.setText(c.getTime().toLocaleString());
                new GetAndSetImage().execute(c);
            }
        });

        btnNext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                c.add(Calendar.DATE, 1);
                tvDate.setText(c.getTime().toLocaleString());
                new GetAndSetImage().execute(c);
            }
        });
    }

    private class GetAndSetImage extends AsyncTask<Calendar, Void, Bitmap>
    {
        ProgressDialog pd;

        @Override
        protected Bitmap doInBackground(Calendar... c) {
            LoveIsParser parser = new LoveIsParser();
            if(parser.downloadImage(c[0]))
                return parser.getLoveIsBitmap();
            else
            {   // We just return a drawable if there's an error in the download.
                return BitmapFactory.decodeResource(getResources(), R.drawable.icon);
            }
        }

        @Override
        protected void onPreExecute()
        {
            pd = new ProgressDialog(MainActivity.this);
            pd.setProgressStyle(ProgressDialog.STYLE_SPINNER);
            pd.setMessage("Downloading image...");
            pd.show();
        }

        @Override
        protected void onPostExecute(Bitmap bm)
        {
            pd.dismiss();
            ImageView iv = (ImageView) findViewById(R.id.ivLove);
            iv.setImageBitmap(bm);
        }
    }

    public boolean isOnline() {
        ConnectivityManager cm =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = cm.getActiveNetworkInfo();
        if (netInfo != null && netInfo.isConnectedOrConnecting()) {
            return true;
        }
        return false;
    }

}

Buttons decrease and increase the date when clicked and download the image based on the current date using a simple AsyncTask that display a 'Progress Dialog' while it's downloading. 

Now just flash it up a bit (hearts and red layout ^^) and install on your girlfriends phone for some extra romance, or implement sharing so you can easily send the picture as an MMS whenever you want.

Screenshot:



fredag 15 juni 2012

Misc : What our loved ones do for us!


Sorry for the bad picture quality, photographed somewhere in Ao Nang, Thailand. I like this one!

Nothing Android related today, in a few days there might be a terrace house in store for us. Just wanted to share this picture. Feels warm & fuzzy in a strange way...

( My dream is perhaps to own a 3 wheeled tractor, and some fields to work. : )

Love you, U.

tisdag 5 juni 2012

Android : Getting / Measuring Fragment width

It’s easy enough with LinearLayout and the weight sum option to specify the width according to the screen but sometimes we need to use another layout, i.e a RelativeLayout. For me personally i needed the pixel width as i wanted to draw a percentage bar, i choose to use a simple TextView with a custom background. I need the total available width of the fragment to set the TextView width as a percent.


I couldnt get the width in any of the startup methods as the layout isn't done.
The ViewTreeObserver helped me out, from the docs:


public ViewTreeObserver getViewTreeObserver ()
Since: API Level 1

Returns the ViewTreeObserver for this view's hierarchy. The view tree observer can be used to get notifications when global events, like layout, happen. The returned ViewTreeObserver observer is not guaranteed to remain valid for the lifetime of this View. If the caller of this method keeps a long-lived reference to ViewTreeObserver, it should always check for the return value of isAlive().

Lets create a test fragment.


src/TestFragment.java
import android.app.Fragment;
import android.os.Bundle;
import android.view.*;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class TestFragment extends Fragment {
    int fragmentWidth;
    int fragmentHeight;
    int calls;
    int percent;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_one, null);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        percent = 100;
        final View myView = getView();
        final TextView tvOne = (TextView) myView.findViewById(R.id.tvOne);
        final TextView tvTwo = (TextView) myView.findViewById(R.id.tvTwo);
        final TextView tvThree = (TextView) myView.findViewById(R.id.tvThree);

        final Button myButton = (Button) myView.findViewById(R.id.myButton);
        myView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
          @Override
          public void onGlobalLayout() {
              calls++;
              fragmentWidth = getView().getWidth();
              fragmentHeight = getView().getHeight();
              if(fragmentWidth > 0)
              {
                  // Width greater than 0, layout should be done.
                  // Set the textviews and remove the listener.
                  tvOne.setText("Fragment Width: " + String.valueOf(fragmentWidth) +                                         " Height: " + String.valueOf(fragmentHeight));
                  tvTwo.setText("Calls to onGlobalLayout: " + String.valueOf(calls));
                  getView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
              }
          }
        });

        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
              percent = percent - 10;
              // Need to switch FILL_PARENT to WRAP_CONTENT 
              // otherwise setWidth(pixels) doesn't work.
             RelativeLayout.LayoutParams myParams 
                      = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
              myParams.addRule(RelativeLayout.BELOW, R.id.myButton);
              tvThree.setLayoutParams(myParams);
              if(percent > 0)
              {
               tvThree.setWidth((fragmentWidth * percent)/100);
               tvThree.setText(String.valueOf(percent) + "%");
              }
              else
              {
               percent = 100;
               tvThree.setWidth(fragmentWidth);
               tvThree.setText("100% again!");
              }
            }
        });
    }
}

layout/fragment_one.xml
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical">
    <TextView android:id="@+id/tvOne"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:background="@android:color/holo_blue_dark"
              android:layout_alignParentLeft="true"
              />
    <TextView android:id="@+id/tvTwo"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:layout_alignParentLeft="true"
              android:layout_below="@id/tvOne"
              />
    <Button android:id="@+id/myButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_below="@id/tvTwo"
            android:text="-10%" />

    <TextView android:id="@+id/tvThree"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:background="@android:color/holo_blue_dark"
              android:text="100%"
              android:gravity="right"
              android:layout_alignParentLeft="true"
              android:layout_below="@id/myButton"
            />
</RelativeLayout>


All seems well, the ViewTreeObserver.OnGlobalLayoutListener() gets the correct width and height as the screenshots prove.


What if we need the width in a custom adapter ? Doing the layout based on the data you set in it.
Let's create another fragment.

layout/fragment_two.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
       <ListView android:id="@+id/myListView"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 />
</LinearLayout>


src/DataFragment.java
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Random;

public class DataFragment extends Fragment {
    IntItemAdapter myAdapter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_two, null);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        ArrayList<Integer> myPercentInts = new ArrayList<Integer>();
        Random myRandom = new Random();
        for(int i = 0; i < 100; i++)
            myPercentInts.add(myRandom.nextInt(100) + 1);

        View myView = getView();
        ListView myListView = (ListView) myView.findViewById(R.id.myListView);
        myAdapter = new IntItemAdapter(getActivity(), R.layout.row_integer_layout, myPercentInts);
        myListView.setAdapter(myAdapter);

        myListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                TextView testWidth = (TextView) view.findViewById(R.id.tvPercentUsed);
                Log.d("TextView Width:", String.valueOf(testWidth.getWidth()));
            }
        });

    }

    public class IntItemAdapter extends ArrayAdapter<Integer>
    {
        private Activity context;
        private int resourceID;
        ArrayList<Integer> data = null;
        int screenWidth;

        public IntItemAdapter(Activity context, int resource, ArrayList<Integer> data)
        {
            super(context, resource, data);
            this.context = context;
            this.data = data;
            this.resourceID = resource;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {
            View row = convertView;
            if(screenWidth <= 0)
            {
                // Get the total width that ListView allocates.
                screenWidth = parent.getWidth();
            }
            if(row == null)
            {
                LayoutInflater inflater = context.getLayoutInflater();
                row = inflater.inflate(resourceID, parent, false);
            }

            Integer item = data.get(position);
            if(item != null)
            {
                TextView itemPercentUsed = (TextView) row.findViewById(R.id.tvPercentUsed);
                if(itemPercentUsed != null)
                {
                    int newWidth = (screenWidth * item) / 100;
                    itemPercentUsed.setWidth(newWidth);
                    itemPercentUsed.setText(item.toString() + "%");
                }
            }
            return row;
        }
    }
}

Works but doesnt look so nice code-wise, more elegant solution ? We could use the first solution instead and create the adapter when we know the true width of the fragment. That's if ListView is supposed to fill it all up or implement the ViewTreeObserver in the adapter.

Screenshots, both fragments visible: (Updated the DataFragment TextView background with a drawable)



torsdag 10 maj 2012

Android : onCreateContextMenu in multiple visible fragments

I've been rewriting my application to support fragments and had a little problem with my context menu as i show two ListView's most of the time. Missed the information at Android Developer homepage.

Note: Although your fragment receives an on-item-selected callback for each menu item it adds, the activity is first to receive the respective callback when the user selects a menu item. If the activity's implementation of the on-item-selected callback does not handle the selected item, then the event is passed to the fragment's callback. This is true for the Options Menu and context menus.

And it' doesnt go directly to the fragment that was pressed, it goes from first created to last created.
I handle them in each fragment so we need to know how which one was orginating the call.

public class FragmentCategories extends Fragment {
    static final int FRAGMENT_GROUPID = 30;
    static final int MENU_EDIT = 1;
    static final int MENU_REMOVE = 2;

   @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuinfo)
    {
        super.onCreateContextMenu(menu, v, menuinfo);
        menu.add(FRAGMENT_GROUPID, MENU_EDIT, Menu.NONE, "Edit");
        menu.add(FRAGMENT_GROUPID, MENU_REMOVE, Menu.NONE, "Remove);
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuinfo;
    }
    @Override
    public boolean onContextItemSelected(MenuItem item)
    {
       // Set a different FRAGMENT_GROUPID on each fragment.
       // A simple check, only continues on the correct fragment.
        if(item.getGroupId() == FRAGMENT_GROUPID)
        {
            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
            selectedCategory = (BudgetCategory) cListView.getItemAtPosition(info.position);
            switch (item.getItemId())
            {
                case MENU_EDIT:
                    // Do something!
                    return true;
                case MENU_REMOVE:
                    // Do something!
                    return true;
            }
        }
        // Be sure to return false or super's so it will proceed to the next fragment!
        return super.onContextItemSelected(item);
    }


Simply set a unique identifier in each fragment and create the menu with the the identifier as groupID.
The callback will fall through until we hit the fragment that made the call.

public abstract MenuItem add(int groupId, int itemId, int order, int titleRes)
  Parameters
    groupId    The group identifier that this item should be part of. 
               This can also be used to define groups of items for batch state changes.
               Normally use NONE if an item should not be in a group.
    itemId     Unique item ID. Use NONE if you do not need a unique ID.
    order      The order for the item. 
               Use NONE if you do not care about the order. See getOrder().
    titleRes   Resource identifier of title string.

torsdag 26 april 2012

Android : Custom Tab layouts just using XML

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

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

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

color/tab_textcolor.xml

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

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

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

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

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

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

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

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


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

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

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

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

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

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


The layout for the activites we create in the tabs.


src/MyActivity.java

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

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

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

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




src/TestActivity.java

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

public class TestActivity extends Activity {

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

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

    }
}


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


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

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

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

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

No more visible corners ! : )



måndag 9 april 2012

Android : Multiple Selection ListView with Custom Layout

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

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

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

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

    private boolean isChecked;
    private List<Checkable> checkableViews;

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

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

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

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

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

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

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

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

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

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

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

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

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

    public String getTeamName() {
        return teamName;
    }

    public int getTeamWins() {
        return teamWins;
    }
}


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

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

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

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

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


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

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

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

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

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

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

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

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

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

Finally we come to our startup activity!


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

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

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


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



... Family time, nightie!


tisdag 27 mars 2012

Android : Custom spinner with custom object!

Lets spin forward by implementing a custom spinner with a custom drop down view!
Using style= tag on the spinner didnt work for me to override the styles, so i just changed the background.
First a custom drawable selector with the appropiate images.

drawable/btn_dropdown.xml
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="false" android:state_enabled="true" android:drawable="@drawable/btn_dropdown_normal" />
    <item android:state_pressed="true" android:drawable="@drawable/btn_dropdown_pressed" />
    <item android:state_focused="true" android:state_enabled="true" android:drawable="@drawable/btn_dropdown_pressed" />
    <item android:state_enabled="true" android:drawable="@drawable/btn_dropdown_normal" />
    <item android:drawable="@drawable/btn_dropdown_normal" />
</selector>

Next, i just reworked the btn_dropdown_* images that ships with the Android SDK. They should be located in ~androidsdkroot/platforms/android-(yourversion)/data/res/drawable-hdpi. There's 5 different states that can be used but i just made images for two. Normal & pressed.

Okay, the images done! Just need to change the spinner background to @drawable/btn_dropdown.xml to get our own look! I still use the android.R.layout.simple_spinner_item to display the objects in my spinner.
Lets get going, the main layout:

layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Custom Spinner with custom data" />
    <Spinner  android:id="@+id/SpinnerOrginal"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              />
    <Spinner android:id="@+id/SpinnerCustom"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:background="@drawable/btn_dropdown"
             />
    <Button android:id="@+id/buttonUseItem"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Use Selection"/>
    <TextView android:id="@+id/myTextView"
              android:layout_height="wrap_content"
              android:layout_width="wrap_content"/>
</LinearLayout>
Next up our helper class from the previous entry with a little modification.
src/CountryInfo.java
public class CountryInfo {
    private String countryName;
    private long countryPopulation;
    private int countryFlag; // Populate it with our resource ID for the correct image.
    
    public CountryInfo(String cName, long cPopulation, int flagImage)
    {
        countryName = cName;
        countryPopulation = cPopulation;
        countryFlag = flagImage;
    }
    public String getCountryName()
    {
        return countryName;
    }
    public long getCountryPopulation()
    {
        return countryPopulation;
    }
    public int getCountryFlag()
    {
        return countryFlag;
    }
    public String toString()
    {
        return countryName;
    }
}
And lastly our main activity with our CountryAdapter class that will implement the view.
src/SpinnerTest.java (First part)

import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import java.util.ArrayList;

public class SpinnerTest extends Activity
{
    
    Button button_UseSelectedItem;
    Spinner mySpinner;
    TextView myTextView;
    ArrayList<CountryInfo> myCountries;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        myCountries = populateList();
        setContentView(R.layout.main);
        mySpinner = (Spinner) findViewById(R.id.SpinnerCustom);
        Spinner OrginalSpinner = (Spinner) findViewById(R.id.SpinnerOrginal);
        button_UseSelectedItem = (Button) findViewById(R.id.buttonUseItem);
        myTextView = (TextView) findViewById(R.id.myTextView);

        CountryAdapter myAdapter = new CountryAdapter(this, android.R.layout.simple_spinner_item, myCountries);

        mySpinner.setAdapter(myAdapter);
        OrginalSpinner.setAdapter(myAdapter);
        
        button_UseSelectedItem.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // Can also use mySpinner.setOnItemClickListener(......) 
                // Using a separate button here as there's often other data to select
                // or if you choose the wrong item.
                CountryInfo myCountry;
                if(mySpinner.getSelectedItem() != null)
                {
                    myCountry = (CountryInfo) mySpinner.getSelectedItem();
                    myTextView.setText(String.format("Country: " + myCountry.getCountryName() + "\t Population: " + myCountry.getCountryPopulation()));
                }
            }
        });
    }

    public ArrayList<CountryInfo> populateList()
    {
        ArrayList<CountryInfo> myCountries = new ArrayList<CountryInfo>();
        myCountries.add(new CountryInfo("USA", 308745538, R.drawable.usa)); // Image stored in /drawable
        myCountries.add(new CountryInfo("Sweden", 9482855, R.drawable.sweden));
        myCountries.add(new CountryInfo("Canada", 34018000, R.drawable.canada));
        return myCountries;
    }


As you see it looks like the previous one except we have created a custom adapter for our new spinner. populateList() method also gets the ints from the icons i have in the drawable/ directory.
Okay lets continue with the adapter!
src/SpinnerTest.java (2nd part)
public class CountryAdapter extends ArrayAdapter<CountryInfo>
    {
        private Activity context;
        ArrayList<CountryInfo> data = null;

        public CountryAdapter(Activity context, int resource, ArrayList<CountryInfo> data)
        {
            super(context, resource, data);
            this.context = context;
            this.data = data;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) 
        {   // Ordinary view in Spinner, we use android.R.layout.simple_spinner_item
            return super.getView(position, convertView, parent);   
        }

        @Override
        public View getDropDownView(int position, View convertView, ViewGroup parent)
        {   // This view starts when we click the spinner.
            View row = convertView;
            if(row == null)
            {
                LayoutInflater inflater = context.getLayoutInflater();
                row = inflater.inflate(R.layout.spinner_layout, parent, false);
            }

            CountryInfo item = data.get(position);

            if(item != null)
            {   // Parse the data from each object and set it.
                ImageView myFlag = (ImageView) row.findViewById(R.id.imageIcon);
                TextView myCountry = (TextView) row.findViewById(R.id.countryName);
                if(myFlag != null)
                {
                    myFlag.setBackgroundDrawable(getResources().getDrawable(item.getCountryFlag()));
                }
                if(myCountry != null)
                    myCountry.setText(item.getCountryName());

            }

            return row;
        }
    }
}



getView() method could be skipped, typed it along for clarity. getDropDownView() inflates my own layout for the drop down list. I just inflate it and set the corresponding ImageView & TextView from each object in the ArrayList. Plenty of information if you google ListView and custom adapters.

layout/spinner_layout.xml ( LinearLayout with a ImageView and TextView )

<?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="wrap_content"
              android:orientation="horizontal"
              android:background="@drawable/bluegradient">
<ImageView android:id="@+id/imageIcon"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:background="@drawable/canada"/>
<TextView android:id="@+id/countryName"
          android:singleLine="true"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:gravity="center"
          android:ellipsize="marquee"
          style="@style/SpinnerText"/>
</LinearLayout>

drawable/bluegradient.xml (Our background in the dropdown view, changes when item is pressed)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" >
        <shape>
            <solid
                android:color="#2183b0" />
            <stroke
                android:width="1dp"
                android:color="#adc6e8" />
           <corners
                android:radius="4dp" />
            <padding
                android:left="4dp"
                android:top="4dp"
                android:right="4dp"
                android:bottom="4dp" />
        </shape>
    </item>
    <item>
        <shape>
            <gradient
                android:startColor="#2183b0"
                android:endColor="#7cbfde"
                android:angle="270" 
                android:type="linear"
                />
            <stroke
                android:width="1dp"
                android:color="#2183b0" />
            <corners
                android:radius="4dp" />
            <padding
                android:left="0dp"
                android:top="4dp"
                android:right="0dp"
                android:bottom="4dp" />
        </shape>
    </item>
</selector>


That's it! Final result, spinner closed, and open!