From ce989cb4eddcecd10e771e2d565065ba44829038 Mon Sep 17 00:00:00 2001 From: Floens Date: Tue, 7 Apr 2015 22:25:34 +0200 Subject: [PATCH] Added the pinned pane Added ThumbnailView for round icons Added a recyclerview listener that allows the items to be reordered and swiped away. --- Clover/app/build.gradle | 1 + Clover/app/proguard.cfg | 4 + .../java/org/floens/chan/ChanApplication.java | 4 +- .../chan/controller/ControllerTransition.java | 2 +- .../chan/controller/NavigationController.java | 4 +- .../floens/chan/core/loader/ChanLoader.java | 6 +- .../chan/core/manager/BoardManager.java | 2 +- .../chan/core/manager/ReplyManager.java | 10 +- .../chan/core/manager/ThreadManager.java | 16 +- .../chan/core/manager/WatchManager.java | 44 +- .../core/presenter/ImageViewerPresenter.java | 22 +- .../chan/core/presenter/ThreadPresenter.java | 26 +- .../chan/core/settings/ChanSettings.java | 8 +- .../floens/chan/core/settings/Setting.java | 2 +- .../floens/chan/ui/adapter/PinAdapter.java | 113 ++- .../floens/chan/ui/adapter/PinnedAdapter.java | 8 +- .../floens/chan/ui/adapter/PostAdapter.java | 10 +- .../ui/controller/ImageViewerController.java | 21 +- .../ui/controller/PassSettingsController.java | 2 +- .../controller/RootNavigationController.java | 46 +- .../chan/ui/controller/ThreadController.java | 8 +- .../ui/controller/ViewThreadController.java | 30 + .../controller/WatchSettingsController.java | 2 +- .../chan/ui/drawable/ThumbDrawable.java | 67 ++ .../chan/ui/fragment/FolderPickFragment.java | 4 +- .../chan/ui/helper/SwipeItemAnimator.java | 656 ++++++++++++++++++ .../floens/chan/ui/helper/SwipeListener.java | 387 +++++++++++ .../floens/chan/ui/layout/CaptchaLayout.java | 4 +- .../floens/chan/ui/layout/ThreadLayout.java | 10 +- .../chan/ui/layout/ThreadListLayout.java | 6 +- .../org/floens/chan/ui/toolbar/Toolbar.java | 2 +- .../chan/ui/toolbar/ToolbarMenuItem.java | 4 +- .../chan/ui/view/CustomScaleImageView.java | 4 +- .../org/floens/chan/ui/view/FloatingMenu.java | 2 +- .../floens/chan/ui/view/MultiImageView.java | 14 +- .../org/floens/chan/ui/view/PostView.java | 48 +- .../floens/chan/ui/view/ThumbnailView.java | 142 +++- .../java/org/floens/chan/utils/FileCache.java | 6 +- Clover/app/src/main/res/layout/cell_pin.xml | 41 +- .../layout/controller_navigation_drawer.xml | 7 +- Clover/app/src/main/res/layout/pin_item.xml | 8 +- 41 files changed, 1624 insertions(+), 179 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/drawable/ThumbDrawable.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeItemAnimator.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeListener.java diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index e8431675..2514ef85 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -81,6 +81,7 @@ dependencies { compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.0' compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.1.3' compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'de.greenrobot:eventbus:2.4.0' compile files('libs/httpclientandroidlib-1.2.1.jar') } diff --git a/Clover/app/proguard.cfg b/Clover/app/proguard.cfg index a5b2c383..394246de 100644 --- a/Clover/app/proguard.cfg +++ b/Clover/app/proguard.cfg @@ -142,3 +142,7 @@ ; } -keepattributes JavascriptInterface + +-keepclassmembers class ** { + public void onEvent*(**); +} diff --git a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java index 9494323d..fd0ccc61 100644 --- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java +++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java @@ -202,7 +202,7 @@ public class ChanApplication extends Application { } } - public static interface ForegroundChangedListener { - public void onForegroundChanged(boolean foreground); + public interface ForegroundChangedListener { + void onForegroundChanged(boolean foreground); } } diff --git a/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java b/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java index 0cda6d5d..e9e2ca95 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java @@ -36,6 +36,6 @@ public abstract class ControllerTransition { } public interface Callback { - public void onControllerTransitionCompleted(ControllerTransition transition); + void onControllerTransitionCompleted(ControllerTransition transition); } } diff --git a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java index bd1c6fde..e0c0e31e 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java @@ -127,7 +127,9 @@ public abstract class NavigationController extends Controller implements Control controllerList.remove(from); } - controllerPopped(to); + if (to != null) { + controllerPopped(to); + } return true; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java index 0655ba5b..6a467fed 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java @@ -360,9 +360,9 @@ public class ChanLoader { clearTimer(); } - public static interface ChanLoaderCallback { - public void onChanLoaderData(ChanThread result); + public interface ChanLoaderCallback { + void onChanLoaderData(ChanThread result); - public void onChanLoaderError(VolleyError error); + void onChanLoaderError(VolleyError error); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java index 8371e537..0576c04d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java @@ -195,6 +195,6 @@ public class BoardManager { } public interface BoardChangeListener { - public void onBoardsChanged(); + void onBoardsChanged(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java index 4fec6de9..644a228c 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java @@ -262,8 +262,8 @@ public class ReplyManager { }); } - public static interface PassListener { - public void onResponse(PassResponse response); + public interface PassListener { + void onResponse(PassResponse response); } public static class PassResponse { @@ -336,7 +336,7 @@ public class ReplyManager { } public interface DeleteListener { - public void onResponse(DeleteResponse response); + void onResponse(DeleteResponse response); } public static class DeleteResponse { @@ -484,8 +484,8 @@ public class ReplyManager { AndroidUtils.runOnUiThread(runnable); } - public static interface ReplyListener { - public void onResponse(ReplyResponse response); + public interface ReplyListener { + void onResponse(ReplyResponse response); } public static class ReplyResponse { diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java index 85725a41..66d7efee 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java @@ -578,21 +578,21 @@ public class ThreadManager implements ChanLoader.ChanLoaderCallback { } public interface ThreadManagerListener { - public void onThreadLoaded(ChanThread thread); + void onThreadLoaded(ChanThread thread); - public void onThreadLoadError(VolleyError error); + void onThreadLoadError(VolleyError error); - public void onPostClicked(Post post); + void onPostClicked(Post post); - public void onThumbnailClicked(Post post); + void onThumbnailClicked(Post post); - public void onScrollTo(int post); + void onScrollTo(int post); - public void onRefreshView(); + void onRefreshView(); - public void onOpenThread(Loadable thread, int highlightedPost); + void onOpenThread(Loadable thread, int highlightedPost); - public ThreadManager.ViewMode getViewMode(); + ThreadManager.ViewMode getViewMode(); } public static class RepliesPopup { diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java index acfe7c91..59163cc2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java @@ -21,10 +21,10 @@ import android.content.Context; import android.content.Intent; import org.floens.chan.ChanApplication; -import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; +import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.service.WatchNotifier; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; @@ -37,6 +37,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import de.greenrobot.event.EventBus; + public class WatchManager implements ChanApplication.ForegroundChangedListener { private static final String TAG = "WatchManager"; private static final int FOREGROUND_TIME = 5; @@ -92,7 +94,7 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { } public List getWatchingPins() { - if (ChanSettings.getWatchEnabled()) { + if (ChanSettings.watchEnabled.get()) { List l = new ArrayList<>(); for (Pin p : pins) { @@ -125,6 +127,8 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { onPinsChanged(); + EventBus.getDefault().post(new PinAddedMessage(pin)); + return true; } @@ -161,6 +165,8 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { ChanApplication.getDatabaseManager().removePin(pin); onPinsChanged(); + + EventBus.getDefault().post(new PinRemovedMessage(pin)); } /** @@ -172,6 +178,8 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { ChanApplication.getDatabaseManager().updatePin(pin); onPinsChanged(); + + EventBus.getDefault().post(new PinChangedMessage(pin)); } /** @@ -217,6 +225,10 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { onPinsChanged(); updateDatabase(); + + for (Pin pin : getPins()) { + EventBus.getDefault().post(new PinChangedMessage(pin)); + } } public void onWatchEnabledChanged(boolean watchEnabled) { @@ -337,8 +349,32 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { updateTimerState(false); } - public static interface PinListener { - public void onPinsChanged(); + public interface PinListener { + void onPinsChanged(); + } + + public static class PinAddedMessage { + public Pin pin; + + public PinAddedMessage(Pin pin) { + this.pin = pin; + } + } + + public static class PinRemovedMessage { + public Pin pin; + + public PinRemovedMessage(Pin pin) { + this.pin = pin; + } + } + + public static class PinChangedMessage { + public Pin pin; + + public PinChangedMessage(Pin pin) { + this.pin = pin; + } } private static class PendingTimer { diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java index 7996d53f..9a7511d6 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java @@ -255,26 +255,26 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager. } public interface Callback { - public void startPreviewInTransition(PostImage postImage); + void startPreviewInTransition(PostImage postImage); - public void startPreviewOutTransition(PostImage postImage); + void startPreviewOutTransition(PostImage postImage); - public void setPreviewVisibility(boolean visible); + void setPreviewVisibility(boolean visible); - public void setPagerVisiblity(boolean visible); + void setPagerVisiblity(boolean visible); - public void setPagerItems(List images, int initialIndex); + void setPagerItems(List images, int initialIndex); - public void setImageMode(PostImage postImage, MultiImageView.Mode mode); + void setImageMode(PostImage postImage, MultiImageView.Mode mode); - public void setTitle(PostImage postImage); + void setTitle(PostImage postImage); - public void scrollTo(PostImage postImage); + void scrollTo(PostImage postImage); - public MultiImageView.Mode getImageMode(PostImage postImage); + MultiImageView.Mode getImageMode(PostImage postImage); - public void showProgress(boolean show); + void showProgress(boolean show); - public void onLoadProgress(float progress); + void onLoadProgress(float progress); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java index 6292fb71..473d0eb1 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java @@ -19,7 +19,6 @@ package org.floens.chan.core.presenter; import android.text.TextUtils; import android.view.Menu; -import android.widget.ImageView; import com.android.volley.VolleyError; @@ -39,6 +38,7 @@ import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.view.PostView; +import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; @@ -167,7 +167,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } @Override - public void onThumbnailClicked(Post post, ImageView thumbnail) { + public void onThumbnailClicked(Post post, ThumbnailView thumbnail) { List images = new ArrayList<>(); int index = -1; for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { @@ -341,26 +341,26 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } public interface ThreadPresenterCallback { - public void showPosts(ChanThread thread); + void showPosts(ChanThread thread); - public void showError(VolleyError error); + void showError(VolleyError error); - public void showLoading(); + void showLoading(); - public void showPostInfo(String info); + void showPostInfo(String info); - public void showPostLinkables(List linkables); + void showPostLinkables(List linkables); - public void clipboardPost(Post post); + void clipboardPost(Post post); - public void showThread(Loadable threadLoadable); + void showThread(Loadable threadLoadable); - public void openLink(String link); + void openLink(String link); - public void showPostsPopup(Post forPost, List posts); + void showPostsPopup(Post forPost, List posts); - public void showImages(List images, int index, Loadable loadable, ImageView thumbnail); + void showImages(List images, int index, Loadable loadable, ThumbnailView thumbnail); - public void scrollTo(int position); + void scrollTo(int position); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 56a0ff94..d9b0de4b 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -21,6 +21,7 @@ import android.content.SharedPreferences; import android.os.Environment; import org.floens.chan.ChanApplication; +import org.floens.chan.chan.ChanUrls; import org.floens.chan.utils.AndroidUtils; import java.io.File; @@ -79,7 +80,12 @@ public class ChanSettings { saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + "Clover"); saveOriginalFilename = new BooleanSetting(p, "preference_image_save_original", false); shareUrl = new BooleanSetting(p, "preference_image_share_url", false); - networkHttps = new BooleanSetting(p, "preference_network_https", true); + networkHttps = new BooleanSetting(p, "preference_network_https", true, new Setting.SettingCallback() { + @Override + public void onValueChange(Setting setting, Boolean value) { + ChanUrls.loadScheme(value); + } + }); forcePhoneLayout = new BooleanSetting(p, "preference_force_phone_layout", false); anonymize = new BooleanSetting(p, "preference_anonymize", false); anonymizeIds = new BooleanSetting(p, "preference_anonymize_ids", false); diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java index 9a71056a..e7c2c4f7 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java @@ -34,6 +34,6 @@ public abstract class Setting { } public interface SettingCallback { - public void onValueChange(Setting setting, T value); + void onValueChange(Setting setting, T value); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java index 992a21d0..9390ce21 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java @@ -1,36 +1,125 @@ package org.floens.chan.ui.adapter; import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -public class PinAdapter extends RecyclerView.Adapter { - @Override - public PinViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { +import org.floens.chan.ChanApplication; +import org.floens.chan.R; +import org.floens.chan.core.model.Pin; +import org.floens.chan.ui.cell.PinCell; +import org.floens.chan.ui.helper.SwipeListener; +import org.floens.chan.ui.view.ThumbnailView; + +import java.util.ArrayList; +import java.util.List; - TextView test = new TextView(parent.getContext()); +import static org.floens.chan.utils.AndroidUtils.dp; - return new PinViewHolder(test); +public class PinAdapter extends RecyclerView.Adapter implements SwipeListener.Callback { + private final Callback callback; + private List pins = new ArrayList<>(); + + public PinAdapter(Callback callback) { + this.callback = callback; + } + + @Override + public PinViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + PinCell pinCell = (PinCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_pin, parent, false); + return new PinViewHolder(pinCell); } @Override public void onBindViewHolder(PinViewHolder holder, int position) { - ((TextView)holder.itemView).setText("Position = " + position); + final Pin pin = pins.get(position); + + holder.textView.setText(pin.loadable.title); + holder.image.setUrl(pin.thumbnailUrl, dp(40), dp(40)); } @Override public int getItemCount() { - return 1000; + return pins.size(); } - public static class PinViewHolder extends RecyclerView.ViewHolder { - private View itemView; + public void onPinsChanged(List pins) { + this.pins.clear(); + this.pins.addAll(pins); + notifyDataSetChanged(); + } + + public void onPinAdded(Pin pin) { + pins.add(pin); + notifyItemInserted(pins.size() - 1); + } - public PinViewHolder(View itemView) { - super(itemView); - this.itemView = itemView; + public void onPinRemoved(Pin pin) { + // TODO: this is a workaround for recyclerview crashing when the last item is removed, remove this when it is fixed + if (pins.size() == 1) { + pins.remove(pin); + notifyDataSetChanged(); + } else { + int location = pins.indexOf(pin); + pins.remove(pin); + notifyItemRemoved(location); } } + public void onPinChanged(Pin pin) { + notifyItemChanged(pins.indexOf(pin)); + } + + @Override + public SwipeListener.Swipeable getSwipeable(int position) { + return SwipeListener.Swipeable.RIGHT; + } + + @Override + public void removeItem(int position) { + ChanApplication.getWatchManager().removePin(pins.get(position)); + } + + @Override + public boolean isMoveable(int position) { + return true; + } + + @Override + public void moveItem(int from, int to) { + Pin item = pins.remove(from); + pins.add(to, item); + notifyItemMoved(from, to); + } + + @Override + public void movingDone() { + } + + public class PinViewHolder extends RecyclerView.ViewHolder { + private PinCell pinCell; + private ThumbnailView image; + private TextView textView; + + public PinViewHolder(PinCell pinCell) { + super(pinCell); + this.pinCell = pinCell; + image = (ThumbnailView) pinCell.findViewById(R.id.thumb); + image.setCircular(true); + textView = (TextView) pinCell.findViewById(R.id.text); + + pinCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + callback.onPinClicked(pins.get(getAdapterPosition())); + } + }); + } + } + + public interface Callback { + void onPinClicked(Pin pin); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java index 8844c0f1..12380549 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java @@ -119,7 +119,7 @@ public class PinnedAdapter extends BaseAdapter { convertView = LayoutInflater.from(context).inflate(R.layout.pin_item, null); } - CustomNetworkImageView imageView = (CustomNetworkImageView) convertView.findViewById(R.id.pin_image); + CustomNetworkImageView imageView = (CustomNetworkImageView) convertView.findViewById(R.id.image); if (pin.thumbnailUrl != null) { imageView.setVisibility(View.VISIBLE); imageView.setFadeIn(0); @@ -129,14 +129,14 @@ public class PinnedAdapter extends BaseAdapter { imageView.setVisibility(View.GONE); } - ((TextView) convertView.findViewById(R.id.pin_text)).setText(pin.loadable.title); + ((TextView) convertView.findViewById(R.id.text)).setText(pin.loadable.title); - FrameLayout timeContainer = (FrameLayout) convertView.findViewById(R.id.pin_time_container); + FrameLayout timeContainer = (FrameLayout) convertView.findViewById(R.id.time_container); FrameLayout countContainer = (FrameLayout) convertView.findViewById(R.id.pin_count_container); if (ChanSettings.getWatchEnabled()) { countContainer.setVisibility(View.VISIBLE); - TextView timeView = (TextView) convertView.findViewById(R.id.pin_time); + TextView timeView = (TextView) convertView.findViewById(R.id.time); if (pin.watching && pin.getPinWatcher() != null && ChanSettings.getWatchCountdownVisibleEnabled()) { timeContainer.setVisibility(View.VISIBLE); long timeRaw = pin.getPinWatcher().getTimeUntilNextLoad(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java index aeda67c4..d1c5fc19 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java @@ -287,15 +287,15 @@ public class PostAdapter extends BaseAdapter implements Filterable { } public interface PostAdapterCallback { - public void onFilteredResults(String filter, int count, boolean all); + void onFilteredResults(String filter, int count, boolean all); - public Loadable getLoadable(); + Loadable getLoadable(); - public void onListScrolledToBottom(); + void onListScrolledToBottom(); - public void onListStatusClicked(); + void onListStatusClicked(); - public void scrollTo(int position); + void scrollTo(int position); } public class StatusView extends LinearLayout { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java index 33262c2b..41ad427f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java @@ -11,14 +11,12 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Point; import android.graphics.PointF; -import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.Window; import android.view.animation.DecelerateInterpolator; -import android.widget.ImageView; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; @@ -41,6 +39,7 @@ import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.view.LoadingBar; import org.floens.chan.ui.view.MultiImageView; import org.floens.chan.ui.view.OptionalSwipeViewPager; +import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.TransitionImageView; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.ImageSaver; @@ -234,7 +233,7 @@ public class ImageViewerController extends Controller implements View.OnClickLis } public void startPreviewInTransition(PostImage postImage) { - ImageView startImageView = getTransitionImageView(postImage); + ThumbnailView startImageView = getTransitionImageView(postImage); if (!setTransitionViewData(startImageView)) { Logger.test("Oops"); @@ -307,7 +306,7 @@ public class ImageViewerController extends Controller implements View.OnClickLis } } - ImageView startImage = getTransitionImageView(postImage); + ThumbnailView startImage = getTransitionImageView(postImage); endAnimation = new AnimatorSet(); if (!setTransitionViewData(startImage) || bitmap == null) { @@ -354,12 +353,12 @@ public class ImageViewerController extends Controller implements View.OnClickLis navigationController.stopPresenting(false); } - private boolean setTransitionViewData(ImageView startView) { + private boolean setTransitionViewData(ThumbnailView startView) { if (startView == null || startView.getWindowToken() == null) { return false; } - Bitmap bitmap = ((BitmapDrawable) startView.getDrawable()).getBitmap(); + Bitmap bitmap = startView.getBitmap(); if (bitmap == null) { return false; } @@ -399,7 +398,7 @@ public class ImageViewerController extends Controller implements View.OnClickLis toolbar.setAlpha(alpha); } - private ImageView getTransitionImageView(PostImage postImage) { + private ThumbnailView getTransitionImageView(PostImage postImage) { return previewCallback.getPreviewImageTransitionView(this, postImage); } @@ -408,12 +407,12 @@ public class ImageViewerController extends Controller implements View.OnClickLis } public interface PreviewCallback { - public ImageView getPreviewImageTransitionView(ImageViewerController imageViewerController, PostImage postImage); + ThumbnailView getPreviewImageTransitionView(ImageViewerController imageViewerController, PostImage postImage); - public void onPreviewCreate(ImageViewerController imageViewerController); + void onPreviewCreate(ImageViewerController imageViewerController); - public void onPreviewDestroy(ImageViewerController imageViewerController); + void onPreviewDestroy(ImageViewerController imageViewerController); - public void scrollTo(PostImage postImage); + void scrollTo(PostImage postImage); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java index 881adbe9..1f5e087e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java @@ -148,6 +148,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi } public interface PassSettingControllerListener { - public void onPassEnabledChanged(boolean enabled); + void onPassEnabledChanged(boolean enabled); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java index 0fa9cf49..7942bf73 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java @@ -20,26 +20,32 @@ package org.floens.chan.ui.controller; import android.content.Context; import android.content.res.Configuration; import android.support.v4.widget.DrawerLayout; -import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; +import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.controller.Controller; import org.floens.chan.controller.NavigationController; +import org.floens.chan.core.manager.WatchManager; +import org.floens.chan.core.model.Pin; import org.floens.chan.ui.adapter.PinAdapter; +import org.floens.chan.ui.helper.SwipeListener; import org.floens.chan.ui.toolbar.Toolbar; import org.floens.chan.utils.AndroidUtils; +import de.greenrobot.event.EventBus; + import static org.floens.chan.utils.AndroidUtils.dp; -public class RootNavigationController extends NavigationController { +public class RootNavigationController extends NavigationController implements PinAdapter.Callback { public DrawerLayout drawerLayout; public FrameLayout drawer; private RecyclerView recyclerView; + private PinAdapter pinAdapter; public RootNavigationController(Context context) { super(context); @@ -49,20 +55,22 @@ public class RootNavigationController extends NavigationController { public void onCreate() { super.onCreate(); + EventBus.getDefault().register(this); + view = inflateRes(R.layout.controller_navigation_drawer); toolbar = (Toolbar) view.findViewById(R.id.toolbar); container = (FrameLayout) view.findViewById(R.id.container); drawerLayout = (DrawerLayout) view.findViewById(R.id.drawer_layout); drawer = (FrameLayout) view.findViewById(R.id.drawer); recyclerView = (RecyclerView) view.findViewById(R.id.drawer_recycler_view); - recyclerView.setHasFixedSize(true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(linearLayoutManager); + pinAdapter = new PinAdapter(this); + recyclerView.setAdapter(pinAdapter); - PinAdapter adapter = new PinAdapter(); - recyclerView.setAdapter(adapter); + new SwipeListener(context, recyclerView, pinAdapter); + + pinAdapter.onPinsChanged(ChanApplication.getWatchManager().getPins()); toolbar.setCallback(this); @@ -74,6 +82,13 @@ public class RootNavigationController extends NavigationController { }); } + @Override + public void onDestroy() { + super.onDestroy(); + + EventBus.getDefault().unregister(this); + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -105,6 +120,23 @@ public class RootNavigationController extends NavigationController { setDrawerEnabled(controller.navigationItem.hasDrawer); } + @Override + public void onPinClicked(Pin pin) { + + } + + public void onEvent(WatchManager.PinAddedMessage message) { + pinAdapter.onPinAdded(message.pin); + } + + public void onEvent(WatchManager.PinRemovedMessage message) { + pinAdapter.onPinRemoved(message.pin); + } + + public void onEvent(WatchManager.PinChangedMessage message) { + pinAdapter.onPinChanged(message.pin); + } + private void setDrawerEnabled(boolean enabled) { drawerLayout.setDrawerLockMode(enabled ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java index 5d6c170b..98f03644 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java @@ -1,12 +1,12 @@ package org.floens.chan.ui.controller; import android.content.Context; -import android.widget.ImageView; import org.floens.chan.controller.Controller; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.PostImage; import org.floens.chan.ui.layout.ThreadLayout; +import org.floens.chan.ui.view.ThumbnailView; import java.util.List; @@ -22,9 +22,9 @@ public abstract class ThreadController extends Controller implements ThreadLayou } @Override - public void showImages(List images, int index, Loadable loadable, final ImageView thumbnail) { + public void showImages(List images, int index, Loadable loadable, final ThumbnailView thumbnail) { // Just ignore the showImages request when the image is not loaded - if (thumbnail.getDrawable() != null && thumbnail.getDrawable().getIntrinsicWidth() > 0 && thumbnail.getDrawable().getIntrinsicHeight() > 0) { + if (thumbnail.getBitmap() != null) { final ImageViewerNavigationController imageViewerNavigationController = new ImageViewerNavigationController(context); presentController(imageViewerNavigationController, false); imageViewerNavigationController.showImages(images, index, loadable, this); @@ -32,7 +32,7 @@ public abstract class ThreadController extends Controller implements ThreadLayou } @Override - public ImageView getPreviewImageTransitionView(ImageViewerController imageViewerController, PostImage postImage) { + public ThumbnailView getPreviewImageTransitionView(ImageViewerController imageViewerController, PostImage postImage) { return threadLayout.getThumbnail(postImage); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java index 3023ca15..4ea0b1e6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -21,8 +21,10 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.model.Loadable; import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.toolbar.ToolbarMenu; @@ -32,6 +34,8 @@ import org.floens.chan.utils.AndroidUtils; import java.util.Arrays; +import de.greenrobot.event.EventBus; + public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback { private static final int POST_ID = 1; private static final int PIN_ID = 2; @@ -54,6 +58,8 @@ public class ViewThreadController extends ThreadController implements ThreadLayo public void onCreate() { super.onCreate(); + EventBus.getDefault().register(this); + view.setBackgroundColor(0xffffffff); threadLayout.getPresenter().bindLoadable(loadable); @@ -74,6 +80,25 @@ public class ViewThreadController extends ThreadController implements ThreadLayo setPinIconState(threadLayout.getPresenter().isPinned()); } + @Override + public void onDestroy() { + super.onDestroy(); + + EventBus.getDefault().unregister(this); + } + + public void onEvent(WatchManager.PinAddedMessage message) { + setPinIconState(); + } + + public void onEvent(WatchManager.PinRemovedMessage message) { + setPinIconState(); + } + + public void onEvent(WatchManager.PinChangedMessage message) { + setPinIconState(); + } + @Override public void openThread(Loadable threadLoadable) { // TODO implement, scroll to post and fix title @@ -119,6 +144,11 @@ public class ViewThreadController extends ThreadController implements ThreadLayo } } + private void setPinIconState() { + WatchManager wm = ChanApplication.getWatchManager(); + setPinIconState(wm.findPinByLoadable(loadable) != null); + } + private void setPinIconState(boolean pinned) { pinItem.setImage(pinned ? R.drawable.ic_bookmark_filled : R.drawable.ic_bookmark); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java index f2a8becf..784d42df 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java @@ -107,6 +107,6 @@ public class WatchSettingsController extends SettingsController implements Compo } public interface WatchSettingControllerListener { - public void onWatchEnabledChanged(boolean enabled); + void onWatchEnabledChanged(boolean enabled); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/drawable/ThumbDrawable.java b/Clover/app/src/main/java/org/floens/chan/ui/drawable/ThumbDrawable.java new file mode 100644 index 00000000..5f6c1031 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/drawable/ThumbDrawable.java @@ -0,0 +1,67 @@ +package org.floens.chan.ui.drawable; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +import static org.floens.chan.utils.AndroidUtils.dp; + + +public class ThumbDrawable extends Drawable { + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Path path = new Path(); + private int width; + private int height; + + public ThumbDrawable() { + width = dp(40); + height = dp(40); + + paint.setStrokeWidth(dp(2)); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setColor(0xff757575); + + path.reset(); + for (int i = 0; i < 3; i++) { + int top = (int) (getMinimumHeight() / 2f + (i - 1) * dp(6)); + path.moveTo(dp(8), top); + path.lineTo(getMinimumWidth() - dp(8), top); + } + path.moveTo(0f, 0f); + path.close(); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawPath(path, paint); + } + + @Override + public int getIntrinsicWidth() { + return width; + } + + @Override + public int getIntrinsicHeight() { + return height; + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + paint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java index 35460188..1e02bd84 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java @@ -195,7 +195,7 @@ public class FolderPickFragment extends DialogFragment { adapter.notifyDataSetChanged(); } - public static interface FolderPickListener { - public void folderPicked(File path); + public interface FolderPickListener { + void folderPicked(File path); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeItemAnimator.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeItemAnimator.java new file mode 100644 index 00000000..64a94ee4 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeItemAnimator.java @@ -0,0 +1,656 @@ +package org.floens.chan.ui.helper; + +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * This is an adaption of the {@link android.support.v7.widget.DefaultItemAnimator} but to animate + * swipes to the left or right. + */ +public class SwipeItemAnimator extends RecyclerView.ItemAnimator { + private static final boolean DEBUG = true; + + private ArrayList mPendingRemovals = new ArrayList(); + private ArrayList mPendingAdditions = new ArrayList(); + private ArrayList mPendingMoves = new ArrayList(); + private ArrayList mPendingChanges = new ArrayList(); + + private ArrayList> mAdditionsList = + new ArrayList>(); + private ArrayList> mMovesList = new ArrayList>(); + private ArrayList> mChangesList = new ArrayList>(); + + private ArrayList mAddAnimations = new ArrayList(); + private ArrayList mMoveAnimations = new ArrayList(); + private ArrayList mRemoveAnimations = new ArrayList(); + private ArrayList mChangeAnimations = new ArrayList(); + + private static class MoveInfo { + public RecyclerView.ViewHolder holder; + public int fromX, fromY, toX, toY; + + private MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public RecyclerView.ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (RecyclerView.ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + RecyclerView.ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + public void run() { + for (RecyclerView.ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final RecyclerView.ViewHolder holder) { + endAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + public static class SwipeAnimationData { + public long time; + public boolean right; + } + + private Map removeData = new HashMap<>(); + + public void addRemoveData(View view, SwipeAnimationData data) { + removeData.put(view, data); + } + + private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + + SwipeAnimationData data = removeData.remove(view); + if (data == null) { + data = new SwipeAnimationData(); + data.time = getRemoveDuration(); + data.right = true; + } + + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + animation.setDuration(data.time) + .translationX(data.right ? view.getWidth() : -view.getWidth()) + .alpha(0) + .setInterpolator(new LinearInterpolator()) + .setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + + // Reset view properties + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0f); + + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + mRemoveAnimations.add(holder); + } + + @Override + public boolean animateAdd(final RecyclerView.ViewHolder holder) { + endAnimation(holder); + ViewCompat.setAlpha(holder.itemView, 0); + mPendingAdditions.add(holder); + return true; + } + + private void animateAddImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + mAddAnimations.add(holder); + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + animation.alpha(1).setInterpolator(new AccelerateDecelerateInterpolator()).setDuration(getAddDuration()). + setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + ViewCompat.setAlpha(view, 1); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += ViewCompat.getTranslationX(holder.itemView); + fromY += ViewCompat.getTranslationY(holder.itemView); + endAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + ViewCompat.setTranslationX(view, -deltaX); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, -deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + private void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + ViewCompat.animate(view).translationX(0); + } + if (deltaY != 0) { + ViewCompat.animate(view).translationY(0); + } + ViewCompat.animate(view).alpha(1f); + // TDO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + mMoveAnimations.add(holder); + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + animation.setDuration(getMoveDuration()).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + if (deltaX != 0) { + ViewCompat.setTranslationX(view, 0); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, 0); + } + ViewCompat.setAlpha(view, 1f); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); + final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); + final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); + endAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); + ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); + ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); + if (newHolder != null && newHolder.itemView != null) { + // carry over translation values + endAnimation(newHolder); + ViewCompat.setTranslationX(newHolder.itemView, -deltaX); + ViewCompat.setTranslationY(newHolder.itemView, -deltaY); + ViewCompat.setAlpha(newHolder.itemView, 0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + private void animateChangeImpl(final ChangeInfo changeInfo) { + final RecyclerView.ViewHolder holder = changeInfo.oldHolder; + final View view = holder.itemView; + final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + mChangeAnimations.add(changeInfo.oldHolder); + + final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( + getChangeDuration()).setInterpolator(new AccelerateDecelerateInterpolator()); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(View view) { + oldViewAnim.setListener(null); + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0); + ViewCompat.setTranslationY(view, 0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + if (newView != null) { + mChangeAnimations.add(changeInfo.newHolder); + final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). + alpha(1).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + + @Override + public void onAnimationEnd(View view) { + newViewAnimation.setListener(null); + ViewCompat.setAlpha(newView, 1); + ViewCompat.setTranslationX(newView, 0); + ViewCompat.setTranslationY(newView, 0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + ViewCompat.setAlpha(item.itemView, 1); + ViewCompat.setTranslationX(item.itemView, 0); + ViewCompat.setTranslationY(item.itemView, 0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(RecyclerView.ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + ViewCompat.animate(view).cancel(); + // TDO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + mPendingMoves.remove(item); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + + // animations should be ended by the cancel above. + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() || + !mPendingChanges.isEmpty() || + !mPendingMoves.isEmpty() || + !mPendingRemovals.isEmpty() || + !mMoveAnimations.isEmpty() || + !mRemoveAnimations.isEmpty() || + !mAddAnimations.isEmpty() || + !mChangeAnimations.isEmpty() || + !mMovesList.isEmpty() || + !mAdditionsList.isEmpty() || + !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + private void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingAdditions.get(i); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + RecyclerView.ViewHolder item = moveInfo.holder; + View view = item.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + RecyclerView.ViewHolder item = additions.get(j); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + ViewCompat.animate(viewHolders.get(i).itemView).cancel(); + } + } + + private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { + @Override + public void onAnimationStart(View view) { + } + + @Override + public void onAnimationEnd(View view) { + } + + @Override + public void onAnimationCancel(View view) { + } + } + + ; +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeListener.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeListener.java new file mode 100644 index 00000000..9851b392 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeListener.java @@ -0,0 +1,387 @@ +package org.floens.chan.ui.helper; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +import org.floens.chan.R; + +import static org.floens.chan.utils.AndroidUtils.dp; + + +/** + * An ItemDecorator and Touch listener that enabled the list to be reordered and items to be swiped away. + * It isn't perfect, but works good for what is should do. + */ +public class SwipeListener extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener { + public enum Swipeable { + NO, + LEFT, + RIGHT, + BOTH; + } + + private static final String TAG = "SwipeListener"; + private static final int MAX_SCROLL_SPEED = 14; // dp/s + + private final int slopPixels; + private final int flingPixels; + private final int maxFlingPixels; + + private final Context context; + private Callback callback; + private final RecyclerView recyclerView; + private final LinearLayoutManager layoutManager; + private final SwipeItemAnimator swipeItemAnimator; + + private VelocityTracker tracker; + private boolean swiping; + private float touchDownX; + private float touchDownY; + private float totalScrolled; + private float touchDownOffsetX; + private float offsetX; + private View downView; + + private boolean dragging; + private boolean somePositionChanged = false; + private int dragPosition = -1; + private float offsetY; + private float touchDownOffsetY; + + private final Runnable scrollRunnable = new Runnable() { + @Override + public void run() { + if (dragging) { + float scroll; + boolean up = offsetY < recyclerView.getHeight() / 2f; + if (up) { + scroll = Math.max(-dp(MAX_SCROLL_SPEED), (offsetY - recyclerView.getHeight() / 6f) * 0.1f); + } else { + scroll = Math.min(dp(MAX_SCROLL_SPEED), (offsetY - recyclerView.getHeight() * 5f / 6f) * 0.1f); + } + + if (up && scroll < 0f && layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { + recyclerView.scrollBy(0, (int) scroll); + } else if (!up && scroll > 0f && layoutManager.findLastCompletelyVisibleItemPosition() != recyclerView.getAdapter().getItemCount() - 1) { + recyclerView.scrollBy(0, (int) scroll); + } + + if (scroll != 0) { + processDrag(); + recyclerView.post(scrollRunnable); + } + } + } + }; + + public SwipeListener(Context context, RecyclerView rv, Callback callback) { + this.context = context; + recyclerView = rv; + this.callback = callback; + + layoutManager = new LinearLayoutManager(context); + rv.setLayoutManager(layoutManager); + swipeItemAnimator = new SwipeItemAnimator(); + swipeItemAnimator.setMoveDuration(250); + rv.setItemAnimator(swipeItemAnimator); + rv.addOnItemTouchListener(this); + rv.addItemDecoration(this); + + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + slopPixels = viewConfiguration.getScaledTouchSlop(); + flingPixels = viewConfiguration.getScaledMinimumFlingVelocity(); + maxFlingPixels = viewConfiguration.getScaledMaximumFlingVelocity(); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + totalScrolled = 0f; + touchDownX = e.getRawX(); + touchDownY = e.getRawY(); + downView = rv.findChildViewUnder(e.getX(), e.getY()); + if (downView == null) { + // There can be gaps when a move animation is running + break; + } + + // Dragging gets initiated immediately if the touch went down on the thumb area + // Do not allow dragging when animations are running + View thumbView = downView.findViewById(R.id.thumb); + if (thumbView != null && e.getX() < rv.getPaddingLeft() + thumbView.getRight() && !swipeItemAnimator.isRunning()) { + int touchAdapterPos = rv.getChildAdapterPosition(downView); + if (touchAdapterPos < 0 || !callback.isMoveable(touchAdapterPos)) { + break; + } + + dragging = true; + dragPosition = touchAdapterPos; + + rv.post(scrollRunnable); + + offsetY = e.getY(); + touchDownOffsetY = offsetY - downView.getTop(); + + downView.setVisibility(View.INVISIBLE); + rv.invalidate(); + + return true; + } + + // Didn't went down on the thumb area, start up the tracker + if (tracker != null) { + Log.w(TAG, "Tracker was not null, recycling extra"); + tracker.recycle(); + } + tracker = VelocityTracker.obtain(); + tracker.addMovement(e); + break; + case MotionEvent.ACTION_MOVE: + if (dragging) { + return true; + } + + float deltaX = e.getRawX() - touchDownX; + float deltaY = e.getRawY() - touchDownY; + totalScrolled += Math.abs(deltaY); + int adapterPosition = rv.getChildAdapterPosition(downView); + if (adapterPosition < 0) { + break; + } + + if (swiping) { + return true; + } else { + // Logic to find out if a swipe should be initiated + Swipeable swipeable = callback.getSwipeable(adapterPosition); + if (swipeable != Swipeable.NO && Math.abs(deltaX) >= slopPixels && totalScrolled < slopPixels) { + boolean wasSwiped = false; + if (swipeable == Swipeable.BOTH) { + wasSwiped = true; + } else if (swipeable == Swipeable.LEFT && deltaX < -slopPixels) { + wasSwiped = true; + } else if (swipeable == Swipeable.RIGHT && deltaX > slopPixels) { + wasSwiped = true; + } + + if (wasSwiped) { + swiping = true; + touchDownOffsetX = deltaX; + return true; + } + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + reset(); + break; + } + + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + if (swiping) { + float deltaX = e.getRawX() - touchDownX; + switch (e.getActionMasked()) { + case MotionEvent.ACTION_MOVE: { + tracker.addMovement(e); + offsetX = deltaX - touchDownOffsetX; + downView.setTranslationX(offsetX); + downView.setAlpha(Math.min(1f, Math.max(0f, 1f - (Math.abs(offsetX) / (float) downView.getWidth())))); + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + boolean reset = false; + + int adapterPosition = rv.getChildAdapterPosition(downView); + if (adapterPosition < 0) { + reset = true; + } else if (e.getActionMasked() == MotionEvent.ACTION_UP) { + tracker.addMovement(e); + tracker.computeCurrentVelocity(1000); + float xVelocity = tracker.getXVelocity(); + + if (Math.abs(xVelocity) > flingPixels && Math.abs(xVelocity) < maxFlingPixels && + (xVelocity < 0) == (deltaX < 0) // Swiping in the same direction + ) { + SwipeItemAnimator.SwipeAnimationData data = new SwipeItemAnimator.SwipeAnimationData(); + data.right = xVelocity > 0; + + // Remove animations are linear, calculate the time here to mimic the fling speed + float timeLeft = (rv.getWidth() - Math.abs(offsetX)) / Math.abs(xVelocity); + timeLeft = Math.min(0.5f, timeLeft); + data.time = (long) (timeLeft * 1000f); + swipeItemAnimator.addRemoveData(downView, data); + callback.removeItem(rv.getChildAdapterPosition(downView)); + } else { + reset = true; + } + } else { + reset = true; + } + + // The item should be reset to its original alpha and position. + // Otherwise our SwipeItemAnimator will handle the swipe remove animation + if (reset) { + swipeItemAnimator.animateMove(rv.getChildViewHolder(downView), 0, 0, 0, 0); + swipeItemAnimator.runPendingAnimations(); + } + + reset(); + break; + } + } + } else if (dragging) { + // Invalidate hover view + recyclerView.invalidate(); + + switch (e.getActionMasked()) { + case MotionEvent.ACTION_MOVE: { + offsetY = e.getY(); + + processDrag(); + + break; + } + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (somePositionChanged) { + callback.movingDone(); + } + + RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(downView); + swipeItemAnimator.endAnimation(vh); + float floatingViewPos = offsetY - touchDownOffsetY - downView.getTop(); + swipeItemAnimator.animateMove(vh, 0, (int) floatingViewPos, 0, 0); + swipeItemAnimator.runPendingAnimations(); + + reset(); + break; + } + } + } + } + + private void processDrag() { + float floatingViewPos = offsetY - touchDownOffsetY + downView.getHeight() / 2f; + + View viewAtPosition = null; + + // like findChildUnder, but without looking at the x axis + for (int c = layoutManager.getChildCount(), i = c - 1; i >= 0; i--) { + final View child = layoutManager.getChildAt(i); + if (floatingViewPos >= child.getTop() && floatingViewPos <= child.getBottom()) { + viewAtPosition = child; + break; + } + } + + if (viewAtPosition == null) { + return; + } + + int touchAdapterPos = recyclerView.getChildAdapterPosition(viewAtPosition); + if (touchAdapterPos < 0) { + return; + } + + int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition(); + int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition(); + + if (touchAdapterPos < firstCompletelyVisible || touchAdapterPos > lastCompletelyVisible) { + return; + } + + if ((touchAdapterPos > dragPosition && floatingViewPos > viewAtPosition.getTop() + viewAtPosition.getHeight() / 5f) || + (touchAdapterPos < dragPosition && floatingViewPos < viewAtPosition.getTop() + viewAtPosition.getHeight() * 4f / 5f)) { + callback.moveItem(dragPosition, touchAdapterPos); + dragPosition = touchAdapterPos; + somePositionChanged = true; + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + if (swiping && this.downView == view) { + outRect.set((int) offsetX, 0, (int) -offsetX, 0); + } else { + outRect.set(0, 0, 0, 0); + } + } + + @Override + public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + if (dragging) { + for (int i = 0, c = layoutManager.getChildCount(); i < c; i++) { + View child = layoutManager.getChildAt(i); + if (child.getVisibility() != View.VISIBLE) { + child.setVisibility(View.VISIBLE); + } + } + + RecyclerView.ViewHolder vh = parent.findViewHolderForAdapterPosition(dragPosition); + if (vh != null) { + vh.itemView.setVisibility(View.INVISIBLE); + } + } + } + + @Override + public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + if (dragging) { + RecyclerView.ViewHolder vh = parent.findViewHolderForAdapterPosition(dragPosition); + if (vh != null) { + int left = parent.getPaddingLeft(); + int top = (int) offsetY - (int) touchDownOffsetY; + canvas.save(); + canvas.translate(left, top); + vh.itemView.draw(canvas); + canvas.restore(); + } + } + } + + private void reset() { + if (tracker != null) { + tracker.recycle(); + tracker = null; + } + downView = null; + for (int i = 0, c = layoutManager.getChildCount(); i < c; i++) { + View child = layoutManager.getChildAt(i); + if (child.getVisibility() != View.VISIBLE) { + child.setVisibility(View.VISIBLE); + } + } + swiping = false; + offsetX = 0f; + dragging = false; + dragPosition = -1; + somePositionChanged = false; + } + + public interface Callback { + Swipeable getSwipeable(int position); + + void removeItem(int position); + + boolean isMoveable(int position); + + void moveItem(int from, int to); + + void movingDone(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java index bd13ade6..63777362 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java @@ -94,9 +94,9 @@ public class CaptchaLayout extends WebView { } public interface CaptchaCallback { - public void captchaLoaded(CaptchaLayout captchaLayout); + void captchaLoaded(CaptchaLayout captchaLayout); - public void captchaEntered(CaptchaLayout captchaLayout, String response); + void captchaEntered(CaptchaLayout captchaLayout, String response); } public static class CaptchaInterface { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index 0b39cca9..dc3a843a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -23,7 +23,6 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.util.AttributeSet; -import android.widget.ImageView; import android.widget.Toast; import com.android.volley.VolleyError; @@ -38,6 +37,7 @@ import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.helper.PostPopupHelper; import org.floens.chan.ui.view.LoadView; +import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; import java.util.List; @@ -164,7 +164,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres } @Override - public void showImages(List images, int index, Loadable loadable, ImageView thumbnail) { + public void showImages(List images, int index, Loadable loadable, ThumbnailView thumbnail) { callback.showImages(images, index, loadable, thumbnail); } @@ -173,7 +173,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres threadListLayout.scrollTo(position); } - public ImageView getThumbnail(PostImage postImage) { + public ThumbnailView getThumbnail(PostImage postImage) { return threadListLayout.getThumbnail(postImage); } @@ -185,8 +185,8 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres } public interface ThreadLayoutCallback { - public void openThread(Loadable threadLoadable); + void openThread(Loadable threadLoadable); - public void showImages(List images, int index, Loadable loadable, ImageView thumbnail); + void showImages(List images, int index, Loadable loadable, ThumbnailView thumbnail); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index 17d7e521..c5b4adde 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -20,7 +20,6 @@ package org.floens.chan.ui.layout; import android.content.Context; import android.util.AttributeSet; import android.view.View; -import android.widget.ImageView; import android.widget.ListView; import android.widget.RelativeLayout; @@ -31,6 +30,7 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostImage; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.view.PostView; +import org.floens.chan.ui.view.ThumbnailView; /** * A layout that wraps around a listview to manage showing posts. @@ -98,8 +98,8 @@ public class ThreadListLayout extends RelativeLayout { } - public ImageView getThumbnail(PostImage postImage) { - ImageView thumbnail = null; + public ThumbnailView getThumbnail(PostImage postImage) { + ThumbnailView thumbnail = null; for (int i = 0; i < listView.getChildCount(); i++) { View view = listView.getChildAt(i); if (view instanceof PostView) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java index 66434550..70b21199 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java @@ -273,6 +273,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } public interface ToolbarCallback { - public void onMenuOrBackClicked(boolean isArrow); + void onMenuOrBackClicked(boolean isArrow); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java index dbfe18e1..170080f1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java @@ -102,8 +102,8 @@ public class ToolbarMenuItem implements View.OnClickListener, FloatingMenu.Float } public interface ToolbarMenuItemCallback { - public void onMenuItemClicked(ToolbarMenuItem item); + void onMenuItemClicked(ToolbarMenuItem item); - public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item); + void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java index 9c1ce52c..c9e02cf7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java @@ -70,7 +70,7 @@ public class CustomScaleImageView extends SubsamplingScaleImageView { } public interface Callback { - public void onReady(); - public void onError(boolean wasInitial); + void onReady(); + void onError(boolean wasInitial); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java b/Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java index deae2da1..d5bc94ce 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java @@ -180,7 +180,7 @@ public class FloatingMenu { } public interface FloatingMenuCallback { - public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item); + void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item); } private static class FloatingMenuArrayAdapter extends ArrayAdapter { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java index 6691a5c7..c2803f86 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java @@ -436,18 +436,18 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener callback.onModeLoaded(this, mode); } - public static interface Callback { - public void onTap(MultiImageView multiImageView); + public interface Callback { + void onTap(MultiImageView multiImageView); - public void showProgress(MultiImageView multiImageView, boolean progress); + void showProgress(MultiImageView multiImageView, boolean progress); - public void onProgress(MultiImageView multiImageView, long current, long total); + void onProgress(MultiImageView multiImageView, long current, long total); - public void onVideoLoaded(MultiImageView multiImageView); + void onVideoLoaded(MultiImageView multiImageView); - public void onVideoError(MultiImageView multiImageView, File video); + void onVideoError(MultiImageView multiImageView, File video); - public void onModeLoaded(MultiImageView multiImageView, Mode mode); + void onModeLoaded(MultiImageView multiImageView, Mode mode); } public static class NoMusicServiceCommandContext extends ContextWrapper { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index 4429b03d..9cee09a3 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -74,7 +74,8 @@ public class PostView extends LinearLayout implements View.OnClickListener { private boolean isBuild = false; private LinearLayout full; private LinearLayout contentContainer; - private CustomNetworkImageView imageView; + private int imageSize; + private ThumbnailView thumbnailView; private TextView titleView; private TextView commentView; private TextView repliesCountView; @@ -141,11 +142,11 @@ public class PostView extends LinearLayout implements View.OnClickListener { ta.recycle(); if (post.hasImage) { - imageView.setVisibility(View.VISIBLE); - imageView.setImageUrl(post.thumbnailUrl, ChanApplication.getVolleyImageLoader()); + thumbnailView.setVisibility(View.VISIBLE); + thumbnailView.setUrl(post.thumbnailUrl, imageSize, imageSize); } else { - imageView.setVisibility(View.GONE); - imageView.setImageUrl(null, null); + thumbnailView.setVisibility(View.GONE); + thumbnailView.setUrl(null, 0, 0); } CharSequence total = new SpannableString(""); @@ -258,8 +259,8 @@ public class PostView extends LinearLayout implements View.OnClickListener { return post; } - public ImageView getThumbnail() { - return imageView; + public ThumbnailView getThumbnail() { + return thumbnailView; } public void setHighlightQuotesWithNo(int no) { @@ -291,7 +292,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { int postCommentSize = 0; int commentPadding = 0; int postPadding = 0; - int imageSize = 0; + imageSize = 0; int repliesCountSize = 0; if (isList()) { postCommentSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, ThemeHelper.getInstance().getFontSize(), getResources().getDisplayMetrics()); @@ -324,23 +325,20 @@ public class PostView extends LinearLayout implements View.OnClickListener { imageContainer.setBackgroundColor(thumbnailBackground); // Create thumbnail - imageView = new CustomNetworkImageView(context); - imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - imageView.setFadeIn(100); - - imageView.setOnClickListener(new View.OnClickListener() { + thumbnailView = new ThumbnailView(context); + thumbnailView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - callback.onThumbnailClicked(post, imageView); + callback.onThumbnailClicked(post, thumbnailView); } }); if (isList()) { - imageContainer.addView(imageView, new LinearLayout.LayoutParams(imageSize, imageSize)); + imageContainer.addView(thumbnailView, new LinearLayout.LayoutParams(imageSize, imageSize)); full.addView(imageContainer, wrapMatchParams); full.setMinimumHeight(imageSize); } else if (isGrid()) { - imageContainer.addView(imageView, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, imageSize)); + imageContainer.addView(thumbnailView, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, imageSize)); full.addView(imageContainer, matchWrapParams); } @@ -495,23 +493,23 @@ public class PostView extends LinearLayout implements View.OnClickListener { } public interface PostViewCallback { - public Loadable getLoadable(); + Loadable getLoadable(); - public void onPostClicked(Post post); + void onPostClicked(Post post); - public void onThumbnailClicked(Post post, ImageView thumbnail); + void onThumbnailClicked(Post post, ThumbnailView thumbnail); - public void onShowPostReplies(Post post); + void onShowPostReplies(Post post); - public void onPopulatePostOptions(Post post, Menu menu); + void onPopulatePostOptions(Post post, Menu menu); - public void onPostOptionClicked(Post post, int id); + void onPostOptionClicked(Post post, int id); - public void onPostLinkableClicked(PostLinkable linkable); + void onPostLinkableClicked(PostLinkable linkable); - public boolean isPostHightlighted(Post post); + boolean isPostHightlighted(Post post); - public boolean isPostLastSeen(Post post); + boolean isPostLastSeen(Post post); } private class PostViewMovementMethod extends LinkMovementMethod { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java index 7db7ccad..9c1ecc07 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java @@ -1,41 +1,171 @@ package org.floens.chan.ui.view; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.text.TextUtils; import android.util.AttributeSet; -import android.widget.ImageView; +import android.view.View; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; import org.floens.chan.ChanApplication; -public class ThumbnailView extends ImageView implements ImageLoader.ImageListener { - private String url; +public class ThumbnailView extends View implements ImageLoader.ImageListener { + private ImageLoader.ImageContainer container; + private int fadeTime = 200; + + private boolean circular = false; + + private boolean calculate; + private Bitmap bitmap; + private RectF bitmapRect = new RectF(); + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private RectF drawRect = new RectF(); + private RectF outputRect = new RectF(); + + private Matrix matrix = new Matrix(); + BitmapShader bitmapShader; + private Paint roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); public ThumbnailView(Context context) { super(context); + init(); } public ThumbnailView(Context context, AttributeSet attrs) { super(context, attrs); + init(); } public ThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + init(); + } + + private void init() { } - public void setUrl(String url) { - this.url = url; + public void setUrl(String url, int width, int height) { + if (container != null && container.getRequestUrl().equals(url)) { + return; + } + + if (container != null) { + container.cancelRequest(); + container = null; + setImageBitmap(null); + } + + if (!TextUtils.isEmpty(url)) { + container = ChanApplication.getVolleyImageLoader().get(url, this, width, height); + } + } - ImageLoader.ImageContainer container = ChanApplication.getVolleyImageLoader().get(url, this); + public void setCircular(boolean circular) { + this.circular = circular; + } + + public void setFadeTime(int fadeTime) { + this.fadeTime = fadeTime; + } + + public Bitmap getBitmap() { + return bitmap; } @Override public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { + if (response.getBitmap() != null) { + setImageBitmap(response.getBitmap()); + + clearAnimation(); + if (fadeTime > 0 && !isImmediate) { + setAlpha(0f); + animate().alpha(1f).setDuration(fadeTime); + } else { + setAlpha(1f); + } + } } @Override public void onErrorResponse(VolleyError error) { + error.printStackTrace(); + } + + @Override + protected boolean onSetAlpha(int alpha) { + if (circular) { + roundPaint.setAlpha(alpha); + } else { + paint.setAlpha(alpha); + } + invalidate(); + + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + if (bitmap == null || getAlpha() == 0f) { + return; + } + + int width = getWidth() - getPaddingLeft() - getPaddingRight(); + int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + if (calculate) { + calculate = false; + bitmapRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + float scale = Math.max( + (float) width / (float) bitmap.getWidth(), + (float) height / (float) bitmap.getHeight()); + float scaledX = bitmap.getWidth() * scale; + float scaledY = bitmap.getHeight() * scale; + float offsetX = (scaledX - width) * 0.5f; + float offsetY = (scaledY - height) * 0.5f; + + drawRect.set(-offsetX, -offsetY, scaledX - offsetX, scaledY - offsetY); + drawRect.offset(getPaddingLeft(), getPaddingTop()); + + outputRect.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); + + matrix.setRectToRect(bitmapRect, drawRect, Matrix.ScaleToFit.FILL); + + if (circular) { + bitmapShader.setLocalMatrix(matrix); + roundPaint.setShader(bitmapShader); + } + } + + canvas.save(); + canvas.clipRect(outputRect); + if (circular) { + canvas.drawRoundRect(outputRect, width / 2, height / 2, roundPaint); + } else { + canvas.drawBitmap(bitmap, matrix, paint); + } + canvas.restore(); + } + + private void setImageBitmap(Bitmap bitmap) { + bitmapShader = null; + roundPaint.setShader(null); + this.bitmap = bitmap; + if (bitmap != null) { + calculate = true; + if (circular) { + bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + } + } + invalidate(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java index 31031cb4..0ed17843 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java @@ -150,11 +150,11 @@ public class FileCache { } public interface DownloadedCallback { - public void onProgress(long downloaded, long total, boolean done); + void onProgress(long downloaded, long total, boolean done); - public void onSuccess(File file); + void onSuccess(File file); - public void onFail(boolean notFound); + void onFail(boolean notFound); } private static class FileCacheDownloader implements Runnable { diff --git a/Clover/app/src/main/res/layout/cell_pin.xml b/Clover/app/src/main/res/layout/cell_pin.xml index 6df16680..833625cb 100644 --- a/Clover/app/src/main/res/layout/cell_pin.xml +++ b/Clover/app/src/main/res/layout/cell_pin.xml @@ -1,39 +1,42 @@ - - - + --> diff --git a/Clover/app/src/main/res/layout/controller_navigation_drawer.xml b/Clover/app/src/main/res/layout/controller_navigation_drawer.xml index 8800b381..b2796cb7 100644 --- a/Clover/app/src/main/res/layout/controller_navigation_drawer.xml +++ b/Clover/app/src/main/res/layout/controller_navigation_drawer.xml @@ -52,8 +52,13 @@ along with this program. If not, see . + android:layout_height="match_parent" + android:elevation="8dp"/> diff --git a/Clover/app/src/main/res/layout/pin_item.xml b/Clover/app/src/main/res/layout/pin_item.xml index c462f96a..3599a094 100644 --- a/Clover/app/src/main/res/layout/pin_item.xml +++ b/Clover/app/src/main/res/layout/pin_item.xml @@ -21,13 +21,13 @@ along with this program. If not, see . android:orientation="horizontal"> . android:textSize="19sp" />