Tuesday, April 30, 2013

Understand lifecycle of Activity and Fragment, Introduction

To develop Android Apps run on Fragment, understanding of lifecycle of Activity and Fragment is very important. (I'm confused now!)

It is a simple exercise to load Fragment using FragmentTransaction, with all lifecycle methods overrided to display the status on Toast and output to Log. Such that you can easy understand the lifecycle behavior between Activity and Fragment.

Understand lifecycle of Activity and Fragment


MainActivity.java
package com.example.androidfragmenttest;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.Toast;
import android.app.Activity;

public class MainActivity extends  FragmentActivity {
 
 static public class MyFragment1 extends Fragment {
  
  String savedText;

  @Override
  public void onAttach(Activity activity) {
   toastFragment1Method();
   super.onAttach(activity);
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
   toastFragment1Method();
   super.onCreate(savedInstanceState);
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   toastFragment1Method();
   View view = inflater.inflate(R.layout.fragment_layout1, null);
   return view;
  }
  
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   toastFragment1Method();
   super.onActivityCreated(savedInstanceState);
  }

  @Override
  public void onStart() {
   toastFragment1Method();
   super.onStart();
  }

  @Override
  public void onResume() {
   toastFragment1Method();
   super.onResume();
  }

  @Override
  public void onPause() {
   toastFragment1Method();
   super.onPause();
  }

  @Override
  public void onStop() {
   toastFragment1Method();
   super.onStop();
  }

  @Override
  public void onDestroyView() {
   toastFragment1Method();
   super.onDestroyView();
  }

  @Override
  public void onDestroy() {
   toastFragment1Method();
   super.onDestroy();
  }

  @Override
  public void onDetach() {
   toastFragment1Method();
   super.onDetach();
  }
  
  private void toastFragment1Method(){
   StackTraceElement[] s = Thread.currentThread().getStackTrace();
   String methodName = s[3].getMethodName();
   Toast.makeText(getActivity(), 
     "MyFragment1 : " + methodName, Toast.LENGTH_SHORT).show();
   
   Log.i("MYTAG", "MyFragment1 : " + methodName);
  }

 }
 
 FrameLayout fragmentContainer;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  toastActivityMethod();
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  fragmentContainer = (FrameLayout)findViewById(R.id.container);
  if(savedInstanceState == null){
   //if's the first time created
   MyFragment1 myListFragment1 = new MyFragment1();
   FragmentManager supportFragmentManager = getSupportFragmentManager();
   FragmentTransaction fragmentTransaction = supportFragmentManager.beginTransaction();
   fragmentTransaction.add(R.id.container, myListFragment1);
   fragmentTransaction.commit(); 
  }
  
 }
 
 @Override
 protected void onStart() {
  toastActivityMethod();
  super.onStart();
 }

 @Override
 protected void onResume() {
  toastActivityMethod();
  super.onResume();
 }

 @Override
 protected void onPause() {
  toastActivityMethod();
  super.onPause();
 }
 
 @Override
 protected void onStop() {
  toastActivityMethod();
  super.onStop();
 }

 @Override
 protected void onDestroy() {
  toastActivityMethod();
  super.onDestroy();
 }

 private void toastActivityMethod(){
  StackTraceElement[] s = Thread.currentThread().getStackTrace();
  String methodName = s[3].getMethodName();
  Toast.makeText(getApplicationContext(), 
    "MainActivity : " + methodName, Toast.LENGTH_SHORT).show();
  
  Log.i("MYTAG", "MainActivity : " + methodName);
 }

}


/res/layout/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </FrameLayout>

</RelativeLayout>


/res/layout/fragment_layout1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Fragment 1"/>
    
</LinearLayout>




download filesDownload the files.

To view the Log in Eclipse, Show View of LogCat in Window menu; select info and enter tag:MYTAG in the search box.



Suggested Android Developers doc:
- Activity
- Fragments





Next:


Sunday, April 28, 2013

Simulate kill activity to test onSaveInstanceState() / onCreate()

In order to test the lifecycle behaviour of our apps, we always have to leave our apps to move it to background, wait sometime for the system to kill it...and restart it.

It's a alternative to simulate "kill activity" by the Immediately destroy activities (or called Don't keep activities/Do not keep activities) selection Development Settings:

Select Developer options in Setting
Select Developer options in Setting

Select Advanced

Check Don't keep activities


According to the Android Developer document of Using the Dev Tools App:

Immediately destroy activities
Tells the system to destroy an activity as soon as it is stopped (as if Android had to reclaim memory). This is very useful for testing the onSaveInstanceState(Bundle) / onCreate(android.os.Bundle) code path, which would otherwise be difficult to force. Choosing this option will probably reveal a number of problems in your application due to not saving state. For more information about saving an activity's state, see the Activities document.


Notice: This setting for developers only, NOT for general user.



Related: The lifecycle in Activity Killed

Saturday, April 27, 2013

Embed WebView in Fragment

This exercise embed WebView in Fragment.

Embed WebView in Fragment


