Supportare differenti dimensioni di schermo in android grazie ai Fragments
Nello sviluppo di applicazioni mobili diventa sempre più importante il supporto di schermi e dimensioni anche molto diverse a causa del diffondersi di modelli e tipologie di dispositivi sempre più disparati.
Per fortuna Android offre una soluzione veramente intelligente e semplice per aiutarci a sviluppare applicazioni che da un lato siano compatibili col maggior numero possibile di dispositivi ma che, dall’altro, si adattino al meglio agli schermi dei dispositivi in termini di usabilità e sfruttamento della superficie disponibile .
La soluzione sono i FRAGMENT.
In realtà i Fragment offrono altri grandi vantaggi come, ad esempio, poter scomporre le Activity in blocchi elementari (una sorta di “sub-activity”) replicabili e riutilizzabili in più layout o schermate applicative.
In questo articolo ci focalizzeremo su un semplicissimo esempio tramite il quale sviluppare un’Activity (che chiameremo MainActivity) in grado di presentarsi all’utente con due fragment affiancati nel caso lo schermo del dispositivo sia abbastanza grande da consentirlo (es. tablet), mentre negli altri casi gli stessi fragment vengono mostrati sequenzialmente (quasi come se fossero due Activity distinte).
L’ipotesi è quella di avere una listView di oggetti nel primo fragment (SxFragment) e il dettaglio dell’oggetto selezionato nel secondo fragment (DxFragment).
La figura seguente mostra l’esempio.
1) Layout
Predisponiamo due differenti layout per schermi piccoli e grandi. La stessa cosa, ad esempio, può essere fatta per Portrait/Landscape, con più fragmen, etc…
file: /res/layout-large/example_layout.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.fragments.SxFragment" android:id="@+id/fgmList" android:layout_weight="3" android:layout_width="0dp" android:layout_height="match_parent" /> <fragment android:name="com.example.fragments.DxFragment" android:id="@+id/fgmDetail" android:layout_weight="4" android:layout_width="0dp" android:layout_height="match_parent" /> </LinearLayout> |
file: /res/layout/example_layout.xml
1 2 3 4 5 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fgmContainer" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> |
Da notare che nel secondo layout non sono inclusi staticamente i Fragment e che è invece assegnato un id (fgmContainer) al contenitore che ci occorrerà per agganciare i fragment.
2) Activity principale
1 |
public class MainActivity extends Activity |
oppure nel caso di utilizzo della Android Support Library (retrocompatibilità con device che hanno versioni di android < 3.0):
1 |
public class MainActivity extends FragmentActivity |
Nel onCreate includiamo il layout e istanziamo dinamicamente il Fragment nel caso in cui siamo alle prese con uno schermo piccolo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_contatti); // Check if using the layout version for small screen if (findViewById(R.id.fgmContattiContainer) != null) { // If restored from previous state do not add fragment if (savedInstanceState != null) return; // Dynamically add list fragment SxFragment firstFragment = new SxFragment(); // Eventually pass parameters from intent firstFragment.setArguments(getIntent().getExtras()); // In case of using the android support library we must use the getSupportFragmentManager() method, // otherwise we use getFragmentManager() method. // The third parameter is an arbitrary TAG assigned to Fragment. We need to do that for being able // to find the corresponding fragment afterwards getSupportFragmentManager().beginTransaction() .add(R.id.fgmContainer, firstFragment, SxFragment.class.getName()).commit(); } } |
3) Fragment A (items list)
Nel fragment di sinistra, contenente la listView, assegnamo all’evento di selezione (OnItemClick) il compito di notificare la selezione tramite un metodo di callback (onItemSelected) all’attività principale.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public class SxFragment extends Fragment ... MainActivity parent; // Having the container activity can be useful to passing messages and parameters List<Item> items; // Objects in listView ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreate(savedInstanceState); View view = inflater.inflate(R.layout.my_list_layout, container, false); parent = (MainActivity) getActivity(); // Get out listView and set onItemListClick listener in order to update the detail Fragment B myList = (ListView) view.findViewById(R.id.myList); myList.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { try { // We can create an interface for calling activities (MainActivity) in order to // be sure to find the expected update method OnItemListSelectedListener callback = (OnItemListSelectedListener)a; Item c = items.get(arg2); // Pass new item selected to MainActivity callback.onItemSelected(c); } catch (ClassCastException e) { throw new ClassCastException(a.toString() + " must implement OnItemListSelectedListener"); } } }); … // MainActivity must implement this interface public interface OnItemListSelectedListener { public void onItemSelected(Contatto c); } } |
4) Activity principale
Aggiungo il metodo di selezione nella MainActivity. Chiaramente il metodo discrimina il caso in cui abbiamo entrambi i fragment da quello in cui occorra istanziare il secondo dinamicamente. Nel primo caso è sufficiente richiamare il metodo di update esposto dal fragment (doItemSelection), mentre nel secondo caso occorre effettuare una transizione dinamica tra i due Fragment. Il primo dei due, viene sostituito tramite “replace”, ma inserito nel Back Stack in modo che premendo il pulsante “Back” non termini l’activity ma venga semplicemente effettuata la transizione al contario e riappaia il primo Fragment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class MainActivity extends FragmentActivity implements SxFragment.OnItemListSelectedListener ... Item selectedItem; ... @Override public void onItemSelected(Item item) { // The user selected the headline of an article from the HeadlinesFragment // We verify if large screen layout is used or not by checking the presence // of the detail Fragment DxFragment itemDetailFrag = (DxFragment) getSupportFragmentManager().findFragmentById(R.id.fgmDetail); // It's better to maintain shared information in the MainActivity. // In this case we keep the item selected in the selectedItem class variable selectedItem = item; if (itemDetailFrag != null) { // if detail fragment exists we simple need to update it itemDetailFrag.doItemSelection(selectedItem.getId().toString()); } else { // Otherwise, swap fragments DxFragment newFragment = new DxFragment(); // Pass the necessary parameters (in this case the selectedItem) Bundle args = new Bundle(); args.putString(DxFragment.KEY_ITEM_SELECTED, selectedItem.getId().toString()); newFragment.setArguments(args); // Create transaction FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); // Replace the fragment and specify the transaction mode //note the arbitrary TAG passed in order to find the fragment afterwards //ID is not fixed because we are creating the fragment at runtime transaction.replace(R.id.fgmContainer, newFragment, DxFragment.class.getName()); transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); //add the transaction to the back stack so the user can navigate back transaction.addToBackStack(null); // Commit the transaction transaction.commit(); } } } |
5) Fragment B (item details)
Il Fragment B, infine, riceve come argomenti i parametri necessari alla sua inizializzazione o, se già inizializzato, espone i metodi necessari all’aggiornamento da parte della MainActivity. Nell’esempio semplicemente l’id dell’item selezionato viene inserito in una TextView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class DxFragment extends Fragment { public static String KEY_ITEM_SELECTED = "selectedItemKey"; ... TextView itemIdTxt; String id; MainActivity parent; ... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { view = inflater.inflate(R.layout.my_detail_layout, container, false); parent = (MainActivity) getActivity(); // Get textView from layout in order to show selected item's id itemIdTxt = (TextView)view.findViewById(R.id.itemIdTxt); // Get arguments containing the ID of selected item if(getArguments() != null) id = getArguments().getString(KEY_ITEM_SELECTED); //initialize the item doItemSelection(id); return view; } //update method public void doItemSelection(String id) { this.id = id; itemIdTxt.setText(id); } } |
Nei prossimi articoli vedremo come effettuare la gestione di Menu e Actionbar nel caso di utilizzo dei Fragment.
Ciao e a presto
Federico