Redesigned playing movies on device running Kore

* Implemented a new widget "fabspeeddial"
   * Provides user with two options to play the media item. One
     option to play the item on Kodi, one to play it on the remote.
   * Replaced deprecated FAB button from
     com.melnykov:floatingactionbutton:1.3.0 with the FAB button from
     the design library
   * Implemented a busy indicator (pulsate) when fab button is clicked
     and JSON  API method is still pending
   * Added a setting to allow the user to disable local playback and
     revert back to the old behaviour
* Refactored AbstractFragmentInfo
   * Replaced RelativeLayout by CoordinatorLayout to support
     hiding/showing the FAB button when scrolling
   * Replaced the tree view observer to fade out art view when scrolling
     with a behavior for the CoordinaterLayout
* Removed empty theme file for v19
* Refactored HostConnection to allow new activities to attach its
  callbacks to any pending ApiMethod. This is required to support device
  configuration changes.
This commit is contained in:
Martijn Brekhof 2017-11-14 08:30:56 +01:00 committed by Synced Synapse
parent 0cd91c3905
commit e2c39e35ba
32 changed files with 1225 additions and 887 deletions

View File

@ -116,6 +116,7 @@ dependencies {
compile "com.android.support:cardview-v7:${supportLibVersion}"
compile "com.android.support:preference-v14:${supportLibVersion}"
compile "com.android.support:support-v13:${supportLibVersion}"
compile "com.android.support:design:${supportLibVersion}"
compile 'com.fasterxml.jackson.core:jackson-databind:2.5.2'
compile 'com.jakewharton:butterknife:6.1.0'
@ -124,7 +125,6 @@ dependencies {
compile 'de.greenrobot:eventbus:2.4.0'
compile 'org.jmdns:jmdns:3.5.1'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'com.melnykov:floatingactionbutton:1.3.0'
compile 'at.blogc:expandabletextview:1.0.3'
compile 'com.sothree.slidinguppanel:library:3.3.1'

View File

@ -155,6 +155,10 @@ public class Settings {
public static final String KEY_PREF_SINGLE_COLUMN = "pref_single_multi_column";
public static final boolean DEFAULT_PREF_SINGLE_COLUMN = false;
// Switch to remote
public static final String KEY_PREF_DISABLE_LOCAL_PLAY = "pref_disable_local_play";
public static final boolean DEFAULT_PREF_DISABLE_LOCAL_PLAY = false;
/**
* Determines the bit flags used by {@link DownloadManager.Request} to correspond to the enabled network connections
* from the settings screen.

View File

@ -15,6 +15,7 @@
*/
package org.xbmc.kore.jsonrpc;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Process;
import android.text.TextUtils;
@ -281,11 +282,13 @@ public class HostConnection {
}
/**
* Calls the a method on the server
* Calls the given method on the server
* This call is always asynchronous. The results will be posted, through the
* {@link ApiCallback callback} parameter, on the specified {@link android.os.Handler}.
*
* @param method Method object that represents the methood too call
* <BR/>
* If you need to update the callback and handler (e.g. due to a device configuration change)
* use {@link #updateClientCallback(int, ApiCallback, Handler)}
* @param method Method object that represents the methood too call
* @param callback {@link ApiCallback} to post the response to
* @param handler {@link Handler} to invoke callbacks on. When null, the
* callbacks are invoked on the same thread as the request.
@ -297,13 +300,21 @@ public class HostConnection {
LogUtils.LOGD(TAG, "Starting method execute. Method: " + method.getMethodName() +
" on host: " + hostInfo.getJsonRpcHttpEndpoint());
if (protocol == PROTOCOL_TCP) {
/**
* Do not call this from the runnable below as it may cause a race condition
* with {@link #updateClientCallback(int, ApiCallback, Handler)}
*/
// Save this method/callback for any later response
addClientCallback(method, callback, handler);
}
// Launch background thread
Runnable command = new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
if (protocol == PROTOCOL_HTTP) {
// executeThroughHttp(method, callback, handler);
executeThroughOkHttp(method, callback, handler);
} else {
executeThroughTcp(method, callback, handler);
@ -342,6 +353,67 @@ public class HostConnection {
return ApiFuture.from(this, method);
}
/**
* Updates the client callback for the given {@link ApiMethod} if it is still pending.
* This can be used when the activity or fragment has been destroyed and recreated and
* you are still interested in the result of any pending {@link ApiMethod}
* @param methodId for which a new callback needs to be attached
* @param callback new callback that needs to be called for the new activity or fragment
* @param handler used to execute the callback on the UI thread
* @param <T> result type
* @return true if the {@link ApiMethod} was still pending, false otherwise.
*/
@SuppressWarnings("unchecked")
public <T> boolean updateClientCallback(final int methodId, final ApiCallback<T> callback,
final Handler handler) {
if (getProtocol() == PROTOCOL_HTTP)
return false;
synchronized (clientCallbacks) {
String id = String.valueOf(methodId);
if (clientCallbacks.containsKey(id)) {
clientCallbacks.put(id, new MethodCallInfo<>((ApiMethod<T>) clientCallbacks.get(id).method,
callback, handler));
return true;
}
return false;
}
}
/**
* Stores the method and callback to handle asynchronous responses.
* Note this is only needed for requests over TCP.
* @param method
* @param callback
* @param handler
* @param <T>
*/
private <T> void addClientCallback(final ApiMethod<T> method, final ApiCallback<T> callback,
final Handler handler) {
if (getProtocol() == PROTOCOL_HTTP)
return;
String methodId = String.valueOf(method.getId());
synchronized (clientCallbacks) {
if (clientCallbacks.containsKey(methodId)) {
if ((handler != null) && (callback != null)) {
handler.post(new Runnable() {
@Override
public void run() {
callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
"A method with the same Id is already executing");
}
});
}
return;
}
clientCallbacks.put(methodId, new MethodCallInfo<T>(method, callback, handler));
}
}
/**
* Sends the JSON RPC request through HTTP (using OkHttp library)
*/
@ -355,7 +427,6 @@ public class HostConnection {
.url(hostInfo.getJsonRpcHttpEndpoint())
.post(RequestBody.create(MEDIA_TYPE_JSON, jsonRequest))
.build();
LogUtils.LOGD(TAG, "Sending request via OkHttp: " + jsonRequest);
Response response = sendOkHttpRequest(client, request);
final T result = method.resultFromJson(parseJsonResponse(handleOkHttpResponse(response)));
@ -517,25 +588,7 @@ public class HostConnection {
private <T> void executeThroughTcp(final ApiMethod<T> method, final ApiCallback<T> callback,
final Handler handler) {
String methodId = String.valueOf(method.getId());
try {
// Save this method/callback for later response
// Check if a method with this id is already running and raise an error if so
synchronized (clientCallbacks) {
if (clientCallbacks.containsKey(methodId)) {
if (callback != null) {
postOrRunNow(handler, new Runnable() {
@Override
public void run() {
callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
"A method with the same Id is already executing");
}
});
}
return;
}
clientCallbacks.put(methodId, new MethodCallInfo<T>(method, callback, handler));
}
try {
// TODO: Validate if this shouldn't be enclosed by a synchronized.
if (socket == null) {
// Open connection to the server and setup reader thread
@ -843,6 +896,7 @@ public class HostConnection {
});
}
}
clientCallbacks.clear();
}
}

View File

@ -17,11 +17,14 @@
package org.xbmc.kore.ui;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
@ -30,6 +33,7 @@ import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import android.util.DisplayMetrics;
@ -39,17 +43,12 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
@ -60,6 +59,7 @@ import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.UIUtils;
@ -79,9 +79,11 @@ abstract public class AbstractInfoFragment extends AbstractFragment
SharedElementTransition.SharedElement {
private static final String TAG = LogUtils.makeLogTag(AbstractInfoFragment.class);
private static final String BUNDLE_KEY_APIMETHOD_PENDING = "pending_apimethod";
// Detail views
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.media_panel) ScrollView panelScrollView;
@InjectView(R.id.media_panel) NestedScrollView panelScrollView;
@InjectView(R.id.art) ImageView artImageView;
@InjectView(R.id.poster) ImageView posterImageView;
@InjectView(R.id.media_title) TextView titleTextView;
@ -101,7 +103,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
@InjectView(R.id.media_description) ExpandableTextView descriptionExpandableTextView;
@InjectView(R.id.media_description_container) LinearLayout descriptionContainer;
@InjectView(R.id.show_all) ImageView expansionImage;
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.fab) FABSpeedDial fabButton;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
private HostManager hostManager;
@ -109,6 +111,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
private ServiceConnection serviceConnection;
private RefreshItem refreshItem;
private boolean expandDescription;
private int methodId;
/**
* Handler on which to post RPC callbacks
@ -144,17 +147,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_info, container, false);
ButterKnife.inject(this, root);
// Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
panelScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = panelScrollView.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
artImageView.setAlpha(newAlpha);
}
});
DataHolder dataHolder = getDataHolder();
@ -171,9 +164,6 @@ abstract public class AbstractInfoFragment extends AbstractFragment
swipeRefreshLayout.setEnabled(false);
}
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) panelScrollView);
if(Utils.isLollipopOrLater()) {
posterImageView.setTransitionName(dataHolder.getPosterTransitionName());
}
@ -207,6 +197,14 @@ abstract public class AbstractInfoFragment extends AbstractFragment
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
if (savedInstanceState != null) {
int methodId = savedInstanceState.getInt(BUNDLE_KEY_APIMETHOD_PENDING);
fabButton.enableBusyAnimation(HostManager.getInstance(getContext()).getConnection()
.updateClientCallback(methodId, createPlayItemOnKodiCallback(),
callbackHandler));
}
}
@Override
@ -245,6 +243,13 @@ abstract public class AbstractInfoFragment extends AbstractFragment
SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(BUNDLE_KEY_APIMETHOD_PENDING, methodId);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -294,37 +299,24 @@ abstract public class AbstractInfoFragment extends AbstractFragment
}
}
protected void fabActionPlayItem(PlaylistType.Item item) {
protected void playItemLocally(String url, String type) {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.setDataAndType(uri, type);
startActivity(intent);
}
protected void playItemOnKodi(PlaylistType.Item item) {
if (item == null) {
Toast.makeText(getActivity(), R.string.no_item_available_to_play, Toast.LENGTH_SHORT).show();
return;
}
fabButton.enableBusyAnimation(true);
Player.Open action = new Player.Open(item);
action.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
methodId = action.getId();
action.execute(HostManager.getInstance(getActivity()).getConnection(),
createPlayItemOnKodiCallback(), callbackHandler);
}
@Override
@ -366,6 +358,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
/**
* Call this when you are ready to provide the titleTextView, undertitle, details, descriptionExpandableTextView, etc. etc.
*/
@SuppressLint("StringFormatInvalid")
protected void updateView(DataHolder dataHolder) {
titleTextView.setText(dataHolder.getTitle());
underTitleTextView.setText(dataHolder.getUnderTitle());
@ -497,7 +490,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
/**
* Uses colors to show to the user the item has been downloaded
* @param state true if item has been watched/listened too, false otherwise
* @param state true if item has been downloaded, false otherwise
*/
protected void setDownloadButtonState(boolean state) {
UIUtils.highlightImageView(getActivity(), downloadButton, state);
@ -558,6 +551,41 @@ abstract public class AbstractInfoFragment extends AbstractFragment
this.expandDescription = expandDescription;
}
public FABSpeedDial getFabButton() {
return fabButton;
}
private ApiCallback<String> createPlayItemOnKodiCallback() {
return new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
fabButton.enableBusyAnimation(false);
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
fabButton.enableBusyAnimation(false);
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
};
}
abstract protected AbstractAdditionalInfoFragment getAdditionalInfoFragment();
/**
@ -586,5 +614,5 @@ abstract public class AbstractInfoFragment extends AbstractFragment
* Called when the fab button is available
* @return true to enable the Floating Action Button, false otherwise
*/
abstract protected boolean setupFAB(ImageButton FAB);
abstract protected boolean setupFAB(FABSpeedDial FAB);
}