package com.example.androidwebviewfragment;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends Activity {

 static public class MyWebViewFragment extends Fragment {
  
  WebView myWebView;
  final static String myBlogAddr = "http://android-er.blogspot.com";
  String myUrl;
  

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.layout_webfragment, container, false);
   myWebView = (WebView)view.findViewById(R.id.mywebview);
   
   myWebView.getSettings().setJavaScriptEnabled(true);                
   myWebView.setWebViewClient(new MyWebViewClient());
   
   if(myUrl == null){
    myUrl = myBlogAddr;
   }
   myWebView.loadUrl(myUrl);
       
         return view;

  }
  
  private class MyWebViewClient extends WebViewClient {
         @Override
         public boolean shouldOverrideUrlLoading(WebView view, String url) {
          myUrl = url;
             view.loadUrl(url);
             return true;
         }
     }

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

 }

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

 @Override
 public void onBackPressed() {
  MyWebViewFragment fragment = 
    (MyWebViewFragment)getFragmentManager().findFragmentById(R.id.myweb_fragment);
  WebView webView = fragment.myWebView;
  
  if(webView.canGoBack()){
   webView.goBack();
  }else{
   super.onBackPressed();
  }
 }

}


Layout, activity_main.xml.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <fragment
        android:name="com.example.androidwebviewfragment.MainActivity$MyWebViewFragment"
        android:id="@+id/myweb_fragment"
        android:layout_height="match_parent"
        android:layout_width="match_parent" />

</RelativeLayout>


Layout of our fragment, layout_webfragment.xml.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <WebView
        android:id="@+id/mywebview"
        android:layout_height="match_parent"
        android:layout_width="match_parent" />

</LinearLayout>


Permission of "android.permission.INTERNET" is need.

- I can't implement using WebViewFragment, so this exercise extends Fragment, not WebViewFragment.
- In this implement, the WebView can load the last loaded address after orientation changed, but cannot keep the navigation history.

download filesDownload the files.

Friday, April 26, 2013

Implement animation in FragmentTransaction

setCustomAnimations(int enter, int exit, int popEnter, int popExit) and setCustomAnimations(int enter, int exit) methods of FragmentTransaction class set specific animation resources to run for the fragments that are entering and exiting in this transaction. The popEnter and popExit animations will be played for enter/exit operations specifically when popping the back stack.

Example Code:

    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();

    fragmentTransaction.setCustomAnimations(
      R.anim.fadein, R.anim.fadeout, R.anim.fadein, R.anim.fadeout);

    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();


animation in FragmentTransaction


Further work on previous exercise "implements OnBackStackChangedListener to display UP icon, automatically by getBackStackEntryCount()".

Create /res/anim/fadein.xml.
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:propertyName="alpha"
    android:valueFrom="0.0" 
    android:valueTo="1" 
    android:duration="500"
/> 


/res/anim/fadeout.xml.
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:propertyName="alpha"
    android:valueFrom="1" 
    android:valueTo="0.0" 
    android:duration="500"
/> 


MainActivity.java
package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager.OnBackStackChangedListener;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity implements OnBackStackChangedListener{
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 static boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
   
   String clickedDetail = (String)l.getItemAtPosition(position);
   
   if(isSinglePane == true){
    /*
     * The second fragment not yet loaded. 
     * Load MyDetailFragment by FragmentTransaction, and pass 
     * data from current fragment to second fragment via bundle.
     */
    MyDetailFragment myDetailFragment = new MyDetailFragment();
    Bundle bundle = new Bundle();
    bundle.putString("KEY_DETAIL", clickedDetail);
    myDetailFragment.setArguments(bundle);
    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();
    
    fragmentTransaction.setCustomAnimations(
      R.anim.fadein, R.anim.fadeout, R.anim.fadein, R.anim.fadeout);
    
    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    
    /*
     * Add this transaction to the back stack. 
     * This means that the transaction will be remembered after it is 
     * committed, and will reverse its operation when later popped off 
     * the stack.
     */
    fragmentTransaction.addToBackStack(null);
    
    fragmentTransaction.commit();
    
   }else{
    /*
     * Activity have two fragments. Pass data between fragments
     * via reference to fragment
     */
    
    //get reference to MyDetailFragment
    MyDetailFragment myDetailFragment =
      (MyDetailFragment)getFragmentManager().findFragmentById(R.id.detail_fragment);
    myDetailFragment.updateDetail(clickedDetail);
    
   }
  }
  
 }
 
 public static class MyDetailFragment extends Fragment {
  
  TextView textDetail;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   textDetail = (TextView)view.findViewById(R.id.text_detail);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String detail = bundle.getString("KEY_DETAIL", "no argument pass");
    textDetail.setText(detail);
   }
   
   return view;
  }

  public void updateDetail(String detail) {
   textDetail.setText(detail);
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
  
  getFragmentManager().addOnBackStackChangedListener(this);
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
  case android.R.id.home:
   getFragmentManager().popBackStack();
            return true;    
  }
  
  return super.onOptionsItemSelected(item);
 }
 
 final static String KEY_DISPLAY_OPT = "KEY_Display_Option";

 @Override
 protected void onSaveInstanceState(Bundle outState) {
  // TODO Auto-generated method stub
  super.onSaveInstanceState(outState);
  outState.putInt(KEY_DISPLAY_OPT, getActionBar().getDisplayOptions());
 }
 
 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onRestoreInstanceState(savedInstanceState);
  int savedDisplayOpt = savedInstanceState.getInt(KEY_DISPLAY_OPT);
  if(savedDisplayOpt != 0){
   getActionBar().setDisplayOptions(savedDisplayOpt);
  }
 }

 @Override
 public void onBackStackChanged() {
  int backStackEntryCount = getFragmentManager().getBackStackEntryCount();
  if(backStackEntryCount > 0){
   getActionBar().setDisplayHomeAsUpEnabled(true);
  }else{
   getActionBar().setDisplayHomeAsUpEnabled(false);
  }
 }
 
}


