Java tutorial
// Copyright 2015 The Vanadium Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package io.v.syncslides; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import io.v.syncslides.db.DB; import io.v.syncslides.model.DynamicList; import io.v.syncslides.model.ListListener; import io.v.syncslides.model.Session; import io.v.syncslides.model.Slide; import io.v.v23.verror.VException; /** * Provides both the presenter and audience views for navigating through a presentation. * Instantiated by the PresentationActivity along with other views/fragments of the presentation * to make transitions between them seamless. */ public class NavigateFragment extends Fragment { private static final String TAG = "NavigateFragment"; private static final String SESSION_ID_KEY = "session_id_key"; private Session mSession; private int mSlideNum = 0; private SlideNumberListener mSlideNumberListener = new SlideNumberListener(); private ListListener mSlideListListener = new SlideListListener(); private ImageView mPrevThumb; private ImageView mNextThumb; private ImageView mCurrentSlide; private TextView mSlideNumText; private EditText mNotes; private boolean mEditing; private DynamicList<Slide> mSlides; public static NavigateFragment newInstance(String sessionId) { NavigateFragment fragment = new NavigateFragment(); Bundle args = new Bundle(); args.putString(SESSION_ID_KEY, sessionId); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // When editing notes, display a menu with "Save". setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle bundle = savedInstanceState; if (bundle == null) { bundle = getArguments(); } String sessionId = bundle.getString(SESSION_ID_KEY); try { mSession = DB.Singleton.get().getSession(sessionId); } catch (VException e) { handleFatalError("Failed to fetch Session", e); } final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false); // mFabSync = rootView.findViewById(R.id.audience_sync_fab); // if (((PresentationActivity) getActivity()).getSynced() || mRole != Role.AUDIENCE) { // mFabSync.setVisibility(View.INVISIBLE); // } else { // mFabSync.setVisibility(View.VISIBLE); // } // // mFabSync.setOnClickListener(new NavigateClickListener() { // @Override // public void onClick(View v) { // super.onClick(v); // sync(); // mFabSync.setVisibility(View.INVISIBLE); // } // }); View.OnClickListener previousSlideListener = new NavigateClickListener() { @Override void onNavigate() { previousSlide(); } }; View arrowBack = rootView.findViewById(R.id.arrow_back); arrowBack.setOnClickListener(previousSlideListener); mPrevThumb = (ImageView) rootView.findViewById(R.id.prev_thumb); mPrevThumb.setOnClickListener(previousSlideListener); View.OnClickListener nextSlideListener = new NavigateClickListener() { @Override void onNavigate() { nextSlide(); } }; // Show either the arrowForward or the FAB but not both. View arrowForward = rootView.findViewById(R.id.arrow_forward); View fabForward = rootView.findViewById(R.id.primary_navigation_fab); // if (mRole == Role.PRESENTER) { // arrowForward.setVisibility(View.INVISIBLE); // fabForward.setOnClickListener(nextSlideListener); // } else { fabForward.setVisibility(View.INVISIBLE); arrowForward.setOnClickListener(nextSlideListener); // } mNextThumb = (ImageView) rootView.findViewById(R.id.next_thumb); mNextThumb.setOnClickListener(nextSlideListener); // mQuestions = (ImageView) rootView.findViewById(R.id.questions); // // TODO(kash): Hide the mQuestions button if mRole == BROWSER. // mQuestions.setOnClickListener(new NavigateClickListener() { // @Override // public void onClick(View v) { // super.onClick(v); // questionButton(); // } // }); mCurrentSlide = (ImageView) rootView.findViewById(R.id.slide_current_medium); mCurrentSlide.setOnClickListener(new NavigateClickListener() { @Override public void onNavigate() { // TODO(kash): Disallow presenter from switching to fullscreen. ((PresentationActivity) getActivity()).showFullscreenSlide(); } }); mSlideNumText = (TextView) rootView.findViewById(R.id.slide_num_text); mNotes = (EditText) rootView.findViewById(R.id.notes); mNotes.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) { ((PresentationActivity) getActivity()).getSupportActionBar().show(); mEditing = true; getActivity().invalidateOptionsMenu(); // We don't want the presentation to advance while the user // is editing the notes. Force the app to stay on this slide. try { mSession.setLocalSlideNum(mSlideNum); } catch (VException e) { handleFatalError("Could not set local slide num", e); } } }); // The parent of mNotes needs to be focusable in order to clear focus // from mNotes when done editing. We set the attributes in code rather // than in XML because it is too easy to add an extra level of layout // in XML and forget to add these attributes. ViewGroup parent = (ViewGroup) mNotes.getParent(); parent.setFocusable(true); parent.setClickable(true); parent.setFocusableInTouchMode(true); // View slideListIcon = rootView.findViewById(R.id.slide_list); // slideListIcon.setOnClickListener(new NavigateClickListener() { // @Override // public void onClick(View v) { // super.onClick(v); // if (mRole == Role.AUDIENCE) { // ((PresentationActivity) getActivity()).showSlideList(); // } else { // getActivity().getSupportFragmentManager().popBackStack(); // } // } // }); // mQuestionsNum = (TextView) rootView.findViewById(R.id.questions_num); // // Start off invisible for everyone. If there are questions, this // // will be set to visible in the mDB.getQuestionerList() callback. // mQuestionsNum.setVisibility(View.INVISIBLE); // // mDB = DB.Singleton.get(getActivity().getApplicationContext()); // mDB.getSlides(mDeckId, new DB.Callback<List<Slide>>() { // @Override // public void done(List<Slide> slides) { // mSlides = slides; // // The CurrentSlideListener could have been notified while we were waiting for // // the slides to load. // if (mLoadingCurrentSlide != -1) { // currentSlideChanged(mLoadingCurrentSlide); // } // updateView(); // } // }); // if (((PresentationActivity) getActivity()).getSynced()) { // sync(); // } else { // unsync(); // } return rootView; } @Override public void onStart() { super.onStart(); ((PresentationActivity) getActivity()).setUiImmersive(true); mSlides = mSession.getSlides(); mSlides.addListener(mSlideListListener); mSession.addSlideNumberListener(mSlideNumberListener); } @Override public void onStop() { super.onStop(); ((PresentationActivity) getActivity()).setUiImmersive(false); mSession.removeSlideNumberListener(mSlideNumberListener); mSlides.removeListener(mSlideListListener); mSlides = null; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(SESSION_ID_KEY, mSession.getId()); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (mEditing) { inflater.inflate(R.menu.edit_notes, menu); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_save: saveNotes(); return true; } return false; } /** * Advances to the next slide, if there is one, and updates the UI. */ private void nextSlide() { if (mSlideNum == -1) { // Wait until the state has loaded before letting the user move around. return; } if (mSlideNum < mSession.getSlides().getItemCount() - 1) { try { mSession.setLocalSlideNum(mSlideNum + 1); } catch (VException e) { handleError("Failed to advance", e); } } } /** * Goes back to the previous slide, if there is one, and updates the UI. */ private void previousSlide() { if (mSlideNum == -1) { // Wait until the state has loaded before letting the user move around. return; } if (mSlideNum > 0) { try { mSession.setLocalSlideNum(mSlideNum - 1); } catch (VException e) { handleError("Failed to go back", e); } } } private void updateView() { if (mSlideNum < 0 || mSlideNum >= mSlides.getItemCount()) { // Still loading. return; } if (mSlideNum > 0) { setThumbBitmap(mPrevThumb, mSlides.get(mSlideNum - 1).getThumb()); } else { setThumbNull(mPrevThumb); } // TODO(kash): Switch to full size image. mCurrentSlide.setImageBitmap(mSlides.get(mSlideNum).getThumb()); if (mSlideNum == mSlides.getItemCount() - 1) { setThumbNull(mNextThumb); } else { setThumbBitmap(mNextThumb, mSlides.get(mSlideNum + 1).getThumb()); } if (!mSlides.get(mSlideNum).getNotes().equals("")) { mNotes.setText(mSlides.get(mSlideNum).getNotes()); } else { mNotes.getText().clear(); } mSlideNumText.setText(String.valueOf(mSlideNum + 1) + " of " + String.valueOf(mSlides.getItemCount())); } private void setThumbBitmap(ImageView thumb, Bitmap bitmap) { thumb.setImageBitmap(bitmap); // In landscape, the height is dependent on the image size. However, if the // image was null, the height is hardcoded to 9/16 of the width in setThumbNull. // This resets it to the actual image size. if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams(); thumbParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; } } private void setThumbNull(ImageView thumb) { thumb.setImageDrawable(null); // In landscape, the height is dependent on the image size. Because we don't have an // image, assume all of the images are 16:9. The width is fixed, so we can calculate // the expected height. if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { ViewGroup grandparent = (ViewGroup) thumb.getParent().getParent(); ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams(); thumbParams.height = (int) ((9 / 16.0) * grandparent.getMeasuredWidth()); } } /** * If the user is editing the text field and the text has changed, save the * notes in Syncbase. That will trigger a notification that the slide has * changed and the UI will refresh. */ public void saveNotes() { final String notes = mNotes.getText().toString(); if (mEditing && (!notes.equals(mSlides.get(mSlideNum).getNotes()))) { try { mSession.setNotes(mSlideNum, notes); } catch (VException e) { handleError("Could not save notes", e); } } mNotes.clearFocus(); mEditing = false; InputMethodManager inputManager = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); if (getActivity().getCurrentFocus() != null) { inputManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } ((PresentationActivity) getActivity()).setUiImmersive(true); } private void handleError(String msg, Throwable throwable) { Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable)); Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); } private void handleFatalError(String msg, Throwable throwable) { handleError(msg, throwable); getActivity().finish(); } /** * Updates the view whenever the list of slides changes. */ private class SlideListListener implements ListListener { @Override public void notifyDataSetChanged() { updateView(); } @Override public void notifyItemChanged(int position) { updateView(); } @Override public void notifyItemInserted(int position) { updateView(); } @Override public void notifyItemRemoved(int position) { updateView(); } @Override public void onError(Exception e) { handleFatalError("Error watching slide list", e); } } private class SlideNumberListener implements Session.SlideNumberListener { @Override public void onChange(int slideNum) { mSlideNum = slideNum; updateView(); } @Override public void onError(Exception e) { handleFatalError("Error listening to slide number changes", e); } } /** * If the user is editing notes and then clicks anywhere else on the screen, * we want that action to save the notes first. Using this class forces * that behavior. */ private abstract class NavigateClickListener implements View.OnClickListener { @Override public final void onClick(View v) { saveNotes(); onNavigate(); } /** * Called when there is a click. */ abstract void onNavigate(); } }