Java tutorial
/* * Copyright (C) 2013 Dario Scoppelletti, <http://www.scoppelletti.it/>. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package it.scoppelletti.mobilepower.app; import java.util.*; import android.app.*; import android.os.*; import android.support.v4.app.*; import org.apache.commons.lang3.*; import org.slf4j.*; import it.scoppelletti.mobilepower.os.*; /** * Controllore dei frammenti di dettaglio. * * <P>Se cambia la configurazione del dispositivo, ad esempio * l’orientamento, e quindi il layout, Android si limita a visualizzare i * soli frammenti inseriti in pannelli ancora esistenti nel nuovo layout mentre * gli altri frammenti rimangono invisibili; anche il back stack resta * invariato, quindi la pressione del bottone back potrebbe, ad esempio, * rimuovere uno dei frammenti invisibili senza alcuna evidenza dal punto di * vista dell’interfaccia utente.<BR> * La classe {@code FragmentLayoutController}, al ripristino dell’istanza * dell’attività, annulla le transazioni nel back stack fino alla * configurazione dei frammenti dell’avvio dell’applicazione e poi * reinserisce le stesse transazioni nel back stack riproducendo le * configurazioni dei frammenti come se le transazioni fossero state * originariamente eseguite con la configurazione del dispositivo corrente.<BR> * La classe {@code FragmentLayoutController} assume che l’applicazione * implementi le transazioni sui frammenti con una logica di pannelli nel layout * organizzati in una gerarchia di livelli di dettaglio nei quali ogni frammento * è inserito nei pannelli disponibili con logica * <ACRONYM TITLE="First In-First Out">FIFO</ACRONYM>.</P> * * @since 1.0 */ public final class FragmentLayoutController { /** * Stato * {@code it.scoppelletti.mobilepower.app.FragmentLayoutController.panelCount}: * Numero di pannelli a disposizione per i frammenti di dettaglio. */ public static final String STATE_PANELCOUNT = "it.scoppelletti.mobilepower.app.FragmentLayoutController.panelCount"; private static final Logger myLogger = LoggerFactory.getLogger("FragmentLayout"); private final Activity myActivity; private final int myFrameCount; private final int[] myFrameIds; /** * Costruttore. * * <P>L’attività istanzia un oggetto * {@code FragmentLayoutController} all’interno del proprio metodo * {@code onCreate} dopo aver costruito la propria vista.</P> * * @param activity Attività. * @param frameIds Identificatori dei pannelli a disposizione per i * frammenti di dettaglio. */ public FragmentLayoutController(Activity activity, int... frameIds) { int frameCount; if (activity == null) { throw new NullPointerException("Argument activity is null."); } if (ArrayUtils.isEmpty(frameIds)) { throw new NullPointerException("Argument frameIds is null."); } myActivity = activity; frameCount = 0; myFrameIds = Arrays.copyOf(frameIds, frameIds.length); for (int frameId : myFrameIds) { if (myActivity.findViewById(frameId) != null) { frameCount++; } } if (frameCount == 0) { throw new IllegalStateException("No panel found."); } myFrameCount = frameCount; } /** * Restituisce il numero di pannelli a disposizione per i frammenti di * dettaglio. * * @return Valore. */ public int getFrameCount() { return myFrameCount; } /** * Salva lo stato dell’istanza. * * <P>L’attività salva lo stato dell’istanza * {@code FragmentLayoutController} all’interno del proprio metodo * {@code onSaveInstanceState}.</P> * * @param outState Stato da salvare. */ public void onSaveInstanceState(Bundle outState) { if (outState == null) { throw new NullPointerException("Argument outState is null."); } outState.putInt(FragmentLayoutController.STATE_PANELCOUNT, myFrameCount); } /** * Ripristina lo stato dell’istanza. * * <P>L’attività ripristina lo stato dell’istanza * {@code FragmentLayoutController} all’interno del proprio metodo * {@code onRestoreInstanceState}.</P> * * @param savedInstanceState Stato dell’istanza. * @param fragmentCollector Collettore dei frammenti di dettaglio. */ public void onRestoreInstanceState(Bundle savedInstanceState, FragmentLayoutController.FragmentCollector fragmentCollector) { int n, oldPanelCount, tnId; String tag; ActivitySupport activitySupport; FragmentSupport fragment; FragmentManager fragmentMgr; FragmentLayoutController.BackStackChangedListener backStackListener; Queue<FragmentSupport> fragmentQueue; Queue<FragmentLayoutController.FragmentEntry> clonedQueue; if (savedInstanceState == null) { throw new NullPointerException("Argument savedInstanceState is null."); } if (fragmentCollector == null) { throw new NullPointerException("Argument fragmentCollector is null."); } if (!(myActivity instanceof ActivitySupport)) { myLogger.warn("Activity not implement interface ActivitySupport."); return; } oldPanelCount = savedInstanceState.getInt(FragmentLayoutController.STATE_PANELCOUNT, 0); if (oldPanelCount < 1) { myLogger.warn("Unexpected {}={} in saved instance state.", FragmentLayoutController.STATE_PANELCOUNT, oldPanelCount); return; } myLogger.debug("{}: current={}, saved instance state={}.", new Object[] { FragmentLayoutController.STATE_PANELCOUNT, myFrameCount, oldPanelCount }); if (oldPanelCount == myFrameCount) { // Il numero di pannelli non e' cambiato: // Il sistema ha gia' ripristinato correttamente i frammenti. return; } fragmentQueue = new ArrayDeque<FragmentSupport>(); fragmentCollector.collectFragments(fragmentQueue); // Ad ogni frammento associo il tag con il quale è stato // inserito clonedQueue = new ArrayDeque<FragmentLayoutController.FragmentEntry>(); while (!fragmentQueue.isEmpty()) { fragment = fragmentQueue.remove(); if (fragment == null) { myLogger.warn("Ignoring null."); continue; } tag = fragment.asFragment().getTag(); if (StringUtils.isBlank(tag)) { myLogger.warn("Ignoring fragment with empty tag."); continue; } clonedQueue.offer(new FragmentLayoutController.FragmentEntry(fragment.cloneFragment(), tag)); } fragmentQueue = null; // free memory activitySupport = (ActivitySupport) myActivity; fragmentMgr = activitySupport.getSupportFragmentManager(); // Ripristino la configurazione dei frammenti iniziale for (n = fragmentMgr.getBackStackEntryCount(); n > 0; n--) { fragmentMgr.popBackStack(); } if (myFrameCount > 1) { tnId = arrangeFragments(fragmentMgr, clonedQueue); } else { tnId = arrangePanel(fragmentMgr, clonedQueue); } if (Build.VERSION.SDK_INT < BuildCompat.VERSION_CODES.HONEYCOMB) { return; } // - Android 4.1.2 // La barra delle azioni non e' correttamente aggiornata forse perche' // si assume che non ce ne sia bisogno con transazioni schedulate // durante il ripristino dell'attivita' (o magari perche' non e' proprio // previsto che si schedulino transazioni durante il ripristino // dell'attivita'): // Visto che l'esecuzione delle transazioni e' asincrona, devo // utilizzare un gestore degli eventi di modifica del back stack che // gestisca l’ultima transazione che ho schedulato. backStackListener = new FragmentLayoutController.BackStackChangedListener(myActivity, fragmentMgr, tnId); fragmentMgr.addOnBackStackChangedListener(backStackListener); } /** * Ricostruisce la successione dei frammenti nell’unico pannello. * * @param fragmentMgr Gestore dei frammenti. * @param fragmentQueue Frammenti. * @return Identificatore dell’ultimo elemento inserito * nel back stack. */ private int arrangePanel(FragmentManager fragmentMgr, Queue<FragmentLayoutController.FragmentEntry> fragmentQueue) { int tnId, lastTnId; FragmentLayoutController.FragmentEntry entry; FragmentTransaction fragmentTn = null; lastTnId = -1; while (!fragmentQueue.isEmpty()) { tnId = -1; entry = fragmentQueue.remove(); try { fragmentTn = fragmentMgr.beginTransaction(); fragmentTn.replace(myFrameIds[0], entry.getFragment().asFragment(), entry.getTag()); fragmentTn.addToBackStack(null); } finally { if (fragmentTn != null) { tnId = fragmentTn.commit(); fragmentTn = null; } } if (tnId >= 0) { lastTnId = tnId; } } return lastTnId; } /** * Ricostruisce la successione della disposizione dei frammenti nei * pannelli. * * @param fragmentMgr Gestore dei frammenti. * @param fragmentQueue Frammenti. * @return Identificatore dell’ultimo elemento inserito * nel back stack. */ private int arrangeFragments(FragmentManager fragmentMgr, Queue<FragmentLayoutController.FragmentEntry> fragmentQueue) { int i; int frameCount, tnId, lastTnId; FragmentLayoutController.FragmentEntry entry; FragmentSupport newFragment, oldFragment; FragmentLayoutController.FragmentEntry[] frames; FragmentTransaction fragmentTn = null; frameCount = 1; frames = new FragmentLayoutController.FragmentEntry[myFrameCount]; Arrays.fill(frames, null); lastTnId = -1; while (!fragmentQueue.isEmpty()) { tnId = -1; entry = fragmentQueue.remove(); try { fragmentTn = fragmentMgr.beginTransaction(); if (frameCount == myFrameCount) { // Tutti i pannelli sono occupati: // Sposto ogni frammento nel pannello precedente per // liberare l'ultimo. for (i = 0; i < frameCount; i++) { if (frames[i] == null) { // Inizialmente il primo pannello risulta vuoto // anche se in realta' e' occupato dal frammento // principale (non di dettaglio). continue; } oldFragment = frames[i].getFragment(); newFragment = (i > 0) ? oldFragment.cloneFragment() : null; fragmentTn.remove(oldFragment.asFragment()); frames[i] = null; if (newFragment != null) { fragmentTn.replace(myFrameIds[i - 1], newFragment.asFragment(), entry.getTag()); frames[i - 1] = new FragmentLayoutController.FragmentEntry(newFragment, entry.getTag()); } } frameCount--; } fragmentTn.add(myFrameIds[frameCount], entry.getFragment().asFragment(), entry.getTag()); frames[frameCount++] = entry; fragmentTn.addToBackStack(null); } finally { if (fragmentTn != null) { tnId = fragmentTn.commit(); fragmentTn = null; } } if (tnId >= 0) { lastTnId = tnId; } } return lastTnId; } /** * Collettore di frammenti di dettaglio. * * @since 1.0.0 */ public interface FragmentCollector { /** * Accoda i frammenti di dettaglio da ridisporre nei pannelli a * disposizione. * * @param queue Coda. */ void collectFragments(Queue<FragmentSupport> queue); } /** * Frammento da reinserire. */ private static final class FragmentEntry { private final FragmentSupport myFragment; private final String myTag; /** * Costruttore. * * @param fragment Frammento. * @param tag Tag. */ FragmentEntry(FragmentSupport fragment, String tag) { myFragment = fragment; myTag = tag; } /** * Restituisce il frammento. * * @return Oggetto. */ FragmentSupport getFragment() { return myFragment; } /** * Restituisce il tag. * * @return Valore. */ String getTag() { return myTag; } } /** * Gestore della modifica del back-stack. */ private static final class BackStackChangedListener implements FragmentManager.OnBackStackChangedListener, Runnable { private static final Logger myLogger = LoggerFactory.getLogger("FragmentLayout"); private final Activity myActivity; private final FragmentManager myFragmentMgr; private final int myWaitingForEntryId; private final Handler myHandler; /** * Costruttore. * * @param activity Attività. * @param fragmentMgr Gestore dei frammenti. * @param waitingForEntryId Identificatore dell’elemento atteso. */ BackStackChangedListener(Activity activity, FragmentManager fragmentMgr, int waitingForEntryId) { myActivity = activity; myFragmentMgr = fragmentMgr; myWaitingForEntryId = waitingForEntryId; myHandler = new Handler(); myLogger.trace("Waiting for back stack entry {}.", myWaitingForEntryId); } /** * Gestisce la modifica del back-stack. */ public void onBackStackChanged() { int n, tnId; FragmentManager.BackStackEntry entry; n = myFragmentMgr.getBackStackEntryCount(); if (n == 0) { myLogger.trace("Back stack is empty."); return; } entry = myFragmentMgr.getBackStackEntryAt(n - 1); tnId = entry.getId(); myLogger.trace("Back stack entry {} intercepted.", tnId); if (tnId == myWaitingForEntryId) { myFragmentMgr.removeOnBackStackChangedListener(this); myHandler.post(this); } } /** * Esegue l’operazione. */ public void run() { ActivityCompat.invalidateOptionsMenu(myActivity); } } }