download filesDownload the files.

Google Play content policies have been updated




Google Play content policies have been updated. See "Content Policies" section of Google Play Developer Program Policies, which clarifies that "An app downloaded from Google Play may not modify, replace or update its own APK binary code using any method other than Google Play's update mechanism." Google Play is a trusted source for Android application downloads, and we are committed to providing a secure and consistent experience.

Thursday, April 25, 2013

Start Animation in Activity start

Last exercise show how to "Implement Animation while switching Activity, using overridePendingTransition()", after startActivity() or finish(). If you want run animation when your activity start, you can startAnimation() with your custom Animation in onCreate() or onResume() depends on what you expect.


Modify from last exercise.

Create /res/anim/rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/linear_interpolator">
   <rotate
       android:fromDegrees="0"
       android:toDegrees="360"
       android:pivotX="50%"
       android:pivotY="50%"
       android:duration="1000"
       android:startOffset="0"
       android:repeatCount="1"
       android:repeatMode="reverse" />
</set>


Modify activity_main.xml to add more view to animate.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    android:id="@+id/screen"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
    <Button
        android:id="@+id/switchbutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Switch Activity2" />
    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
    <ImageView
        android:id="@+id/bigicon"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/ic_launcher" />
        
</LinearLayout>


MainActivity.java
package com.example.androidactivityanimation;

import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.app.Activity;
import android.content.Intent;

public class MainActivity extends Activity {
 
 LinearLayout screen;
 Button switchButton;
 ImageView bigIcon;
 ImageView icon;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  
  setContentView(R.layout.activity_main);
  
  screen = (LinearLayout)findViewById(R.id.screen);
  bigIcon = (ImageView)findViewById(R.id.bigicon);
  icon = (ImageView)findViewById(R.id.icon);
  
  switchButton = (Button)findViewById(R.id.switchbutton);
  switchButton.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View arg0) {
    Intent intent = new Intent();
    intent.setClass(MainActivity.this, Activity2.class);
    startActivity(intent);
    overridePendingTransition(R.anim.right_in, R.anim.left_out);
   }});
  
  Animation animRightIn = AnimationUtils.loadAnimation(this, R.anim.right_in);
  Animation animRotateIn_icon = AnimationUtils.loadAnimation(this, R.anim.rotate);
  
  screen.startAnimation(animRightIn);
  icon.startAnimation(animRotateIn_icon);
 }

 @Override
 protected void onResume() {
  super.onResume();
  Animation animRotateIn_big = AnimationUtils.loadAnimation(this, R.anim.rotate);
  bigIcon.startAnimation(animRotateIn_big);
 }

}



download filesDownload the files.

Custom Animation while switching Activity, using overridePendingTransition()

Call overridePendingTransition(int enterAnim, int exitAnim) immediately after startActivity(Intent) or finish() to specify an explicit transition animation to perform next.


Create animation XMLs in /res/anim/ folder.

/res/anim/left_out.xml
<?xml version="1.0" encoding="utf-8"?>  
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:interpolator="@android:anim/linear_interpolator">  
  <translate 
      android:fromXDelta="0" 
      android:toXDelta="-100%p" 
      android:duration="500"/>
</set> 


/res/anim/right_in.xml
<?xml version="1.0" encoding="utf-8"?>  
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:interpolator="@android:anim/linear_interpolator">  
  <translate 
      android:fromXDelta="100%p" 
      android:toXDelta="0" 
      android:duration="500"/>
</set>  


/res/anim/top_out.xml
<?xml version="1.0" encoding="utf-8"?>  
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:interpolator="@android:anim/linear_interpolator">  
  <translate 
      android:fromYDelta="0" 
      android:toYDelta="-100%p" 
      android:duration="500"/>
</set> 


/res/anim/bottom_in.xml
<?xml version="1.0" encoding="utf-8"?>  
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:interpolator="@android:anim/linear_interpolator">  
  <translate 
      android:fromYDelta="100%p" 
      android:toYDelta="0" 
      android:duration="500"/>
</set> 


MainActivity.java
package com.example.androidactivityanimation;

import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.app.Activity;
import android.content.Intent;

public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Button switchButton = (Button)findViewById(R.id.switchbutton);
  switchButton.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View arg0) {
    Intent intent = new Intent();
    intent.setClass(MainActivity.this, Activity2.class);
    startActivity(intent);
    overridePendingTransition(R.anim.right_in, R.anim.left_out);
   }});
 }

}


/res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
    <Button
        android:id="@+id/switchbutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Switch Activity2" />
        

</LinearLayout>


Activity2.java
package com.example.androidactivityanimation;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class Activity2 extends Activity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.layout2);
  
  Button backButton = (Button)findViewById(R.id.backbutton);
  backButton.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View v) {
    finish();
    overridePendingTransition(R.anim.bottom_in, R.anim.top_out);
   }});
 }

}


/res/layout/layout2.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".Activity2" >

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/ic_launcher"/>
    <Button
        android:id="@+id/backbutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Back" />
    
</RelativeLayout>


AndroidManifest.xml have to be modified to add <activity> of Activity2.

download filesDownload the files.

Related:
- Start Animation in Activity start

