/* T his file is part of the Android Clementine Remote. * Copyright (C) 2013, Andreas Muttscheller <asfa194@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.qspool.clementineremote.ui.fragments.playerpages; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Fragment; import android.content.SharedPreferences; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.Message; import android.preference.PreferenceManager; 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.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RatingBar; import android.widget.TextView; import android.widget.Toast; import de.qspool.clementineremote.App; import de.qspool.clementineremote.R; import de.qspool.clementineremote.SharedPreferencesKeys; import de.qspool.clementineremote.backend.pb.ClementineMessage; import de.qspool.clementineremote.backend.pb.ClementineMessageFactory; import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.MsgType; import de.qspool.clementineremote.backend.player.MySong; import de.qspool.clementineremote.ui.interfaces.BackPressHandleable; import de.qspool.clementineremote.ui.interfaces.NameableTitle; import de.qspool.clementineremote.ui.interfaces.RemoteDataReceiver; import de.qspool.clementineremote.utils.Utilities; public class SongDetailFragment extends Fragment implements BackPressHandleable, RemoteDataReceiver, NameableTitle { private FrameLayout mContainer; private ImageButton iv_art; private ImageView iv_large_art; private TextView tv_artist; private TextView tv_title; private TextView tv_album; private TextView tv_genre; private TextView tv_year; private TextView tv_track; private TextView tv_disc; private TextView tv_playcount; private TextView tv_length; private TextView tv_size; private TextView tv_filename; private RatingBar rb_rating; // Hold a reference to the current animator, // so that it can be canceled mid-way. private Animator mCurrentAnimator; // The system "short" animation time duration, in milliseconds. This // duration is ideal for subtle animations or animations that occur // very frequently. private int mShortAnimationDuration; private MySong mCurrentSong; private SharedPreferences mSharedPref; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get the shared preferences mSharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity()); setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_player_songdetail, container, false); mContainer = (FrameLayout) view.findViewById(R.id.si_container); iv_art = (ImageButton) view.findViewById(R.id.si_art); iv_large_art = (ImageView) view.findViewById(R.id.si_large_art); iv_art.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { zoomImageFromThumb(iv_art); } }); // Retrieve and cache the system's default "short" animation time. mShortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); tv_artist = (TextView) view.findViewById(R.id.si_artist); tv_title = (TextView) view.findViewById(R.id.si_title); tv_album = (TextView) view.findViewById(R.id.si_album); tv_genre = (TextView) view.findViewById(R.id.si_genre); tv_year = (TextView) view.findViewById(R.id.si_year); tv_track = (TextView) view.findViewById(R.id.si_track); tv_disc = (TextView) view.findViewById(R.id.si_disc); tv_playcount = (TextView) view.findViewById(R.id.si_playcount); tv_length = (TextView) view.findViewById(R.id.si_length); tv_size = (TextView) view.findViewById(R.id.si_size); tv_filename = (TextView) view.findViewById(R.id.si_filename); rb_rating = (RatingBar) view.findViewById(R.id.si_rating); rb_rating.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { if (fromUser && App.Clementine.getCurrentSong() != null) { // Send the rat ing message to Clementine Message msg = Message.obtain(); msg.obj = ClementineMessageFactory.buildRateTrack(rating / 5); App.ClementineConnection.mHandler.sendMessage(msg); // Show a toast String toast = getString(R.string.song_info_rated); Toast.makeText(getActivity(), toast.replace("$stars$", Float.toString(rating)), Toast.LENGTH_SHORT).show(); } } }); return view; } @Override public void onResume() { super.onResume(); updateTrackMetadata(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.clear(); inflater.inflate(R.menu.song_info_menu, menu); // Shall we show the lastfm buttons? boolean showLastFm = mSharedPref.getBoolean(SharedPreferencesKeys.SP_LASTFM, true); menu.findItem(R.id.love).setVisible(showLastFm); menu.findItem(R.id.ban).setVisible(showLastFm); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.love: if (App.Clementine.getCurrentSong() != null && !App.Clementine.getCurrentSong().isLoved()) { // You can love only one Message msg = Message.obtain(); msg.obj = ClementineMessage.getMessage(MsgType.LOVE); App.ClementineConnection.mHandler.sendMessage(msg); App.Clementine.getCurrentSong().setLoved(true); } Toast.makeText(getActivity(), R.string.track_loved, Toast.LENGTH_SHORT).show(); ; break; case R.id.ban: Message msg = Message.obtain(); msg.obj = ClementineMessage.getMessage(MsgType.BAN); App.ClementineConnection.mHandler.sendMessage(msg); Toast.makeText(getActivity(), R.string.track_banned, Toast.LENGTH_SHORT).show(); break; default: break; } return true; } @Override public void MessageFromClementine(ClementineMessage clementineMessage) { switch (clementineMessage.getMessageType()) { case CURRENT_METAINFO: updateTrackMetadata(); break; default: break; } } /** * The track changed. Update the metadata shown on the user interface */ public void updateTrackMetadata() { // Get the currently played song MySong currentSong = App.Clementine.getCurrentSong(); if (currentSong == null) { tv_artist.setText(""); tv_title.setText(getString(R.string.player_nosong)); tv_album.setText(""); tv_genre.setText(""); tv_year.setText(""); tv_track.setText(""); tv_disc.setText(""); tv_playcount.setText(""); tv_length.setText(""); tv_size.setText(""); tv_filename.setText(""); rb_rating.setRating(0); } else { tv_artist.setText(currentSong.getArtist()); tv_title.setText(currentSong.getTitle()); tv_album.setText(currentSong.getAlbum()); tv_genre.setText(currentSong.getGenre()); tv_year.setText(currentSong.getYear()); tv_track.setText(currentSong.getTrack() != -1 ? String.valueOf(currentSong.getTrack()) : ""); tv_disc.setText(currentSong.getDisc() != -1 ? String.valueOf(currentSong.getDisc()) : ""); tv_playcount.setText(String.valueOf(currentSong.getPlaycount())); tv_length.setText(currentSong.getPrettyLength()); tv_size.setText(Utilities.humanReadableBytes(currentSong.getSize(), true)); tv_filename.setText(currentSong.getFilename()); rb_rating.setRating(currentSong.getRating() * 5); if (currentSong.getArt() != null) { iv_art.setImageBitmap(currentSong.getArt()); } } mCurrentSong = currentSong; } private void zoomImageFromThumb(final View thumbView) { // If there's an animation in progress, cancel it // immediately and proceed with this one. if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } // If we don't have an image, do not zoom! if (mCurrentSong == null || mCurrentSong.getArt() == null) { return; } // Load the high-resolution "zoomed-in" image. iv_large_art.setImageBitmap(mCurrentSong.getArt()); // 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); mContainer.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); iv_large_art.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). iv_large_art.setPivotX(0f); iv_large_art.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(iv_large_art, "x", startBounds.left, finalBounds.left)) .with(ObjectAnimator.ofFloat(iv_large_art, "y", startBounds.top, finalBounds.top)) .with(ObjectAnimator.ofFloat(iv_large_art, "scaleX", startScale, 1f)).with(ObjectAnimator.ofFloat(iv_large_art, "scaleY", 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; iv_large_art.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(iv_large_art, "x", startBounds.left)) .with(ObjectAnimator .ofFloat(iv_large_art, "y", startBounds.top)) .with(ObjectAnimator .ofFloat(iv_large_art, "scaleX", startScaleFinal)) .with(ObjectAnimator .ofFloat(iv_large_art, "scaleY", startScaleFinal)); set.setDuration(mShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { thumbView.setAlpha(1f); iv_large_art.setVisibility(View.GONE); mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { thumbView.setAlpha(1f); iv_large_art.setVisibility(View.GONE); mCurrentAnimator = null; } }); set.start(); mCurrentAnimator = set; } }); } @Override public boolean onBackPressed() { return false; } @Override public int getTitleId() { return R.string.fragment_title_details; } }