View File

@ -1,721 +0,0 @@
/*
* Copyright 2015 Synced Synapse. All rights reserved.
*
* 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 org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.GridLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.method.Playlist;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.jsonrpc.type.VideoType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
/**
* Presents movie details
*/
public class MovieDetailsFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = LogUtils.makeLogTag(MovieDetailsFragment.class);
public static final String BUNDLE_KEY_MOVIETITLE = "movie_title";
public static final String BUNDLE_KEY_MOVIEPLOT = "movie_plot";
public static final String BUNDLE_KEY_MOVIEID = "movie_id";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_MOVIEGENRES = "movie_genres";
public static final String BUNDLE_KEY_MOVIEYEAR = "movie_year";
public static final String BUNDLE_KEY_MOVIERUNTIME = "movie_runtime";
public static final String BUNDLE_KEY_MOVIERATING = "movie_rating";
// Loader IDs
private static final int LOADER_MOVIE = 0,
LOADER_CAST = 1;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
// Displayed movie id
private int movieId = -1;
private String movieTitle;
private ArrayList<VideoType.Cast> castArrayList;
// Info for downloading the movie
private FileDownloadHelper.MovieInfo movieDownloadInfo = null;
// Controls whether a automatic sync refresh has been issued for this show
private static boolean hasIssuedOutdatedRefresh = false;
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
@InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
@InjectView(R.id.go_to_imdb) ImageButton imdbButton;
@InjectView(R.id.download) ImageButton downloadButton;
@InjectView(R.id.seen) ImageButton seenButton;
@InjectView(R.id.local_play) ImageButton localPlayButton;
// Detail views
@InjectView(R.id.media_panel) ScrollView mediaPanel;
@InjectView(R.id.art) ImageView mediaArt;
@InjectView(R.id.poster) ImageView mediaPoster;
@InjectView(R.id.media_title) TextView mediaTitle;
@InjectView(R.id.media_undertitle) TextView mediaUndertitle;
@InjectView(R.id.rating) TextView mediaRating;
@InjectView(R.id.max_rating) TextView mediaMaxRating;
@InjectView(R.id.year) TextView mediaYear;
@InjectView(R.id.genres) TextView mediaGenres;
@InjectView(R.id.rating_votes) TextView mediaRatingVotes;
@InjectView(R.id.media_description) TextView mediaDescription;
@InjectView(R.id.directors) TextView mediaDirectors;
@InjectView(R.id.cast_list) GridLayout videoCastList;
/**
* Create a new instance of this, initialized to show the movie movieId
*/
@TargetApi(21)
public static MovieDetailsFragment newInstance(MovieListFragment.ViewHolder vh) {
MovieDetailsFragment fragment = new MovieDetailsFragment();
Bundle args = new Bundle();
args.putInt(BUNDLE_KEY_MOVIEID, vh.movieId);
args.putString(BUNDLE_KEY_MOVIETITLE, vh.movieTitle);
args.putString(BUNDLE_KEY_MOVIEPLOT, vh.movieTagline);
args.putString(BUNDLE_KEY_MOVIEGENRES, vh.movieGenres);
args.putInt(BUNDLE_KEY_MOVIEYEAR, vh.movieYear);
args.putInt(BUNDLE_KEY_MOVIERUNTIME, vh.movieRuntime);
args.putDouble(BUNDLE_KEY_MOVIERATING, vh.movieRating);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
movieId = bundle.getInt(BUNDLE_KEY_MOVIEID, -1);
if (movieId == -1) {
// There's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_movie_details, container, false);
ButterKnife.inject(this, root);
//UIUtils.setSwipeRefreshLayoutColorScheme(swipeRefreshLayout);
// Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
mediaPanel.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = mediaPanel.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
mediaArt.setAlpha(newAlpha);
}
});
FloatingActionButton fab = (FloatingActionButton)fabButton;
fab.attachToScrollView((ObservableScrollView) mediaPanel);
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(getArguments().getString(POSTER_TRANS_NAME));
}
mediaTitle.setText(bundle.getString(BUNDLE_KEY_MOVIETITLE));
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_MOVIEPLOT));
mediaGenres.setText(bundle.getString(BUNDLE_KEY_MOVIEGENRES));
setMediaYear(bundle.getInt(BUNDLE_KEY_MOVIERUNTIME), bundle.getInt(BUNDLE_KEY_MOVIEYEAR));
setMediaRating(bundle.getDouble(BUNDLE_KEY_MOVIERATING));
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
// mediaPanel.setClipToPadding(false);
return root;
}
@Override
protected String getSyncType() {
return LibrarySyncService.SYNC_SINGLE_MOVIE;
}
@Override
protected String getSyncID() {
return LibrarySyncService.SYNC_MOVIEID;
}
@Override
protected int getSyncItemID() {
return movieId;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return swipeRefreshLayout;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
hasIssuedOutdatedRefresh = false;
// Start the loaders
getLoaderManager().initLoader(LOADER_MOVIE, null, this);
getLoaderManager().initLoader(LOADER_CAST, null, this);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
//As we make mediaPoster invisible in onStop() we need to make it visible here.
mediaPoster.setVisibility(View.VISIBLE);
super.onResume();
}
@Override
public void onStop() {
//For some reason poster is included in the bottom slide animation, by making it invisible it is not noticeable for the user
mediaPoster.setVisibility(View.INVISIBLE);
super.onStop();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
getLoaderManager().restartLoader(LOADER_MOVIE, null, this);
getLoaderManager().restartLoader(LOADER_CAST, null, this);
}
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_MOVIE:
uri = MediaContract.Movies.buildMovieUri(getHostInfo().getId(), movieId);
return new CursorLoader(getActivity(), uri,
MovieDetailsQuery.PROJECTION, null, null, null);
case LOADER_CAST:
uri = MediaContract.MovieCast.buildMovieCastListUri(getHostInfo().getId(), movieId);
return new CursorLoader(getActivity(), uri,
MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_MOVIE:
displayMovieDetails(cursor);
checkOutdatedMovieDetails(cursor);
break;
case LOADER_CAST:
displayCastList(cursor);
break;
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
/**
* Callbacks for button bar
*/
@OnClick(R.id.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = movieId;
Player.Open action = new Player.Open(item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
getPlaylists.execute(getHostManager().getConnection(), new ApiCallback<ArrayList<PlaylistType.GetPlaylistsReturnType>>() {
@Override
public void onSuccess(ArrayList<PlaylistType.GetPlaylistsReturnType> result) {
if (!isAdded()) return;
// Ok, loop through the playlists, looking for the video one
int videoPlaylistId = -1;
for (PlaylistType.GetPlaylistsReturnType playlist : result) {
if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
videoPlaylistId = playlist.playlistid;
break;
}
}
// If found, add to playlist
if (videoPlaylistId != -1) {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = movieId;
Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
.show();
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
} else {
Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
.show();
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.go_to_imdb)
public void onImdbClicked(View v) {
String imdbNumber = (String)v.getTag();
if (imdbNumber != null) {
Utils.openImdbForMovie(getActivity(), imdbNumber);
}
}
@OnClick(R.id.seen)
public void onSeenClicked(View v) {
// Set the playcount
Integer playcount = (Integer)v.getTag();
int newPlaycount = (playcount > 0) ? 0 : 1;
VideoLibrary.SetMovieDetails action =
new VideoLibrary.SetMovieDetails(movieId, newPlaycount, null);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Force a refresh, but don't show a message
startSync(true);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
// Change the button, to provide imeddiate feedback, even if it isn't yet stored in the db
// (will be properly updated and refreshed after the refresh callback ends)
setupSeenButton(newPlaycount);
}
@OnClick(R.id.local_play)
public void onLocalPlayClicked(View v) {
if (movieDownloadInfo == null) {
// Nothing to play on local
Toast.makeText(getActivity(), R.string.no_files_to_play, Toast.LENGTH_SHORT).show();
return;
}
String videoUrl = movieDownloadInfo.getMediaUrl(getHostInfo());
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(videoUrl));
intent.setDataAndType(Uri.parse(videoUrl), "video/*");
startActivity(intent);
}
@Override
protected void onDownload() {
if (movieDownloadInfo == null) {
// Nothing to download
Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show();
return;
}
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
File file = new File(movieDownloadInfo.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_file_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
// Confirm that the user really wants to download the file
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_movie_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
/**
* Display the movie details
*
* @param cursor Cursor with the data
*/
private void displayMovieDetails(Cursor cursor) {
LogUtils.LOGD(TAG, "Refreshing movie details");
cursor.moveToFirst();
movieTitle = cursor.getString(MovieDetailsQuery.TITLE);
mediaTitle.setText(movieTitle);
mediaUndertitle.setText(cursor.getString(MovieDetailsQuery.TAGLINE));
setMediaYear(cursor.getInt(MovieDetailsQuery.RUNTIME) / 60, cursor.getInt(MovieDetailsQuery.YEAR));
mediaGenres.setText(cursor.getString(MovieDetailsQuery.GENRES));
double rating = cursor.getDouble(MovieDetailsQuery.RATING);
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRatingVotes.setVisibility(View.VISIBLE);
setMediaRating(rating);
String votes = cursor.getString(MovieDetailsQuery.VOTES);
mediaRatingVotes.setText((TextUtils.isEmpty(votes)) ?
"" : String.format(getString(R.string.votes), votes));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
mediaRatingVotes.setVisibility(View.INVISIBLE);
}
mediaDescription.setText(cursor.getString(MovieDetailsQuery.PLOT));
mediaDirectors.setText(cursor.getString(MovieDetailsQuery.DIRECTOR));
// IMDB button
imdbButton.setTag(cursor.getString(MovieDetailsQuery.IMDBNUMBER));
setupSeenButton(cursor.getInt(MovieDetailsQuery.PLAYCOUNT));
// Images
Resources resources = getActivity().getResources();
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height);
UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(),
cursor.getString(MovieDetailsQuery.THUMBNAIL), movieTitle,
mediaPoster, posterWidth, posterHeight);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
UIUtils.loadImageIntoImageview(getHostManager(),
cursor.getString(MovieDetailsQuery.FANART),
mediaArt, displayMetrics.widthPixels, artHeight);
// Setup movie download info
movieDownloadInfo = new FileDownloadHelper.MovieInfo(
movieTitle, cursor.getString(MovieDetailsQuery.FILE));
// Check if downloaded file exists
if (movieDownloadInfo.downloadFileExists()) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
R.attr.colorAccent});
downloadButton.setColorFilter(
styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
downloadButton.clearColorFilter();
}
}
private void setMediaRating(double rating) {
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
}
private void setMediaYear(int runtime, int year) {
String durationYear = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + year :
String.valueOf(year);
mediaYear.setText(durationYear);
}
private void setupSeenButton(int playcount) {
// Seen button
if (playcount > 0) {
Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
R.attr.colorAccent});
seenButton.setColorFilter(styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle();
} else {
seenButton.clearColorFilter();
}
// Save the playcount
seenButton.setTag(playcount);
}
/**
* Display the cast details
*
* @param cursor Cursor with the data
*/
private void displayCastList(Cursor cursor) {
// Transform the cursor into a List<VideoType.Cast>
if (cursor.moveToFirst()) {
castArrayList = new ArrayList<VideoType.Cast>(cursor.getCount());
do {
castArrayList.add(new VideoType.Cast(cursor.getString(MovieCastListQuery.NAME),
cursor.getInt(MovieCastListQuery.ORDER),
cursor.getString(MovieCastListQuery.ROLE),
cursor.getString(MovieCastListQuery.THUMBNAIL)));
} while (cursor.moveToNext());
UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList,
AllCastActivity.buildLaunchIntent(getActivity(), movieTitle, castArrayList));
}
}
/**
* Checks wether we should refresh the movie details with the info on XBMC
* The details will be updated if the last update is older than what is configured in the
* settings
*
* @param cursor Cursor with the data
*/
private void checkOutdatedMovieDetails(Cursor cursor) {
if (hasIssuedOutdatedRefresh)
return;
cursor.moveToFirst();
long lastUpdated = cursor.getLong(MovieDetailsQuery.UPDATED);
if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) {
// Trigger a silent refresh
hasIssuedOutdatedRefresh = true;
startSync(true);
}
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
/**
* Movie details query parameters.
*/
private interface MovieDetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Movies.TITLE,
MediaContract.Movies.TAGLINE,
MediaContract.Movies.THUMBNAIL,
MediaContract.Movies.FANART,
MediaContract.Movies.YEAR,
MediaContract.Movies.GENRES,
MediaContract.Movies.RUNTIME,
MediaContract.Movies.RATING,
MediaContract.Movies.VOTES,
MediaContract.Movies.PLOT,
MediaContract.Movies.PLAYCOUNT,
MediaContract.Movies.DIRECTOR,
MediaContract.Movies.IMDBNUMBER,
MediaContract.Movies.FILE,
MediaContract.SyncColumns.UPDATED,
};
final int ID = 0;
final int TITLE = 1;
final int TAGLINE = 2;
final int THUMBNAIL = 3;
final int FANART = 4;
final int YEAR = 5;
final int GENRES = 6;
final int RUNTIME = 7;
final int RATING = 8;
final int VOTES = 9;
final int PLOT = 10;
final int PLAYCOUNT = 11;
final int DIRECTOR = 12;
final int IMDBNUMBER = 13;
final int FILE = 14;
final int UPDATED = 15;
}
/**
* Movie cast list query parameters.
*/
public interface MovieCastListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.MovieCast.NAME,
MediaContract.MovieCast.ORDER,
MediaContract.MovieCast.ROLE,
MediaContract.MovieCast.THUMBNAIL,
};
String SORT = MediaContract.MovieCast.ORDER + " ASC";
final int ID = 0;
final int NAME = 1;
final int ORDER = 2;
final int ROLE = 3;
final int THUMBNAIL = 4;
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.animators;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import org.xbmc.kore.utils.LogUtils;
public class ChangeImageFadeAnimation {
private Drawable fadeOutImage;
private Drawable fadeInImage;
private Drawable animatedImage;
private FloatingActionButton imageHolder;
private ValueAnimator fadeOutAnimator;
private ChangeImageFadeAnimation() {
}
public ChangeImageFadeAnimation(@NonNull FloatingActionButton imageHolder,
@NonNull Drawable fadeOutImage, @NonNull Drawable fadeInImage) {
this.fadeOutImage = fadeOutImage.getConstantState().newDrawable();
this.fadeOutImage.mutate();
this.fadeInImage = fadeInImage.getConstantState().newDrawable();
this.fadeInImage.mutate();
this.imageHolder = imageHolder;
setupAnimation();
}
public void cancel() {
fadeOutAnimator.cancel();
}
public void start() {
fadeOutAnimator.start();
}
private void setupAnimation() {
fadeOutAnimator = new ValueAnimator();
fadeOutAnimator.setIntValues(255, 0);
fadeOutAnimator.setDuration(500);
final ValueAnimator fadeInAnimator = new ValueAnimator();
fadeInAnimator.setIntValues(0, 255);
fadeInAnimator.setDuration(500);
animatedImage = fadeOutImage;
ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedImage.setAlpha((int) animation.getAnimatedValue());
}
};
fadeInAnimator.addUpdateListener(updateListener);
fadeOutAnimator.addUpdateListener(updateListener);
fadeOutAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
animatedImage = fadeInImage;
animatedImage.setAlpha(0);
imageHolder.setImageDrawable(animatedImage);
fadeInAnimator.start();
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.animators;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.graphics.PorterDuff;
import android.view.View;
public class PulsateAnimation {
private View view;
private AnimatorSet animatorSet;
private boolean stopAnimation;
private int startColor;
private int endColor;
private PulsateAnimation() {
}
public PulsateAnimation(View v, int startColor, int endColor) {
view = v;
this.startColor = startColor;
this.endColor = endColor;
setupAnimation();
}
public void start() {
stopAnimation = false;
animatorSet.start();
}
public void stop() {
stopAnimation = true;
}
public boolean isRunning() {
return animatorSet.isRunning();
}
private void setupAnimation() {
animatorSet = new AnimatorSet();
//Creates an animation that first changes color from startColor to endColor and
//afterwards changes color from endColor to startColor
animatorSet.playSequentially(createValueAnimator(startColor, endColor),
createValueAnimator(endColor, startColor));
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (!stopAnimation)
animatorSet.start();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
private ValueAnimator createValueAnimator(int startColor, int endColor) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setDuration(1000);
valueAnimator.setIntValues(startColor, endColor);
valueAnimator.setEvaluator(new ArgbEvaluator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int color = (int) animator.getAnimatedValue();
view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
});
return valueAnimator;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.behaviors;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
public class FABSpeedDialBehavior extends CoordinatorLayout.Behavior {
private boolean hide;
public FABSpeedDialBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//Make sure we respond to vertical scroll events
return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
axes, type);
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull final View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (dyConsumed > 0 && !hide) {
hide = true;
ViewCompat.animate(child).translationY(child.getHeight());
} else if (dyConsumed < 0 && hide) {
hide = false;
ViewCompat.animate(child).translationY(0);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.behaviors;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
public class FadeOutOnVerticalScrollBehavior extends CoordinatorLayout.Behavior {
private int maxScroll = 0;
private int currentScroll;
public FadeOutOnVerticalScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
if ( axes == ViewCompat.SCROLL_AXIS_VERTICAL ) {
if (maxScroll == 0)
maxScroll = child.getHeight();
return true;
}
return false;
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
currentScroll += dyConsumed;
child.setAlpha((float) ((maxScroll - currentScroll) / (double) maxScroll));
}
}

View File

@ -20,7 +20,6 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@ -30,6 +29,7 @@ import org.xbmc.kore.jsonrpc.type.AddonType;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
import java.util.Collections;
@ -86,20 +86,23 @@ public class AddonInfoFragment extends AbstractInfoFragment {
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(final FABSpeedDial FAB) {
FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FAB.enableBusyAnimation(true);
Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId);
action.execute(getHostManager().getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Do nothing
if (!isAdded()) return;
FAB.enableBusyAnimation(false);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
FAB.enableBusyAnimation(false);
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();

View File

@ -26,7 +26,6 @@ import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@ -40,6 +39,7 @@ import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
@ -172,13 +172,13 @@ public class AlbumInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(FABSpeedDial FAB) {
FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.albumid = getDataHolder().getId();
fabActionPlayItem(item);
playItemOnKodi(item);
}
});
return true;

View File

@ -26,7 +26,6 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.ImageButton;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
@ -34,10 +33,10 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.provider.MediaProvider;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncMusic;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
@ -98,13 +97,13 @@ public class ArtistInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(FABSpeedDial FAB) {
FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.artistid = getDataHolder().getId();
fabActionPlayItem(item);
playItemOnKodi(item);
}
});
return true;