Wednesday, April 24, 2013

implements OnBackStackChangedListener to display UP icon, automatically by getBackStackEntryCount()

Last exercise handle UP icon for Fragment navigation in hard code; call setDisplayHomeAsUpEnabled(true) when switch to MyDetailFragment, and call setDisplayHomeAsUpEnabled(false) when switch back to MyListFragment(). In case you have many Fragment Transaction, it's not a good approach to display the UP icon.

Alternatively, we can implements OnBackStackChangedListener, add it in FragmentManager by calling addOnBackStackChangedListener(), and override onBackStackChanged() method to check emptiness of back stack entry by calling getBackStackEntryCount(), then call setDisplayHomeAsUpEnabled() accordingly.

implements OnBackStackChangedListener to display UP icon, automatically by getBackStackEntryCount()


package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager.OnBackStackChangedListener;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity implements OnBackStackChangedListener{
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 static boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
   
   String clickedDetail = (String)l.getItemAtPosition(position);
   
   if(isSinglePane == true){
    /*
     * The second fragment not yet loaded. 
     * Load MyDetailFragment by FragmentTransaction, and pass 
     * data from current fragment to second fragment via bundle.
     */
    MyDetailFragment myDetailFragment = new MyDetailFragment();
    Bundle bundle = new Bundle();
    bundle.putString("KEY_DETAIL", clickedDetail);
    myDetailFragment.setArguments(bundle);
    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    
    /*
     * Add this transaction to the back stack. 
     * This means that the transaction will be remembered after it is 
     * committed, and will reverse its operation when later popped off 
     * the stack.
     */
    fragmentTransaction.addToBackStack(null);
    
    fragmentTransaction.commit();
    
    //getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
    
   }else{
    /*
     * Activity have two fragments. Pass data between fragments
     * via reference to fragment
     */
    
    //get reference to MyDetailFragment
    MyDetailFragment myDetailFragment =
      (MyDetailFragment)getFragmentManager().findFragmentById(R.id.detail_fragment);
    myDetailFragment.updateDetail(clickedDetail);
    
   }
  }

 }
 
 public static class MyDetailFragment extends Fragment {
  
  TextView textDetail;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   textDetail = (TextView)view.findViewById(R.id.text_detail);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String detail = bundle.getString("KEY_DETAIL", "no argument pass");
    textDetail.setText(detail);
   }
   
   return view;
  }

  public void updateDetail(String detail) {
   textDetail.setText(detail);
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
  
  getFragmentManager().addOnBackStackChangedListener(this);
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
  case android.R.id.home:
   getFragmentManager().popBackStack();
   //getActionBar().setDisplayHomeAsUpEnabled(false);
            return true;    
  }
  
  return super.onOptionsItemSelected(item);
 }
 
 final static String KEY_DISPLAY_OPT = "KEY_Display_Option";

 @Override
 protected void onSaveInstanceState(Bundle outState) {
  // TODO Auto-generated method stub
  super.onSaveInstanceState(outState);
  outState.putInt(KEY_DISPLAY_OPT, getActionBar().getDisplayOptions());
 }
 
 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onRestoreInstanceState(savedInstanceState);
  int savedDisplayOpt = savedInstanceState.getInt(KEY_DISPLAY_OPT);
  if(savedDisplayOpt != 0){
   getActionBar().setDisplayOptions(savedDisplayOpt);
  }
 }

 @Override
 public void onBackStackChanged() {
  int backStackEntryCount = getFragmentManager().getBackStackEntryCount();
  if(backStackEntryCount > 0){
   getActionBar().setDisplayHomeAsUpEnabled(true);
  }else{
   getActionBar().setDisplayHomeAsUpEnabled(false);
  }
 }
 
}


download filesDownload the files.

Next:
- Implement animation in FragmentTransaction


Tuesday, April 23, 2013

Display UP icon on action bar and implement BACK navigation in Fragment Transaction

Follow the exercise "Allow navigate BACK through FragmentTransaction, by calling addToBackStack()", we are going to add a UP icon on non-home fragment, MyDetailFragment, to navigate BACK in the task history.

UP icon on action bar


  • To display/hide the UP icon, call getActionBar().setDisplayHomeAsUpEnabled(true/false) method.
  • Handle MenuItem of android.R.id.home in onOptionsItemSelected() to detect the UP icon clicked event.
  • To navigate back, call getFragmentManager().popBackStack().
  • Save and restore DisplayOptions of ActionBar in onSaveInstanceState() and onRestoreInstanceState(). Otherwise, the UP icon will disappear after orientation changed.

Modify MainActivity.java in "Allow navigate BACK through FragmentTransaction, by calling addToBackStack()".

