1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.annotation.SuppressLint;
17import android.os.Bundle;
18import android.support.v17.leanback.transition.TransitionHelper;
19import android.support.v17.leanback.transition.TransitionListener;
20import android.support.v17.leanback.util.StateMachine;
21import android.support.v17.leanback.util.StateMachine.Condition;
22import android.support.v17.leanback.util.StateMachine.Event;
23import android.support.v17.leanback.util.StateMachine.State;
24import android.view.View;
25import android.view.ViewTreeObserver;
26
27/**
28 * Base class for leanback Fragments. This class is not intended to be subclassed by apps.
29 */
30@SuppressWarnings("FragmentNotInstantiable")
31public class BaseSupportFragment extends BrandedSupportFragment {
32
33    /**
34     * The start state for all
35     */
36    final State STATE_START = new State("START", true, false);
37
38    /**
39     * Initial State for ENTRNACE transition.
40     */
41    final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
42
43    /**
44     * prepareEntranceTransition is just called, but view not ready yet. We can enable the
45     * busy spinner.
46     */
47    final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
48        @Override
49        public void run() {
50            mProgressBarManager.show();
51        }
52    };
53
54    /**
55     * prepareEntranceTransition is called and main content view to slide in was created, so we can
56     * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
57     * in this State, the process is very different in subclass, e.g. BrowseSupportFragment hide header
58     * views and hide main fragment view in two steps.
59     */
60    final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
61            "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
62        @Override
63        public void run() {
64            onEntranceTransitionPrepare();
65        }
66    };
67
68    /**
69     * execute the entrance transition.
70     */
71    final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
72        @Override
73        public void run() {
74            mProgressBarManager.hide();
75            onExecuteEntranceTransition();
76        }
77    };
78
79    /**
80     * execute onEntranceTransitionEnd.
81     */
82    final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
83        @Override
84        public void run() {
85            onEntranceTransitionEnd();
86        }
87    };
88
89    /**
90     * either entrance transition completed or skipped
91     */
92    final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
93
94    /**
95     * Event fragment.onCreate()
96     */
97    final Event EVT_ON_CREATE = new Event("onCreate");
98
99    /**
100     * Event fragment.onViewCreated()
101     */
102    final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
103
104    /**
105     * Event for {@link #prepareEntranceTransition()} is called.
106     */
107    final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
108
109    /**
110     * Event for {@link #startEntranceTransition()} is called.
111     */
112    final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
113
114    /**
115     * Event for entrance transition is ended through Transition listener.
116     */
117    final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
118
119    /**
120     * Event for skipping entrance transition if not supported.
121     */
122    final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
123        @Override
124        public boolean canProceed() {
125            return !TransitionHelper.systemSupportsEntranceTransitions();
126        }
127    };
128
129    final StateMachine mStateMachine = new StateMachine();
130
131    Object mEntranceTransition;
132    final ProgressBarManager mProgressBarManager = new ProgressBarManager();
133
134    @SuppressLint("ValidFragment")
135    BaseSupportFragment() {
136    }
137
138    @Override
139    public void onCreate(Bundle savedInstanceState) {
140        createStateMachineStates();
141        createStateMachineTransitions();
142        mStateMachine.start();
143        super.onCreate(savedInstanceState);
144        mStateMachine.fireEvent(EVT_ON_CREATE);
145    }
146
147    void createStateMachineStates() {
148        mStateMachine.addState(STATE_START);
149        mStateMachine.addState(STATE_ENTRANCE_INIT);
150        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
151        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
152        mStateMachine.addState(STATE_ENTRANCE_PERFORM);
153        mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
154        mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
155    }
156
157    void createStateMachineTransitions() {
158        mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
159        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
160                COND_TRANSITION_NOT_SUPPORTED);
161        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
162                EVT_ON_CREATEVIEW);
163        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
164                EVT_PREPARE_ENTRANCE);
165        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
166                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
167                EVT_ON_CREATEVIEW);
168        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
169                STATE_ENTRANCE_PERFORM,
170                EVT_START_ENTRANCE);
171        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
172                STATE_ENTRANCE_PERFORM);
173        mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
174                STATE_ENTRANCE_ON_ENDED,
175                EVT_ENTRANCE_END);
176        mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
177    }
178
179    @Override
180    public void onViewCreated(View view, Bundle savedInstanceState) {
181        super.onViewCreated(view, savedInstanceState);
182        mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
183    }
184
185    /**
186     * Enables entrance transition.<p>
187     * Entrance transition is the standard slide-in transition that shows rows of data in
188     * browse screen and details screen.
189     * <p>
190     * The method is ignored before LOLLIPOP (API21).
191     * <p>
192     * This method must be called in or
193     * before onCreate().  Typically entrance transition should be enabled when savedInstance is
194     * null so that fragment restored from instanceState does not run an extra entrance transition.
195     * When the entrance transition is enabled, the fragment will make headers and content
196     * hidden initially.
197     * When data of rows are ready, app must call {@link #startEntranceTransition()} to kick off
198     * the transition, otherwise the rows will be invisible forever.
199     * <p>
200     * It is similar to android:windowsEnterTransition and can be considered a late-executed
201     * android:windowsEnterTransition controlled by app.  There are two reasons that app needs it:
202     * <li> Workaround the problem that activity transition is not available between launcher and
203     * app.  Browse activity must programmatically start the slide-in transition.</li>
204     * <li> Separates DetailsOverviewRow transition from other rows transition.  So that
205     * the DetailsOverviewRow transition can be executed earlier without waiting for all rows
206     * to be loaded.</li>
207     * <p>
208     * Transition object is returned by createEntranceTransition().  Typically the app does not need
209     * override the default transition that browse and details provides.
210     */
211    public void prepareEntranceTransition() {
212        mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
213    }
214
215    /**
216     * Create entrance transition.  Subclass can override to load transition from
217     * resource or construct manually.  Typically app does not need to
218     * override the default transition that browse and details provides.
219     */
220    protected Object createEntranceTransition() {
221        return null;
222    }
223
224    /**
225     * Run entrance transition.  Subclass may use TransitionManager to perform
226     * go(Scene) or beginDelayedTransition().  App should not override the default
227     * implementation of browse and details fragment.
228     */
229    protected void runEntranceTransition(Object entranceTransition) {
230    }
231
232    /**
233     * Callback when entrance transition is prepared.  This is when fragment should
234     * stop user input and animations.
235     */
236    protected void onEntranceTransitionPrepare() {
237    }
238
239    /**
240     * Callback when entrance transition is started.  This is when fragment should
241     * stop processing layout.
242     */
243    protected void onEntranceTransitionStart() {
244    }
245
246    /**
247     * Callback when entrance transition is ended.
248     */
249    protected void onEntranceTransitionEnd() {
250    }
251
252    /**
253     * When fragment finishes loading data, it should call startEntranceTransition()
254     * to execute the entrance transition.
255     * startEntranceTransition() will start transition only if both two conditions
256     * are satisfied:
257     * <li> prepareEntranceTransition() was called.</li>
258     * <li> has not executed entrance transition yet.</li>
259     * <p>
260     * If startEntranceTransition() is called before onViewCreated(), it will be pending
261     * and executed when view is created.
262     */
263    public void startEntranceTransition() {
264        mStateMachine.fireEvent(EVT_START_ENTRANCE);
265    }
266
267    void onExecuteEntranceTransition() {
268        // wait till views get their initial position before start transition
269        final View view = getView();
270        if (view == null) {
271            // fragment view destroyed, transition not needed
272            return;
273        }
274        view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
275            @Override
276            public boolean onPreDraw() {
277                view.getViewTreeObserver().removeOnPreDrawListener(this);
278                if (getContext() == null || getView() == null) {
279                    // bail out if fragment is destroyed immediately after startEntranceTransition
280                    return true;
281                }
282                internalCreateEntranceTransition();
283                onEntranceTransitionStart();
284                if (mEntranceTransition != null) {
285                    runEntranceTransition(mEntranceTransition);
286                } else {
287                    mStateMachine.fireEvent(EVT_ENTRANCE_END);
288                }
289                return false;
290            }
291        });
292        view.invalidate();
293    }
294
295    void internalCreateEntranceTransition() {
296        mEntranceTransition = createEntranceTransition();
297        if (mEntranceTransition == null) {
298            return;
299        }
300        TransitionHelper.addTransitionListener(mEntranceTransition, new TransitionListener() {
301            @Override
302            public void onTransitionEnd(Object transition) {
303                mEntranceTransition = null;
304                mStateMachine.fireEvent(EVT_ENTRANCE_END);
305            }
306        });
307    }
308
309    /**
310     * Returns the {@link ProgressBarManager}.
311     * @return The {@link ProgressBarManager}.
312     */
313    public final ProgressBarManager getProgressBarManager() {
314        return mProgressBarManager;
315    }
316}
317