View File

@ -27,7 +27,6 @@ import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@ -40,6 +39,7 @@ import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
@ -107,13 +107,13 @@ public class MusicVideoInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(FABSpeedDial FAB) {
FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = getDataHolder().getId();
fabActionPlayItem(item);
playItemOnKodi(item);
}
});
return true;

View File

@ -28,7 +28,6 @@ import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@ -44,6 +43,7 @@ import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.CastFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
@ -241,13 +241,18 @@ public class MovieInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(final FABSpeedDial FAB) {
FAB.setOnDialItemClickListener(new FABSpeedDial.DialListener() {
@Override
public void onClick(View v) {
public void onLocalPlayClicked() {
playItemLocally(movieDownloadInfo.getMediaUrl(getHostInfo()), "video/*");
}
@Override
public void onRemotePlayClicked() {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = getDataHolder().getId();
fabActionPlayItem(item);
playItemOnKodi(item);
}
});
return true;
@ -332,6 +337,8 @@ public class MovieInfoFragment extends AbstractInfoFragment
break;
}
}
getFabButton().enableLocalPlay(movieDownloadInfo != null);
}
/** {@inheritDoc} */

View File

@ -28,7 +28,6 @@ import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.ApiCallback;
@ -40,6 +39,7 @@ import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
@ -134,13 +134,18 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
FAB.setOnClickListener(new View.OnClickListener() {
protected boolean setupFAB(FABSpeedDial FAB) {
FAB.setOnDialItemClickListener(new FABSpeedDial.DialListener() {
@Override
public void onClick(View v) {
public void onLocalPlayClicked() {
playItemLocally(fileDownloadHelper.getMediaUrl(getHostInfo()), "video/*");
}
@Override
public void onRemotePlayClicked() {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = getDataHolder().getId();
fabActionPlayItem(item);
playItemOnKodi(item);
}
});
return true;
@ -198,10 +203,10 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
director = getActivity().getResources().getString(R.string.directors) + " " + director;
}
int runtime = cursor.getInt(EpisodeDetailsQuery.RUNTIME) / 60;
String durationPremiered = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
String durationPremiered = runtime > 0 ?
String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
" | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
String season = String.format(getString(R.string.season_episode),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE));
@ -228,6 +233,8 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
break;
}
}
getFabButton().enableLocalPlay(fileDownloadHelper != null);
}
/** {@inheritDoc} */
@ -237,13 +244,6 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
}
private void downloadEpisode() {
final FileDownloadHelper.TVShowInfo tvshowDownloadInfo = new FileDownloadHelper.TVShowInfo(
cursor.getString(EpisodeDetailsQuery.SHOWTITLE),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE),
cursor.getString(EpisodeDetailsQuery.TITLE),
cursor.getString(EpisodeDetailsQuery.FILE));
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
@ -251,7 +251,7 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
};
// Check if the directory exists and whether to overwrite it
File file = new File(tvshowDownloadInfo.getAbsoluteFilePath());
File file = new File(fileDownloadHelper.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
@ -261,7 +261,7 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
fileDownloadHelper, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
@ -270,7 +270,7 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
fileDownloadHelper, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
@ -286,7 +286,7 @@ public class TVShowEpisodeInfoFragment extends AbstractInfoFragment
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
fileDownloadHelper, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})