package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 static boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
   
   String clickedDetail = (String)l.getItemAtPosition(position);
   
   if(isSinglePane == true){
    /*
     * The second fragment not yet loaded. 
     * Load MyDetailFragment by FragmentTransaction, and pass 
     * data from current fragment to second fragment via bundle.
     */
    MyDetailFragment myDetailFragment = new MyDetailFragment();
    Bundle bundle = new Bundle();
    bundle.putString("KEY_DETAIL", clickedDetail);
    myDetailFragment.setArguments(bundle);
    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    
    /*
     * Add this transaction to the back stack. 
     * This means that the transaction will be remembered after it is 
     * committed, and will reverse its operation when later popped off 
     * the stack.
     */
    fragmentTransaction.addToBackStack(null);
    
    fragmentTransaction.commit();
    
    getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
    
   }else{
    /*
     * Activity have two fragments. Pass data between fragments
     * via reference to fragment
     */
    
    //get reference to MyDetailFragment
    MyDetailFragment myDetailFragment =
      (MyDetailFragment)getFragmentManager().findFragmentById(R.id.detail_fragment);
    myDetailFragment.updateDetail(clickedDetail);
    
   }
  }

 }
 
 public static class MyDetailFragment extends Fragment {
  
  TextView textDetail;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   textDetail = (TextView)view.findViewById(R.id.text_detail);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String detail = bundle.getString("KEY_DETAIL", "no argument pass");
    textDetail.setText(detail);
   }
   
   return view;
  }

  public void updateDetail(String detail) {
   textDetail.setText(detail);
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
  case android.R.id.home:
   getFragmentManager().popBackStack();
   getActionBar().setDisplayHomeAsUpEnabled(false);
            return true;    
  }
  
  return super.onOptionsItemSelected(item);
 }
 
 final static String KEY_DISPLAY_OPT = "KEY_Display_Option";

 @Override
 protected void onSaveInstanceState(Bundle outState) {
  // TODO Auto-generated method stub
  super.onSaveInstanceState(outState);
  outState.putInt(KEY_DISPLAY_OPT, getActionBar().getDisplayOptions());
 }
 
 
 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onRestoreInstanceState(savedInstanceState);
  int savedDisplayOpt = savedInstanceState.getInt(KEY_DISPLAY_OPT);
  if(savedDisplayOpt != 0){
   getActionBar().setDisplayOptions(savedDisplayOpt);
  }
 }

}


download filesDownload the files.

Next:
- implements OnBackStackChangedListener to display UP icon, automatically by getBackStackEntryCount()


Replace Fragment

In this exercise, we are doing Fragment replacement by Java code. There are three button on the main layout to select fragment to display, a container of FrameLayout to load fragments dynamically.

Replace Fragment dynamically


Main layout, activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <Button
            android:id="@+id/button1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Fragment 1"/>
        <Button
            android:id="@+id/button2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Fragment 2"/>
        <Button
            android:id="@+id/button3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Fragment 3"/>
        
    </LinearLayout>
    <FrameLayout 
        android:id="@+id/maincontainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        
    </FrameLayout>

</LinearLayout>


The layouts of the fragments.

fragmentlayout_01.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" 
    android:id="@+id/container1" >

    <TextView
        android:id="@+id/textmsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment 1" />
   <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/ic_launcher" />
        
</LinearLayout>


fragmentlayout_02.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:id="@+id/container2" >

    <TextView
        android:id="@+id/textmsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment 2" />
   <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />
        
</LinearLayout>


fragmentlayout_03.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:id="@+id/container3" >

    <TextView
        android:id="@+id/textmsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment 3" />
   <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="center"
        android:src="@drawable/ic_launcher" />
        
</LinearLayout>


Main code, MainActivity.java
package com.example.androidreplacefragment;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;

public class MainActivity extends FragmentActivity {
 
 public static class MyFragment1 extends Fragment {
  
