com.alainesp.fan.sanderson.BookActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.alainesp.fan.sanderson.BookActivity.java

Source

// This file is part of Fan of Sanderson app,
// Copyright (c) 2015-2016 by Alain Espinosa.
// See LICENSE for details.

package com.alainesp.fan.sanderson;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RatingBar;
import android.widget.TextView;

import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;

/**
 * Activity to show details of a book and provide navigation of books in the same serie
 */
public class BookActivity extends AppCompatActivity {
    private static final DateFormat dateFormat = new SimpleDateFormat("MMM d, yyyy", Locale.US);
    /**
     * Pager to provide Slide animation from book to book in the same serie
     */
    private ViewPager mPager;
    /**
     * Book serie to show
     */
    private static BookSerie serie;
    // The system "short" animation time duration, in milliseconds. This duration is ideal
    // for subtle animations or animations that occur very frequently.
    private static int mShortAnimationDuration;
    /**
     * Provide Expand animation of the book cover
     */
    private static ImageView expandedImageView;
    private static View container;

    /**
     * Fragment to show a given book
     */
    public static class ScreenSlidePageFragment extends Fragment {
        private ViewGroup rootView;
        private Book book;
        //private BookActivity context;
        private static final String BOOK_INDEX = "BOOK_INDEX";
        // Hold a reference to the current animator, so that it can be canceled mid-way.
        private Animator mCurrentAnimator;

        public ScreenSlidePageFragment() {
        }

        /**
         * Set a book
         * @param aBook The book whose data is showed
         */
        public Fragment setBook(Book aBook) {
            book = aBook;
            return this;
        }

        @Override
        public void onSaveInstanceState(Bundle outState) {
            if (book != null)
                outState.putInt(BOOK_INDEX, serie.bookIndexOf(book));

            super.onSaveInstanceState(outState);
        }

        /**
         * Utility method to format and show data as "HEADER: context"
         * @param resID The text view to show data in
         * @param header The header
         * @param value The content
         * @return The text view
         */
        private TextView setTextFormatted(int resID, String header, String value) {
            TextView textView = (TextView) rootView.findViewById(resID);
            textView.setText(Html.fromHtml("<b>" + header + ": </b>" + value));

            return textView;
        }