View File

@ -22,7 +22,6 @@ import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
@ -32,6 +31,7 @@ import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
/**
@ -72,7 +72,7 @@ public class TVShowInfoFragment extends AbstractInfoFragment
}
@Override
protected boolean setupFAB(ImageButton FAB) {
protected boolean setupFAB(FABSpeedDial FAB) {
return false;
}

View File

@ -0,0 +1,206 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.widgets.fabspeeddial;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Interpolator;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife;
import butterknife.InjectView;
public class DialActionButton extends LinearLayout {
@InjectView(R.id.dial_label) AppCompatTextView label;
@InjectView(R.id.dial_action_button) FloatingActionButton button;
private View anchorView;
private boolean isHiding;
private TimeInterpolator showInterpolator;
private TimeInterpolator hideInterpolator;
public DialActionButton(Context context) {
this(context, null, 0);
}
public DialActionButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DialActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeView(context, attrs, defStyleAttr);
}
public void setShowInterpolator(TimeInterpolator showInterpolator) {
this.showInterpolator = showInterpolator;
}
public void setHideInterpolator(TimeInterpolator hideInterpolator) {
this.hideInterpolator = hideInterpolator;
}
/**
* Sets the View from which the DialActionButtons should appear or disappear.
* It uses the anchorView's animation duration to set the duration for
* the DialActionButton.
* <br/>
* Use {@link #setShowInterpolator(TimeInterpolator)} and
* {@link #setHideInterpolator(TimeInterpolator)} to set the appropriate interpolators
* for this DialActionButton
* @param anchorView
*/
public void setAnchorView(View anchorView) {
this.anchorView = anchorView;
//Initialize animation
long anim_duration = anchorView.animate().getDuration();
label.setAlpha(0f);
label.animate().setDuration(anim_duration);
label.setScaleX(0f);
label.setScaleY(0f);
animate().setDuration(anim_duration);
animate().setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (isHiding) {
setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
public void show() {
isHiding = false;
setVisibility(View.VISIBLE);
if (anchorView != null) {
setY(anchorView.getY());
animate().translationY(0);
animate().setInterpolator(showInterpolator);
label.animate().setInterpolator(showInterpolator);
label.setX(anchorView.getX());
label.animate().translationX(0);
label.animate().alpha(1f);
label.animate().scaleX(1f);
label.animate().scaleY(1f);
}
}
public void hide() {
if (isHiding)
return;
if (anchorView == null) {
setVisibility(View.GONE);
} else {
isHiding = true;
animate().setInterpolator(hideInterpolator);
animate().translationY(anchorView.getY() - getY());
label.animate().setInterpolator(hideInterpolator);
label.animate().translationX(anchorView.getX() - label.getX());
label.animate().alpha(0f);
label.animate().scaleX(0f);
label.animate().scaleY(0f);
}
}
public Drawable getDrawable() {
return button.getDrawable();
}
public AppCompatTextView getLabel() {
return label;
}
public void setColorFilter(int color) {
button.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
label.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
private void initializeView(Context context, AttributeSet attrs, int defStyleAttr) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.dial_action_button, this);
ButterKnife.inject(view);
// Make sure shadow is not clipped
setClipToPadding(false);
// Make sure translation animations do not cause clipping
// by parent view group when moving outside its boundaries.
// For example, when using the overshoot interpolator.
setClipChildren(false);
Resources.Theme theme = getContext().getTheme();
TypedArray typedArray = theme.obtainStyledAttributes(attrs, new int[]{android.R.attr.text,
R.attr.iconFABDial},
defStyleAttr,
0);
String text = typedArray.getString(0);
if (text != null) {
label.setText(text);
} else {
label.setVisibility(View.GONE);
}
TypedValue typedValue = new TypedValue();
typedArray.getValue(1, typedValue);
button.setImageResource(typedValue.resourceId);
typedArray.recycle();
ColorStateList colorStateList = AppCompatResources.getColorStateList(context, R.color.fabspeeddial);
button.setBackgroundTintList(colorStateList);
}
}