  TextView textMsg;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.fragmentlayout_01, null);
   textMsg = (TextView)view.findViewById(R.id.textmsg);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String msg = bundle.getString(KEY_MSG_1);
    if(msg != null){
     textMsg.setText(msg);
    }
   }
   
   return view;
  }
  
  public void setMsg(String msg){
   textMsg.setText(msg);
  }
  
 }
 
 public static class MyFragment2 extends Fragment {
  
  TextView textMsg;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.fragmentlayout_02, null);
   textMsg = (TextView)view.findViewById(R.id.textmsg);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String msg = bundle.getString(KEY_MSG_2);
    if(msg != null){
     textMsg.setText(msg);
    }
   }
   
   return view;
  }
  
  public void setMsg(String msg){
   textMsg.setText(msg);
  }
  
 }
 
 public static class MyFragment3 extends Fragment {
  
  TextView textMsg;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.fragmentlayout_03, null);
   textMsg = (TextView)view.findViewById(R.id.textmsg);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String msg = bundle.getString(KEY_MSG_3);
    if(msg != null){
     textMsg.setText(msg);
    }
   }
   
   return view;
  }
  
  public void setMsg(String msg){
   textMsg.setText(msg);
  }
  
 }
 
 FrameLayout container;
 FragmentManager myFragmentManager;
 MyFragment1 myFragment1;
 MyFragment2 myFragment2;
 MyFragment3 myFragment3;
 final static String TAG_1 = "FRAGMENT_1";
 final static String TAG_2 = "FRAGMENT_2";
 final static String TAG_3 = "FRAGMENT_3";
 final static String KEY_MSG_1 = "FRAGMENT1_MSG";
 final static String KEY_MSG_2 = "FRAGMENT2_MSG";
 final static String KEY_MSG_3 = "FRAGMENT3_MSG";

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  container = (FrameLayout)findViewById(R.id.maincontainer);
  
  Button button1 = (Button)findViewById(R.id.button1);
  Button button2 = (Button)findViewById(R.id.button2);
  Button button3 = (Button)findViewById(R.id.button3);
  
  button1.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View arg0) {
    
    MyFragment1 fragment = (MyFragment1)myFragmentManager.findFragmentByTag(TAG_1);
    
    if (fragment == null) {
     
     Bundle bundle = new Bundle();
     bundle.putString(KEY_MSG_1, "Replace MyFragment1");
     myFragment1.setArguments(bundle);
     
     FragmentTransaction fragmentTransaction = myFragmentManager.beginTransaction();
     fragmentTransaction.replace(R.id.maincontainer, myFragment1, TAG_1);
     fragmentTransaction.commit();

    }else{
     
     fragment.setMsg("MyFragment1 already loaded");
    }
   }});
  
  button2.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View arg0) {

    MyFragment2 fragment = (MyFragment2)myFragmentManager.findFragmentByTag(TAG_2);
    
    if (fragment == null) {
     
     Bundle bundle = new Bundle();
     bundle.putString(KEY_MSG_2, "Replace MyFragment2");
     myFragment2.setArguments(bundle);
     
     FragmentTransaction fragmentTransaction = myFragmentManager.beginTransaction();
     fragmentTransaction.replace(R.id.maincontainer, myFragment2, TAG_2);
     fragmentTransaction.commit();

    }else{
     fragment.setMsg("MyFragment2 already loaded");
    }
   }});
  
  button3.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View arg0) {

    MyFragment3 fragment = (MyFragment3)myFragmentManager.findFragmentByTag(TAG_3);
    
    if (fragment == null) {
     
     Bundle bundle = new Bundle();
     bundle.putString(KEY_MSG_3, "Replace MyFragment3");
     myFragment3.setArguments(bundle);
     
     FragmentTransaction fragmentTransaction = myFragmentManager.beginTransaction();
     fragmentTransaction.replace(R.id.maincontainer, myFragment3, TAG_3);
     fragmentTransaction.commit();
     
    }else{
     fragment.setMsg("MyFragment3 already loaded");
    }
   }});
  
  myFragmentManager = getSupportFragmentManager();
  myFragment1 = new MyFragment1();
  myFragment2 = new MyFragment2();
  myFragment3 = new MyFragment3();
  
  if(savedInstanceState == null){
   //if's the first time created
   
   FragmentTransaction fragmentTransaction = myFragmentManager.beginTransaction();
   fragmentTransaction.add(R.id.maincontainer, myFragment1, TAG_1);
   fragmentTransaction.commit();
  }
 }

}


download filesDownload the files.



Related:
- Dual display mode using Fragment step-by-step - one-pane with fragment transaction for phone, two-pane for tablet

Monday, April 22, 2013

Allow navigate BACK through FragmentTransaction, by calling addToBackStack()

If you try last exercise "Handle onListItemClick() of ListFragment, to pass data between fragment" on phone FragmentTransaction, when you BACK in MyDetailFragment, it will exit the application, not back to MyListFragment as common expected. To allow the user to navigate backward through the fragment transactions, you must call addToBackStack() before you commit the FragmentTransaction.


package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 static boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
   
   String clickedDetail = (String)l.getItemAtPosition(position);
   
   if(isSinglePane == true){
    /*
     * The second fragment not yet loaded. 
     * Load MyDetailFragment by FragmentTransaction, and pass 
     * data from current fragment to second fragment via bundle.
     */
    MyDetailFragment myDetailFragment = new MyDetailFragment();
    Bundle bundle = new Bundle();
    bundle.putString("KEY_DETAIL", clickedDetail);
    myDetailFragment.setArguments(bundle);
    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    
    /*
     * Add this transaction to the back stack. 
     * This means that the transaction will be remembered after it is 
     * committed, and will reverse its operation when later popped off 
     * the stack.
     */
    fragmentTransaction.addToBackStack(null);
    
    fragmentTransaction.commit();
    
   }else{
    /*
     * Activity have two fragments. Pass data between fragments
     * via reference to fragment
     */
    
    //get reference to MyDetailFragment
    MyDetailFragment myDetailFragment =
      (MyDetailFragment)getFragmentManager().findFragmentById(R.id.detail_fragment);
    myDetailFragment.updateDetail(clickedDetail);
    
   }
  }

 }
 
 public static class MyDetailFragment extends Fragment {
  
  TextView textDetail;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   textDetail = (TextView)view.findViewById(R.id.text_detail);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String detail = bundle.getString("KEY_DETAIL", "no argument pass");
    textDetail.setText(detail);
   }
   
   return view;
  }

  public void updateDetail(String detail) {
   textDetail.setText(detail);
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
 }

}


Next:
- Display UP icon on action bar and implement BACK navigation in Fragment Transaction


Sunday, April 21, 2013

Handle onListItemClick() of ListFragment, to pass data between fragment

Last exercise "Handle different layout for phone and tablet" demonstrate how to display difference layouts on phone and tablet. Here we are going to handle user click on list item and display the item on the detail fragment.

There are two case in our approach:

- In phone mode with single pane, the target fragment (MyDetailFragment) is not yet displayed. We have to replace the fragment in container with FragmentTransaction, and pass the data via Bundle by calling setArguments(), and retrieve the data in target fragment by calling getArguments() method.

Replace fragment in phone mode


- In tablet mode with two panes, the detail fragment is already displayed. We can call a method in target fragment via FragmentManager and find the target fragment by calling findFragmentById() method.

Pass data between fragment in tablet mode


Modify /res/layout/layout_detailfragment.xml to add a TextView to display the clicked item.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title_detailfragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Detail Fragment"/>
    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    