        /**
         * Expand an image from thumbnail to occupy all the screen.
         * This code is from Android Training, Adding Animations, Zooming a View
         * @param thumbView The small view to enlarge
         * @param image The image to enlarge
         */
        private void zoomImageFromThumb(final View thumbView, final Drawable image) {
            // If there's an animation in progress, cancel it
            // immediately and proceed with this one.
            if (mCurrentAnimator != null)
                mCurrentAnimator.cancel();

            // Load the high-resolution "zoomed-in" image.
            expandedImageView.setImageDrawable(image);

            // Calculate the starting and ending bounds for the zoomed-in image.
            // This step involves lots of math. Yay, math.
            final Rect startBounds = new Rect();
            final Rect finalBounds = new Rect();
            final Point globalOffset = new Point();

            // The start bounds are the global visible rectangle of the thumbnail,
            // and the final bounds are the global visible rectangle of the container
            // view. Also set the container view's offset as the origin for the
            // bounds, since that's the origin for the positioning animation
            // properties (X, Y).
            thumbView.getGlobalVisibleRect(startBounds);
            container.getGlobalVisibleRect(finalBounds, globalOffset);
            startBounds.offset(-globalOffset.x, -globalOffset.y);
            finalBounds.offset(-globalOffset.x, -globalOffset.y);

            // Adjust the start bounds to be the same aspect ratio as the final
            // bounds using the "center crop" technique. This prevents undesirable
            // stretching during the animation. Also calculate the start scaling
            // factor (the end scaling factor is always 1.0).
            float startScale;
            if ((float) finalBounds.width() / finalBounds.height() > (float) startBounds.width()
                    / startBounds.height()) {
                // Extend start bounds horizontally
                startScale = (float) startBounds.height() / finalBounds.height();
                float startWidth = startScale * finalBounds.width();
                float deltaWidth = (startWidth - startBounds.width()) / 2;
                startBounds.left -= deltaWidth;
                startBounds.right += deltaWidth;
            } else {
                // Extend start bounds vertically
                startScale = (float) startBounds.width() / finalBounds.width();
                float startHeight = startScale * finalBounds.height();
                float deltaHeight = (startHeight - startBounds.height()) / 2;
                startBounds.top -= deltaHeight;
                startBounds.bottom += deltaHeight;
            }

            // Hide the thumbnail and show the zoomed-in view. When the animation
            // begins, it will position the zoomed-in view in the place of the
            // thumbnail.
            thumbView.setAlpha(0f);
            expandedImageView.setVisibility(View.VISIBLE);

            // Set the pivot point for SCALE_X and SCALE_Y transformations
            // to the top-left corner of the zoomed-in view (the default
            // is the center of the view).
            expandedImageView.setPivotX(0f);
            expandedImageView.setPivotY(0f);

            // Construct and run the parallel animation of the four translation and
            // scale properties (X, Y, SCALE_X, and SCALE_Y).
            AnimatorSet set = new AnimatorSet();
            set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
                    .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
            set.setDuration(mShortAnimationDuration);
            set.setInterpolator(new DecelerateInterpolator());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mCurrentAnimator = null;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    mCurrentAnimator = null;
                }
            });
            set.start();
            mCurrentAnimator = set;

            // Upon clicking the zoomed-in image, it should zoom back down
            // to the original bounds and show the thumbnail instead of
            // the expanded image.
            final float startScaleFinal = startScale;
            expandedImageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (mCurrentAnimator != null) {
                        mCurrentAnimator.cancel();
                    }

                    // Animate the four positioning/sizing properties in parallel,
                    // back to their original values.
                    AnimatorSet set = new AnimatorSet();
                    set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
                            .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
                            .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
                            .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
                    set.setDuration(mShortAnimationDuration);
                    set.setInterpolator(new DecelerateInterpolator());
                    set.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            thumbView.setAlpha(1f);
                            expandedImageView.setVisibility(View.GONE);
                            mCurrentAnimator = null;
                        }

                        @Override
                        public void onAnimationCancel(Animator animation) {
                            thumbView.setAlpha(1f);
                            expandedImageView.setVisibility(View.GONE);
                            mCurrentAnimator = null;
                        }
                    });
                    set.start();
                    mCurrentAnimator = set;
                }
            });
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            rootView = (ViewGroup) inflater.inflate(R.layout.activity_book, container, false);

            if (savedInstanceState != null)
                book = serie.getBookAt(savedInstanceState.getInt(BOOK_INDEX));

            // Normal cover
            final ImageView coverView = (ImageView) rootView.findViewById(R.id.book_cover);
            final Drawable coverImg = book.loadCover();
            coverView.setImageDrawable(coverImg);
            // Provide tap to zoom animation
            coverView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    zoomImageFromThumb(coverView, coverImg);
                }
            });

            // Set small details of book
            NumberFormat intFormat = NumberFormat.getIntegerInstance();
            intFormat.setGroupingUsed(true);
            setTextFormatted(R.id.book_serie, "Serie",
                    book.parent.title != null ? book.parent.title : book.parent.parent.title);
            setTextFormatted(R.id.book_genre, "Genre", book.parent.genre);
            setTextFormatted(R.id.book_pub_date, book.isPublished() ? "Released" : "Expected",
                    dateFormat.format(book.publishedDate));
            setTextFormatted(R.id.book_pages_chapters, "Pages",
                    intFormat.format(book.pages) + " <b>Chapters: </b>" + intFormat.format(book.chapters));
            setTextFormatted(R.id.book_words_audio, "Words", intFormat.format(book.words) + " <b>Audio: </b>"
                    + (book.audioTime != null ? book.audioTime : "0h 0m"));
            // The rate
            TextView rate = (TextView) rootView.findViewById(R.id.book_rate);
            rate.setText("" + book.rate + " - " + intFormat.format(book.ratings) + " ratings");
            RatingBar starBar = (RatingBar) rootView.findViewById(R.id.book_ratingbar);
            starBar.setRating(book.rate);

            // Add awards if any
            String htmlAwards = book.getFormattedAwards();
            LinearLayout bookInfo = (LinearLayout) rootView.findViewById(R.id.book_info);
            Context context = getActivity().getBaseContext();
            if (htmlAwards != null) {
                LinearLayout awardsLayout = new LinearLayout(context);

                // Image
                ImageView img = new ImageView(context);
                img.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                img.setImageResource(R.drawable.award);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        context.getResources().getDimensionPixelSize(R.dimen.award_width),
                        context.getResources().getDimensionPixelSize(R.dimen.award_height));
                params.gravity = Gravity.CENTER_VERTICAL;
                awardsLayout.addView(img, params);
                // The awards
                TextView book_awards = new TextView(context);
                book_awards.setPadding(
                        context.getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin), 0, 0, 0);
                book_awards.setText(Html.fromHtml(htmlAwards));
                book_awards.setTextColor(Color.BLACK);
                params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.CENTER_VERTICAL;
                awardsLayout.addView(book_awards, params);

                bookInfo.addView(awardsLayout);
            } else// Add links here to balance the GUI. Without them looks with holes
            {
                TextView book_links = new TextView(context);
                book_links.setText(Html.fromHtml(book.getFormattedLinks(true)));
                book_links.setTextColor(Color.BLACK);
                book_links.setMovementMethod(LinkMovementMethod.getInstance());
                bookInfo.addView(book_links);
            }

            // Synopsis
            TextView synopsis = (TextView) rootView.findViewById(R.id.book_synopsis);
            synopsis.setText(book.synopsis);
            synopsis.setMovementMethod(LinkMovementMethod.getInstance());
            synopsis.setTextSize(TypedValue.COMPLEX_UNIT_SP, SettingsFragment.getTextSize());

            // Add links at the footer of the view
            TextView links = setTextFormatted(R.id.book_links, "Links", book.getFormattedLinks(false));
            links.setMovementMethod(LinkMovementMethod.getInstance());
            links.setTextSize(TypedValue.COMPLEX_UNIT_SP, SettingsFragment.getTextSize());

            return rootView;
        }
    }

    /**
     * Adapter to provide books to the pager
     */
    private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
        public ScreenSlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return new ScreenSlidePageFragment().setBook(serie.getBookAt(position));
        }

        @Override
        public int getCount() {
            return serie.getBookCount();
        }
    }

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

        // Set the title of the book
        setTitle(Catalog.readingBook.title);
        serie = Catalog.readingBook.parent.parent;

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(new ScreenSlidePagerAdapter(getSupportFragmentManager()));
        mPager.setCurrentItem(Catalog.readingBook.getIndexInSerie());
        // Listener to change Title and ActionMenus based on book selected
        mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int position) {
                Catalog.readingBook = serie.getBookAt(position);
                setTitle(Catalog.readingBook.title);
                if (prevBookItem != null)
                    prevBookItem.setVisible(position > 0);
                if (nextBookItem != null)
                    nextBookItem.setVisible((position + 1) < serie.getBookCount());
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });

        // Retrieve and cache the system's default "short" animation time and the expanded and container view used in cover zoom animation.
        mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
        expandedImageView = (ImageView) findViewById(R.id.slider_expanded_image);
        container = findViewById(R.id.slider_container);
    }

    /**
     * The ActionMenus to provide navigation in the book serie
     */
    private MenuItem nextBookItem, prevBookItem;

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.show_book_menu, menu);

        nextBookItem = menu.findItem(R.id.book_menu_next);
        prevBookItem = menu.findItem(R.id.book_menu_prev);

        prevBookItem.setVisible(Catalog.readingBook.getIndexInSerie() > 0);
        nextBookItem.setVisible((Catalog.readingBook.getIndexInSerie() + 1) < serie.getBookCount());

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.book_menu_next:
            mPager.setCurrentItem(mPager.getCurrentItem() + 1, true);
            return true;
        case R.id.book_menu_prev:
            mPager.setCurrentItem(mPager.getCurrentItem() - 1, true);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}