View File

@ -0,0 +1,362 @@
/*
* Copyright 2017 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui.widgets.fabspeeddial;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.content.res.AppCompatResources;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.LinearLayout;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.ui.animators.ChangeImageFadeAnimation;
import org.xbmc.kore.ui.animators.PulsateAnimation;
import butterknife.ButterKnife;
import butterknife.InjectView;
/**
* The Floating Action Button Speed Dial uses a {@link FloatingActionButton} and can
* optionally show a speed dial menu. To enable the speed dials add a listener
* for the dials using {@link #setOnDialItemClickListener(DialListener)}.
*
* <p>The icons for the FAB needs to be set through your theme:
* <ul>
* <li>org.xbmc.kore.R.attr.iconFABDefault sets the icon when the dials are disabled</li>
* <li>org.xbmc.kore.R.attr.iconFABDialsOpenClose sets the icon when the dials are enabled</li>
* </ul>
* </p>
*
* <p>
* The background color can be set through your theme:
* <ul>
* <li>org.xbmc.kore.R.attr.fabColorNormal sets the default color</li>
* <li>org.xbmc.kore.R.attr.fabColorPressed sets the pressed state color</li>
* <li>org.xbmc.kore.R.attr.fabColorFocus sets the focus state color</li>
* </ul>
* </p>
*/
public class FABSpeedDial extends LinearLayout {
@InjectView(R.id.fabspeeddial) FloatingActionButton FABMain;
@InjectView(R.id.play_local) DialActionButton FABPlayLocal;
@InjectView(R.id.play_remote) DialActionButton FABPlayRemote;
private final String BUNDLE_KEY_EXPANDED = "expanded";
private final String BUNDLE_KEY_PARENT = "parent";
private final String BUNDLE_KEY_DIALCLICKED = "dialclicked";
private PulsateAnimation busyAnimation;
private DialActionButton dialSelected;
private boolean dialsVisible;
private boolean dialsEnabled;
private Drawable iconFABDefault;
private Drawable iconFABOpenClose;
private OvershootInterpolator showDialsInterpolator = new OvershootInterpolator();
private AccelerateInterpolator hideDialsInterpolator = new AccelerateInterpolator();
public interface DialListener {
void onLocalPlayClicked();
void onRemotePlayClicked();
}
private DialListener dialListener;
private OnClickListener fabListener;
public FABSpeedDial(Context context) {
this(context, null);
}
public FABSpeedDial(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FABSpeedDial(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeView(context);
}
@TargetApi(21)
public FABSpeedDial(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initializeView(context);
}
/**
* Enables/disables the speed dials. This means that if enabled,
* the dials will be shown if the user pressed the FAB button.
* @param enable true to enable the dials, false to disable
* @param animate true to use animation to change FAB icon, false to instantly change the FAB icon
*/
public void enableSpeedDials(boolean enable, boolean animate) {
if (dialsEnabled == enable)
return;
dialsEnabled = enable;
changeFABIcon(animate);
}
/**
* Add listener to handle dial button click events.
* <br/>
* Note: adding a listener for the dials also enables the speed dials if
* user didn't disable usage in settings
* @param dialListener
*/
public void setOnDialItemClickListener(DialListener dialListener) {
this.dialListener = dialListener;
// Disable speed dials if user disabled it through settings
boolean disable = PreferenceManager
.getDefaultSharedPreferences(getContext())
.getBoolean(Settings.KEY_PREF_DISABLE_LOCAL_PLAY,
Settings.DEFAULT_PREF_DISABLE_LOCAL_PLAY);
enableSpeedDials(!disable, false);
}
/**
* Add listener to handle FAB click events.
* <br/>
* Note: if the speed dials are enabled this won't be called
* when the FAB button is pressed.
* @param fabListener
*/
public void setOnFabClickListener(OnClickListener fabListener) {
this.fabListener = fabListener;
}
/**
* WARNING: Do not use this to set a listener for the FAB button.
* Use {@link #setOnFabClickListener(OnClickListener)}
* instead.
* <br/>
* {@inheritDoc}
* @param l
*/
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
super.setOnClickListener(l);
}
/**
* Enables/disables the FAB button and starts/stops the busy animation
* @param enable true to disable the FAB button and start the busy animation, false to enable
* the FAB button and stop the busy animation.
*/
public void enableBusyAnimation(boolean enable) {
if (enable) {
busyAnimation.start();
if (dialSelected != null) {
changeFABIcon(FABMain.getDrawable(), dialSelected.getDrawable());
}
FABMain.setEnabled(false);
} else {
busyAnimation.stop();
if (dialSelected != null) {
changeFABIcon(true);
dialSelected = null;
}
FABMain.setEnabled(true);
}
}
public boolean busyAnimationIsEnabled() {
return busyAnimation.isRunning();
}
public void enableLocalPlay(boolean enable) {
FABPlayLocal.setEnabled(enable);
}
public void showDials(boolean show) {
dialsVisible = show;
if (show) {
FABMain.animate().setInterpolator(showDialsInterpolator);
FABMain.animate().rotation(-45f);
FABPlayLocal.show();
FABPlayRemote.show();
} else {
FABMain.animate().setInterpolator(hideDialsInterpolator);
FABMain.animate().rotation(0f);
FABPlayLocal.hide();
FABPlayRemote.hide();
}
}
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(BUNDLE_KEY_PARENT, super.onSaveInstanceState());
bundle.putBoolean(BUNDLE_KEY_EXPANDED, dialsVisible);
if (dialSelected != null) {
bundle.putCharSequence(BUNDLE_KEY_DIALCLICKED, dialSelected.getLabel().getText());
}
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state != null) {
Bundle bundle = (Bundle) state;
super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_KEY_PARENT));
showDials(bundle.getBoolean(BUNDLE_KEY_EXPANDED));
CharSequence charSequence = bundle.getCharSequence(BUNDLE_KEY_DIALCLICKED);
if ((charSequence != null) && (! charSequence.equals(FABPlayLocal.getLabel().getText()))) {
dialSelected = FABPlayRemote;
enableBusyAnimation(true);
}
}
}
private void initializeView(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.fab_speed_dial, this);
ButterKnife.inject(view);
// Makes sure shadow is not clipped
setClipToPadding(false);
// Makes sure translation animations do not cause clipping
// by parent view group when moving outside its boundaries.
// For example, when using the overshoot interpolator.
setClipChildren(false);
setupListeners();
setupFABIcon(context);
setupDial(FABPlayLocal);
setupDial(FABPlayRemote);
}
private void setupFABIcon(Context context) {
TypedValue tv = new TypedValue();
context.getTheme().resolveAttribute(R.attr.iconFABDialsOpenClose, tv, false);
iconFABOpenClose = AppCompatResources.getDrawable(context, tv.data);
context.getTheme().resolveAttribute(R.attr.iconFABDefault, tv, false);
iconFABDefault = AppCompatResources.getDrawable(context, tv.data);
FABMain.setImageDrawable(dialsEnabled ? iconFABOpenClose : iconFABDefault);
ColorStateList colorStateList = AppCompatResources.getColorStateList(context, R.color.fabspeeddial);
int fabColorNormal = colorStateList.getColorForState(new int[] {android.R.attr.state_enabled},
R.attr.colorPrimaryDark);
int fabColorPressed = colorStateList.getColorForState(new int[] {android.R.attr.state_pressed},
R.attr.colorPrimary);
busyAnimation = new PulsateAnimation(FABMain, fabColorNormal, fabColorPressed);
FABMain.setBackgroundTintList(colorStateList);
}
private void setupDial(DialActionButton dialActionButton) {
dialActionButton.setAnchorView(FABMain);
dialActionButton.setShowInterpolator(showDialsInterpolator);
dialActionButton.setHideInterpolator(hideDialsInterpolator);
}
private void setupListeners() {
FABMain.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (dialsEnabled) {
showDials(!FABPlayLocal.isShown());
} else {
if (fabListener != null) {
fabListener.onClick(v);
} else if (dialListener != null) {
/**
* We take remote play as default and we try to fallback if dev misconfigured
* the FAB in {@link org.xbmc.kore.ui.AbstractInfoFragment#setupFAB(FABSpeedDial)}.
* This is also needed to support disabling local playback through settings.
*/
dialListener.onRemotePlayClicked();
}
}
}
});
FABPlayLocal.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialSelected = FABPlayLocal;
if (dialListener != null) {
dialListener.onLocalPlayClicked();
showDials(false);
}
}
});
FABPlayRemote.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialSelected = FABPlayRemote;
if (dialListener != null) {
dialListener.onRemotePlayClicked();
showDials(false);
}
}
});
}
private ChangeImageFadeAnimation changeImageFadeAnimation;
private void changeFABIcon(final Drawable from, final Drawable to) {
// Cancel previous animation if any
if (changeImageFadeAnimation != null)
changeImageFadeAnimation.cancel();
changeImageFadeAnimation = new ChangeImageFadeAnimation(FABMain, from, to);
changeImageFadeAnimation.start();
}
/**
* Changes the FAB icon to its default value.
* @param animate true to use an animation to change the icon, false to change it instantly
*/
private void changeFABIcon(boolean animate) {
Drawable drawable = dialsEnabled ? iconFABOpenClose : iconFABDefault;
if (animate) {
changeFABIcon(FABMain.getDrawable(), drawable);
} else {
FABMain.setImageDrawable(drawable);
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
android:color="?attr/fabColorFocused" />
<item android:state_pressed="true"
android:color="?attr/fabColorPressed" />
<item android:color="?attr/fabColorNormal" />
</selector>

View File

@ -0,0 +1,8 @@
<!-- drawacellphone_android_white_24dp_white_24dp.xml.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M17.25,18H6.75V4H17.25M14,21H10V20H14M16,1H8A3,3 0 0,0 5,4V20A3,3 0 0,0 8,23H16A3,3 0 0,0 19,20V4A3,3 0 0,0 16,1Z" />
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/plus.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FFF" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF"/>
<corners android:radius="6dp"/>
<padding android:left="5dp" android:top="5dp" android:right="5dp" android:bottom="5dp" />
</shape>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatTextView
android:id="@+id/dial_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="@dimen/default_padding"
android:layout_marginEnd="@dimen/default_padding"
app:backgroundTint="@color/fabspeeddial"
style="@style/TextAppearance.Label"
android:elevation="4dp"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/dial_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:useCompatPadding="true"
app:fabSize="mini"
app:backgroundTint="@color/fabspeeddial"/>
</merge>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.xbmc.kore.ui.widgets.fabspeeddial.DialActionButton
android:id="@+id/play_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/play_locally"
app:iconFABDial="?attr/iconRemoteDevice"
app:fabSize="mini"
android:visibility="invisible"
/>
<org.xbmc.kore.ui.widgets.fabspeeddial.DialActionButton
android:id="@+id/play_remote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/play_on_kodi"
app:iconFABDial="?attr/iconTvShows"
app:fabSize="mini"
android:visibility="invisible"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fabspeeddial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="normal"
android:layout_gravity="end"
app:useCompatPadding="true"/>
</merge>

View File

@ -15,11 +15,12 @@
limitations under the License.
-->
<RelativeLayout
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:clipChildren="false">
<ImageView
android:id="@+id/art"
@ -28,6 +29,7 @@
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
app:layout_behavior="org.xbmc.kore.ui.behaviors.FadeOutOnVerticalScrollBehavior"
android:scaleType="centerCrop"/>
<android.support.v4.widget.SwipeRefreshLayout
@ -35,7 +37,8 @@
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.melnykov.fab.ObservableScrollView
<android.support.v4.widget.NestedScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -140,14 +143,6 @@
android:src="?attr/iconSeen"
android:contentDescription="@string/seen"
android:visibility="gone"/>
<ImageButton
android:id="@+id/local_play"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconPlay"
android:contentDescription="LocalPlay"/>
</LinearLayout>
<LinearLayout
@ -252,22 +247,18 @@
android:layout_marginTop="@dimen/default_padding"/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
</android.support.v4.widget.NestedScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
<com.melnykov.fab.FloatingActionButton
<org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:layout_marginEnd="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"
app:layout_anchor="@id/media_panel"
app:layout_anchorGravity="bottom|end|right"
app:layout_behavior="org.xbmc.kore.ui.behaviors.FABSpeedDialBehavior"
app:layout_scrollFlags="scroll|enterAlways"
android:orientation="vertical"
android:visibility="gone"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
@ -277,4 +268,4 @@
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 Synced Synapse. All rights reserved.
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.
-->
<resources>
<!--<style name="DarkThemeImmersive" parent="DarkTheme">-->
<!--&lt;!&ndash; For transparent status, nav and action bar &ndash;&gt;-->
<!--<item name="android:windowTranslucentStatus">true</item>-->
<!--<item name="android:windowTranslucentNavigation">true</item>-->
<!--</style>-->
</resources>

View File

@ -31,6 +31,7 @@
<attr name="colorToolbar" format="reference|color" />
<attr name="fabColorNormal" format="reference|color" />
<attr name="fabColorFocused" format="reference|color" />
<attr name="fabColorPressed" format="reference|color" />
<attr name="colorinProgress" format="reference|color" />
@ -50,6 +51,7 @@
<attr name="iconRemoteToolbar" format="reference" />
<attr name="iconMovies" format="reference" />
<attr name="iconTvShows" format="reference" />
<attr name="iconRemoteDevice" format="reference" />
<attr name="iconMusic" format="reference" />
<attr name="iconPicture" format="reference" />
<attr name="iconHome" format="reference" />
@ -114,7 +116,12 @@
<attr name="iconOpenInNew" format="reference" />
<attr name="iconBookmark" format="reference" />
<attr name="iconFABDialsOpenClose" format="reference" />
<attr name="iconFABDefault" format="reference" />
<attr name="iconFABDial" format="integer"/>
<declare-styleable name="SquareGridLayout">
<attr name="columnCount" format="integer"/>
</declare-styleable>
</resources>
</resources>

View File

@ -23,6 +23,7 @@
<!--<color name="accent_default">@color/light_green_A700</color>-->
<color name="accent_default">@color/orange_A400</color>
<color name="accent_default_dark">@color/orange_A700</color>
<color name="accent_default_light">@color/orange_A100</color>
<color name="white">#ffffffff</color>
<color name="white_dim_50pct">#88ffffff</color>
@ -32,6 +33,7 @@
<color name="black_dim_54pct">#8a000000</color>
<color name="estuary_default">#0f85a5</color>
<color name="estuary_default_light">#17cdff</color>
<color name="estuary_default_dark">#0a5b71</color>
<!--<color name="estuary_default">#147a96</color>-->
<!--<color name="estuary_default_dark">#0f4d5e</color>-->

View File

@ -423,5 +423,8 @@
<string name="zeroprogress">0:00</string>
<string name="toggle_expand">Expand/Collapse</string>
<string name="play_locally">Play Locally</string>
<string name="disable_local_playback_support">Disable local playback support</string>
<string name="disable_local_playback_support_summary">Disables support for playing media locally on the device running Kore.</string>
</resources>

View File

@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Text styles -->
<style name="TextAppearance">
@ -332,6 +333,13 @@
<item name="android:textStyle">italic</item>
</style>
<style name="TextAppearance.Label">
<item name="android:background">@drawable/rounded_corners_shape</item>
<item name="android:colorBackground">@drawable/rounded_corners_shape</item>
<item name="android:textColor">?attr/textColorOverPrimary</item>
<item name="android:textStyle">bold</item>
</style>
<style name="ControlPad.FrameLayout">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>

View File

@ -61,8 +61,7 @@
<item name="fabColorNormal">?attr/colorPrimary</item>
<item name="fabColorPressed">?attr/colorPrimaryDark</item>
<!--<item name="fabColorNormal">@color/accent_default</item>-->
<!--<item name="fabColorPressed">@color/accent_default_dark</item>-->
<item name="fabColorFocused">@color/estuary_default_light</item>
<item name="colorinProgress">@color/yellow_800</item>
<item name="colorFinished">@color/green_600</item>
@ -87,6 +86,9 @@
<!--<item name="remoteBackgroundColorFilter">@color/dark_content_background</item>-->
<!-- Icons -->
<item name="iconFABDialsOpenClose">@drawable/ic_plus_white_24dp</item>
<item name="iconFABDefault">@drawable/ic_play_arrow_white_24dp</item>
<item name="iconRemoteDevice">@drawable/ic_cellphone_android_white_24dp</item>
<item name="iconInfo">@drawable/remote_info_white</item>
<item name="iconBack">@drawable/remote_back_white</item>
<item name="iconMenu">@drawable/remote_menu_white</item>
@ -189,6 +191,7 @@
<item name="fabColorNormal">@color/accent_default</item>
<item name="fabColorPressed">@color/accent_default_dark</item>
<item name="fabColorFocused">@color/accent_default_light</item>
<item name="colorinProgress">@color/yellow_800</item>
<item name="colorFinished">@color/light_green_600</item>
@ -213,6 +216,9 @@
<!--<item name="remoteBackgroundColorFilter">@color/light_content_background</item>-->
<!-- Icons, same for all themes, will be colored dynamically -->
<item name="iconFABDialsOpenClose">@drawable/ic_plus_white_24dp</item>
<item name="iconFABDefault">@drawable/ic_play_arrow_white_24dp</item>
<item name="iconRemoteDevice">@drawable/ic_cellphone_android_white_24dp</item>
<item name="iconInfo">@drawable/remote_info_white</item>
<item name="iconBack">@drawable/remote_back_white</item>
<item name="iconMenu">@drawable/remote_menu_white</item>

View File

@ -76,6 +76,12 @@
android:summary="@string/show_now_playing_panel_summary"
android:defaultValue="true"/>
<SwitchPreferenceCompat
android:key="pref_disable_local_play"
android:title="@string/disable_local_playback_support"
android:summary="@string/disable_local_playback_support_summary"
android:defaultValue="false"/>
<MultiSelectListPreference
android:key="pref_nav_drawer_items"
android:title="@string/nav_drawer_items"