</LinearLayout>


MainActivity.java
package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 static boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
   
   String clickedDetail = (String)l.getItemAtPosition(position);
   
   if(isSinglePane == true){
    /*
     * The second fragment not yet loaded. 
     * Load MyDetailFragment by FragmentTransaction, and pass 
     * data from current fragment to second fragment via bundle.
     */
    MyDetailFragment myDetailFragment = new MyDetailFragment();
    Bundle bundle = new Bundle();
    bundle.putString("KEY_DETAIL", clickedDetail);
    myDetailFragment.setArguments(bundle);
    FragmentTransaction fragmentTransaction =
      getActivity().getFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.phone_container, myDetailFragment);
    fragmentTransaction.commit();
    
   }else{
    /*
     * Activity have two fragments. Pass data between fragments
     * via reference to fragment
     */
    
    //get reference to MyDetailFragment
    MyDetailFragment myDetailFragment =
      (MyDetailFragment)getFragmentManager().findFragmentById(R.id.detail_fragment);
    myDetailFragment.updateDetail(clickedDetail);
    
   }
  }

 }
 
 public static class MyDetailFragment extends Fragment {
  
  TextView textDetail;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   textDetail = (TextView)view.findViewById(R.id.text_detail);
   
   Bundle bundle = getArguments();
   if(bundle != null){
    String detail = bundle.getString("KEY_DETAIL", "no argument pass");
    textDetail.setText(detail);
   }
   
   return view;
  }

  public void updateDetail(String detail) {
   textDetail.setText(detail);
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
 }

}




download filesDownload the files.

Next:
- Allow navigate BACK through FragmentTransaction, by calling addToBackStack()


Handle different layout for phone and tablet, load fragment in Java code vs XML

Last exercise demonstrate to create /res/layout/activity_main.xml for phone, and /res/layout-sw600dp/activity_main.xml for tablet. We are going to handle the layout separately.

In single pane mode run on phone, We have one pane displayed only. We have to switch fragment using Java code programm programmatically; if define in XML we cannot switch fragment later. We have a FrameLayout with id "phone_container" in layout xml. It will be used to determine if it's run on phone or tablet. We will load MyListFragment in onCreate() programmatically.

/res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Normal" />
    
    <FrameLayout
        android:id="@+id/phone_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </FrameLayout>

</LinearLayout>

Run on phone with a ListFragment


In two pane mode, run on tablet. There are two fragments in layout. The fragments are loaded in XML.

/res/layout-sw600dp/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="sw600dp" />
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <fragment
            android:name="com.example.androiddualmode.MainActivity$MyListFragment"
            android:id="@+id/list_fragment"
            android:layout_height="match_parent"
            android:layout_width="0dp"
            android:layout_weight="1" />
        <fragment
            android:name="com.example.androiddualmode.MainActivity$MyDetailFragment"
            android:id="@+id/detail_fragment"
            android:layout_height="match_parent"
            android:layout_width="0dp"
            android:layout_weight="2" />
        
    </LinearLayout>

</LinearLayout>

Run on tablet with two panes


Create /res/layout/layout_detailfragment.xml, it's a simple layout in the MyDetailFragment.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title_detailfragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Detail Fragment"/>
    
</LinearLayout>


Modify MainActivity.java. Implement inner class of MyListFragment and MyDetailFragment.
package com.example.androiddualmode;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;

public class MainActivity extends Activity {
 
 // if run on phone, isSinglePane = true
 // if run on tablet, isSinglePane = false
 boolean isSinglePane;
 
 static String[] month ={
   "January", "February", "March", "April",
   "May", "June", "July", "August",
   "September", "October", "November", "December"};

 public static class MyListFragment extends ListFragment {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   super.onActivityCreated(savedInstanceState);
   
   ListAdapter myArrayAdapter = 
     new ArrayAdapter<String>(
       getActivity(), android.R.layout.simple_list_item_1, month);
   setListAdapter(myArrayAdapter);
   
  }

 }
 
 public static class MyDetailFragment extends Fragment {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   // TODO Auto-generated method stub
   View view = inflater.inflate(R.layout.layout_detailfragment, null);
   return view;
  }

 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  View v = findViewById(R.id.phone_container);
  if(v == null){
   //it's run on tablet
   isSinglePane = false;
   /*
    * MyListFragment and MyDetailFragment have been loaded in XML,
    * no need load.
    */
   
  }else{
   //it's run on phone
   //Load MyListFragment programmatically
   isSinglePane = true;
   
   if(savedInstanceState == null){
    //if's the first time created
    MyListFragment myListFragment = new MyListFragment();
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.phone_container, myListFragment);
    fragmentTransaction.commit();
   }
  }
 }

}


download filesDownload the files.

Next:
- Handle onListItemClick() of ListFragment, to pass data between fragment


Handle different layout for phone and tablet, in seperated layout folder

For the first generation of tablets running Android 3.0, the proper way to declare tablet layouts was to put them in a directory with the xlarge configuration qualifier (for example, res/layout-xlarge/). In Android 3.2 introduces a new way to specify resources for more discrete screen sizes. The new technique is based on the amount of space your layout needs (such as 600dp of width), rather than trying to make your layout fit the generalized size groups (such as large or xlarge).

