Added the pinned pane

Added ThumbnailView for round icons
Added a recyclerview listener that allows the items to be reordered and swiped away.
filtering
Floens 10 years ago
parent 328a13669f
commit ce989cb4ed
  1. 1
      Clover/app/build.gradle
  2. 4
      Clover/app/proguard.cfg
  3. 4
      Clover/app/src/main/java/org/floens/chan/ChanApplication.java
  4. 2
      Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java
  5. 2
      Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java
  6. 6
      Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java
  7. 2
      Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
  8. 10
      Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java
  9. 16
      Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java
  10. 44
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  11. 22
      Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
  12. 26
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  13. 8
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  14. 2
      Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java
  15. 113
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java
  16. 8
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java
  17. 10
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  18. 21
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  19. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java
  20. 46
      Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
  21. 8
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  22. 30
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  23. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
  24. 67
      Clover/app/src/main/java/org/floens/chan/ui/drawable/ThumbDrawable.java
  25. 4
      Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java
  26. 656
      Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeItemAnimator.java
  27. 387
      Clover/app/src/main/java/org/floens/chan/ui/helper/SwipeListener.java
  28. 4
      Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java
  29. 10
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  30. 6
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  31. 2
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  32. 4
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java
  33. 4
      Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java
  34. 2
      Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java
  35. 14
      Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
  36. 48
      Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java
  37. 142
      Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java
  38. 6
      Clover/app/src/main/java/org/floens/chan/utils/FileCache.java
  39. 41
      Clover/app/src/main/res/layout/cell_pin.xml
  40. 7
      Clover/app/src/main/res/layout/controller_navigation_drawer.xml
  41. 8
      Clover/app/src/main/res/layout/pin_item.xml

@ -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')
}

@ -142,3 +142,7 @@
<methods>;
}
-keepattributes JavascriptInterface
-keepclassmembers class ** {
public void onEvent*(**);
}

@ -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);
}
}

@ -36,6 +36,6 @@ public abstract class ControllerTransition {
}
public interface Callback {
public void onControllerTransitionCompleted(ControllerTransition transition);
void onControllerTransitionCompleted(ControllerTransition transition);
}
}

@ -127,7 +127,9 @@ public abstract class NavigationController extends Controller implements Control
controllerList.remove(from);
}
if (to != null) {
controllerPopped(to);
}
return true;
}

@ -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);
}
}

@ -195,6 +195,6 @@ public class BoardManager {
}
public interface BoardChangeListener {
public void onBoardsChanged();
void onBoardsChanged();
}
}

@ -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 {

@ -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 {

@ -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<Pin> getWatchingPins() {
if (ChanSettings.getWatchEnabled()) {
if (ChanSettings.watchEnabled.get()) {
List<Pin> 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 {

@ -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<PostImage> images, int initialIndex);
void setPagerItems(List<PostImage> 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);
}
}

@ -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<PostImage> 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<PostLinkable> linkables);
void showPostLinkables(List<PostLinkable> 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<Post> posts);
void showPostsPopup(Post forPost, List<Post> posts);
public void showImages(List<PostImage> images, int index, Loadable loadable, ImageView thumbnail);
void showImages(List<PostImage> images, int index, Loadable loadable, ThumbnailView thumbnail);
public void scrollTo(int position);
void scrollTo(int position);
}
}

@ -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<Boolean>() {
@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);

@ -34,6 +34,6 @@ public abstract class Setting<T> {
}
public interface SettingCallback<T> {
public void onValueChange(Setting setting, T value);
void onValueChange(Setting setting, T value);
}
}

@ -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<PinAdapter.PinViewHolder> {
@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;
import static org.floens.chan.utils.AndroidUtils.dp;
public class PinAdapter extends RecyclerView.Adapter<PinAdapter.PinViewHolder> implements SwipeListener.Callback {
private final Callback callback;
private List<Pin> pins = new ArrayList<>();
TextView test = new TextView(parent.getContext());
public PinAdapter(Callback callback) {
this.callback = callback;
}
return new PinViewHolder(test);
@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<Pin> pins) {
this.pins.clear();
this.pins.addAll(pins);
notifyDataSetChanged();
}
public PinViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
public void onPinAdded(Pin pin) {
pins.add(pin);
notifyItemInserted(pins.size() - 1);
}
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);
}
}

@ -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();

