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)