For other cases in which you want to further customize your UI to differentiate between sizes such as 7” and 10” tablets, you can define additional smallest width layouts:
  • res/layout/main_activity.xml:
    # For handsets (smaller than 600dp available width)
  • res/layout-sw600dp/main_activity.xml:
    # For 7” tablets (600dp wide and bigger)
  • res/layout-sw720dp/main_activity.xml:
    # For 10” tablets (720dp wide and bigger)
Read more: Supporting Multiple Screens

Exercise:

Create a new Android Application Project with Minimum Required SDK of API 13: Android 3.2 (Honeycomb).



Modify /res/layout/activity_main.xml for normal device.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Normal" />

</LinearLayout>


Run on normal Android devices


Create /res/layout-sw600dp/activity_main.xml to define layout for tablet.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="sw600dp" />

</LinearLayout>

Run on tablet devices




More: Step-by-step to create dual mode app, single-pane for phone/dual-pane for tablet.


Related:
- Replace Fragment dynamically

Thursday, April 18, 2013

Get all the models for a given camera brand, using Flickr API.

Flickr Service provide API of flickr.cameras.getBrandModels, returns all the models for a given camera brand. It's a exercise to show how to access get models of brand "nikon".

Get all the models for a given camera brand, using Flickr API.


package com.example.androidflickrcameras;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.TextView;
import android.app.Activity;

public class MainActivity extends Activity {
 
 /*
  * Example of Flickr API, flickr.cameras.getBrandModels
  * http://api.flickr.com/services/rest/?method=flickr.cameras.getBrandModels&api_key=65d0c67c148bb772766f317c5118f380&brand=Nikon&format=json&nojsoncallback=1
  */
 
 final static String Flickr_KEY = "insert your Flickr API key here";
 final static String subUrl_Flickr = "http://api.flickr.com/services/rest/?";
 final static String subUrl_method_getBrandModels = "flickr.cameras.getBrandModels";
 final static String brand = "nikon";
 final static String url_FlickrApi_getBrandModels
   = subUrl_Flickr
   + "method=" + subUrl_method_getBrandModels
   + "&api_key=" + Flickr_KEY
   + "&brand=" + brand
   + "&format=json&nojsoncallback=1";

 TextView tvResult;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  tvResult = (TextView)findViewById(R.id.result);

  new MyTask().execute();
 }

 private String QueryFlickr(String q){
  String qResult = null;
  HttpClient httpClient = new DefaultHttpClient();
  HttpGet httpGet = new HttpGet(q);
  
  try {
   HttpEntity httpEntity = httpClient.execute(httpGet).getEntity();
   if (httpEntity != null){
    InputStream inputStream = httpEntity.getContent();
    Reader in = new InputStreamReader(inputStream);
    BufferedReader bufferedreader = new BufferedReader(in);
    StringBuilder stringBuilder = new StringBuilder();

    String stringReadLine = null;
    
    while ((stringReadLine = bufferedreader.readLine()) != null) {
     stringBuilder.append(stringReadLine + "\n"); 
    }
    
    qResult = stringBuilder.toString();
   }
   
   
  } catch (ClientProtocolException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return qResult;
 }
 
 private String ParseJSON_getBrandModels(String json){
  String jResult = null;
  
  try {
   JSONObject JsonObject = new JSONObject(json);
   JSONObject Json_cameras = JsonObject.getJSONObject("cameras");
   
   JSONArray JsonArray_camera = Json_cameras.getJSONArray("camera");
  
   jResult = "Brand: " + Json_cameras.getString("brand") + "\n\n";
   for(int i = 0; i < JsonArray_camera.length(); i++){
    JSONObject jsonObjectCamera = JsonArray_camera.getJSONObject(i);
    
    String camera_id = jsonObjectCamera.getString("id");
    
    JSONObject jsonObjectCamera_name = jsonObjectCamera.getJSONObject("name");
    String camera_name_content = jsonObjectCamera_name.getString("_content");
    
    JSONObject jsonObjectCamera_details = jsonObjectCamera.getJSONObject("details");
    String details_megapixels 
     = jsonObjectCamera_details
      .getJSONObject("megapixels").getString("_content");
    String details_lcd_screen_size 
     = jsonObjectCamera_details
      .getJSONObject("lcd_screen_size").getString("_content");
    String details_memory_type 
     = jsonObjectCamera_details
      .getJSONObject("memory_type").getString("_content");

    jResult += "id: " + camera_id + "\n"
      + "name: " + camera_name_content + "\n"
      + "megapixels: " + details_megapixels + "\n"
      + "lcd_screen_size: " + details_lcd_screen_size + "\n"
      + "memory_type: " + details_memory_type + "\n\n";
   }
   
  } catch (JSONException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return jResult;
 }
 
 private class MyTask extends AsyncTask<Void, Void, Void>{
  
  String flickrReturn;
  String parsedFlickrReturn;

  @Override
  protected Void doInBackground(Void... arg0) {
   flickrReturn = QueryFlickr(url_FlickrApi_getBrandModels);
   parsedFlickrReturn = ParseJSON_getBrandModels(flickrReturn);
   return null;
  }

  @Override
  protected void onPostExecute(Void result) {
   if(parsedFlickrReturn == null){
    tvResult.setText("Fail!");
   }else{
    tvResult.setText(parsedFlickrReturn);
   }
  }

 }

}


Layout refer to the exercise "Get all brands of cameras that Flickr knows about, using Flickr API".

Permission of "android.permission.INTERNET" is needed.


download filesDownload the files.