@ -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 {

@ -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);
}
}

@ -148,6 +148,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi
}
public interface PassSettingControllerListener {
public void onPassEnabledChanged(boolean enabled);
void onPassEnabledChanged(boolean enabled);
}
}

@ -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);
}

@ -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<PostImage> images, int index, Loadable loadable, final ImageView thumbnail) {
public void showImages(List<PostImage> 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);
}

@ -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);
}

@ -107,6 +107,6 @@ public class WatchSettingsController extends SettingsController implements Compo
}
public interface WatchSettingControllerListener {
public void onWatchEnabledChanged(boolean enabled);
void onWatchEnabledChanged(boolean enabled);
}
}

@ -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;
}
}

@ -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);
}
}

@ -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<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<RecyclerView.ViewHolder>();
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<RecyclerView.ViewHolder>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
private ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList =
new ArrayList<ArrayList<RecyclerView.ViewHolder>>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
private ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<RecyclerView.ViewHolder>();
private ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<RecyclerView.ViewHolder>();
private ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<RecyclerView.ViewHolder>();
private ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<RecyclerView.ViewHolder>();
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<MoveInfo> moves = new ArrayList<MoveInfo>();
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<ChangeInfo> changes = new ArrayList<ChangeInfo>();
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<RecyclerView.ViewHolder> additions = new ArrayList<RecyclerView.ViewHolder>();
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<View, SwipeAnimationData> 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<ChangeInfo> 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<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> 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<RecyclerView.ViewHolder> 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<MoveInfo> 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<RecyclerView.ViewHolder> 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<ChangeInfo> 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<RecyclerView.ViewHolder> 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) {
}
}
;
}

@ -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();
}
}

@ -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 {

@ -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<PostImage> images, int index, Loadable loadable, ImageView thumbnail) {
public void showImages(List<PostImage> 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<PostImage> images, int index, Loadable loadable, ImageView thumbnail);
void showImages(List<PostImage> images, int index, Loadable loadable, ThumbnailView thumbnail);
}
}

@ -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) {

@ -273,6 +273,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
}
public interface ToolbarCallback {
public void onMenuOrBackClicked(boolean isArrow);
void onMenuOrBackClicked(boolean isArrow);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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<String> {

@ -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 {

@ -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 {

@ -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();
}
public void setUrl(String url) {
this.url = url;
private void init() {
}
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();
}
}

@ -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 {

@ -1,39 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<org.floens.chan.ui.cell.PinCell xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="48dp">
<org.floens.chan.ui.view.CustomNetworkImageView
android:id="@+id/pin_image"
android:layout_width="48dp"
android:layout_height="match_parent"
<org.floens.chan.ui.view.ThumbnailView
android:id="@+id/thumb"
android:layout_width="60dp"
android:layout_height="48dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="4dp"
android:paddingBottom="4dp"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/pin_text"
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="1"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="0dp"
android:paddingBottom="12dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textColor="#fff"
android:textColor="#ff757575"
android:textSize="19sp" />
<FrameLayout
android:id="@+id/pin_time_container"
android:layout_width="wrap_content"
<!--<FrameLayout
android:id="@+id/time_container"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingLeft="4dp">
<TextView
android:id="@+id/pin_time"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
@ -45,13 +48,13 @@
</FrameLayout>
<FrameLayout
android:id="@+id/pin_count_container"
android:id="@+id/count_container"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/pin_icon_blue">
<TextView
android:id="@+id/pin_count"
android:id="@+id/count"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
@ -59,10 +62,10 @@
android:textSize="16dp" />
<ProgressBar
android:id="@+id/pin_load"
android:id="@+id/load"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>-->
</org.floens.chan.ui.cell.PinCell>

@ -52,8 +52,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<android.support.v7.widget.RecyclerView
android:id="@+id/drawer_recycler_view"
android:scrollbars="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:elevation="8dp"/>
</FrameLayout>

@ -21,13 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:orientation="horizontal">
<org.floens.chan.ui.view.CustomNetworkImageView
android:id="@+id/pin_image"
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/pin_text"
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
@ -43,13 +43,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:textSize="19sp" />
<FrameLayout
android:id="@+id/pin_time_container"
android:id="@+id/time_container"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingLeft="4dp">
<TextView
android:id="@+id/pin_time"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"

Loading…
Cancel
Save