diff --git a/Chan/src/org/floens/chan/adapter/PostAdapter.java b/Chan/src/org/floens/chan/adapter/PostAdapter.java index f9edfeea..4483fdaa 100644 --- a/Chan/src/org/floens/chan/adapter/PostAdapter.java +++ b/Chan/src/org/floens/chan/adapter/PostAdapter.java @@ -6,6 +6,7 @@ import java.util.List; import org.floens.chan.R; import org.floens.chan.manager.ThreadManager; import org.floens.chan.model.Post; +import org.floens.chan.utils.ScrollerRunnable; import org.floens.chan.utils.Utils; import org.floens.chan.view.PostView; import org.floens.chan.view.ThreadWatchCounterView; @@ -129,10 +130,14 @@ public class PostAdapter extends BaseAdapter { } public void scrollToPost(Post post) { + notifyDataSetChanged(); + for (int i = 0; i < postList.size(); i++) { if (postList.get(i).no == post.no) { -// listView.smoothScrollToPosition(i); does not work when a view is taller than the container - listView.setSelection(i); +// listView.smoothScrollToPosition(i); + + ScrollerRunnable r = new ScrollerRunnable(listView); + r.start(i); break; } diff --git a/Chan/src/org/floens/chan/fragment/PostRepliesFragment.java b/Chan/src/org/floens/chan/fragment/PostRepliesFragment.java index 6d17d34e..c7092f0d 100644 --- a/Chan/src/org/floens/chan/fragment/PostRepliesFragment.java +++ b/Chan/src/org/floens/chan/fragment/PostRepliesFragment.java @@ -21,53 +21,53 @@ import android.widget.ListView; */ public class PostRepliesFragment extends DialogFragment { private ListView listView; - + private List posts; private ThreadManager manager; private boolean callback = true; - + public static PostRepliesFragment newInstance(List posts, ThreadManager manager) { PostRepliesFragment fragment = new PostRepliesFragment(); fragment.posts = posts; fragment.manager = manager; - + return fragment; } - + public void dismissNoCallback() { callback = false; dismiss(); } - + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + setStyle(STYLE_NO_TITLE, 0); } - + @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - + if (callback && manager != null) { manager.onPostRepliesPop(); } } - + @Override public View onCreateView(LayoutInflater inflater, ViewGroup unused, Bundle savedInstanceState) { View container = inflater.inflate(R.layout.post_replies, null); - + listView = (ListView) container.findViewById(R.id.post_list); - + container.findViewById(R.id.replies_back).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); } }); - + container.findViewById(R.id.replies_close).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -75,16 +75,16 @@ public class PostRepliesFragment extends DialogFragment { dismiss(); } }); - + return container; } - + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - + if (posts == null) { - // Restoring from background. + // Restoring from background. dismiss(); } else { ArrayAdapter adapter = new ArrayAdapter(getActivity(), 0) { @@ -96,23 +96,24 @@ public class PostRepliesFragment extends DialogFragment { } else { postView = new PostView(getActivity()); } - + final Post p = getItem(position); - + postView.setPost(p, manager); - postView.setOnClickListener(new View.OnClickListener() { + postView.setOnClickListeners(new View.OnClickListener() { @Override public void onClick(View v) { manager.closeAllPostFragments(); dismiss(); + manager.highlightPost(p); manager.scrollToPost(p); } }); - + return postView; } }; - + adapter.addAll(posts); listView.setAdapter(adapter); } diff --git a/Chan/src/org/floens/chan/manager/ThreadManager.java b/Chan/src/org/floens/chan/manager/ThreadManager.java index e12c9f77..de91b906 100644 --- a/Chan/src/org/floens/chan/manager/ThreadManager.java +++ b/Chan/src/org/floens/chan/manager/ThreadManager.java @@ -37,50 +37,51 @@ import android.widget.Toast; import com.android.volley.VolleyError; /** - * All PostView's need to have this referenced. - * This manages some things like pages, starting and stopping of loading, + * All PostView's need to have this referenced. + * This manages some things like pages, starting and stopping of loading, * handling linkables, replies popups etc. * onDestroy, onStart and onStop must be called from the activity/fragment */ public class ThreadManager implements Loader.LoaderListener { private static final String TAG = "ThreadManager"; - + private final Activity activity; private final ThreadManager.ThreadManagerListener threadManagerListener; private final List> popupQueue = new ArrayList>(); private PostRepliesFragment currentPopupFragment; - + private Post highlightedPost; + private Loader loader; - + public ThreadManager(Activity context, final ThreadManagerListener listener) { - this.activity = context; + activity = context; threadManagerListener = listener; } - + public void onDestroy() { unbindLoader(); } - + public void onStart() { if (loader != null) { loader.onStart(); } } - + public void onStop() { if (loader != null) { loader.onStop(); } } - + public void bindLoader(Loadable loadable) { if (loader != null) { unbindLoader(); } - + loader = LoaderPool.getInstance().obtain(loadable, this); } - + public void unbindLoader() { if (loader != null) { LoaderPool.getInstance().release(loader, this); @@ -88,8 +89,10 @@ public class ThreadManager implements Loader.LoaderListener { } else { Logger.e(TAG, "Loader already unbinded"); } + + highlightedPost = null; } - + public void requestData() { if (loader != null) { loader.requestData(); @@ -97,7 +100,7 @@ public class ThreadManager implements Loader.LoaderListener { Logger.e(TAG, "Loader null in requestData"); } } - + /** * Called by postadapter and threadwatchcounterview.onclick */ @@ -108,50 +111,50 @@ public class ThreadManager implements Loader.LoaderListener { Logger.e(TAG, "Loader null in requestData"); } } - + @Override public void onError(VolleyError error) { threadManagerListener.onThreadLoadError(error); } - + @Override public void onData(List result, boolean append) { threadManagerListener.onThreadLoaded(result, append); } - + public boolean hasLoader() { return loader != null; } - + public Post findPostById(int id) { if (loader == null) return null; return loader.findPostById(id); } - + public Loadable getLoadable() { if (loader == null) return null; return loader.getLoadable(); } - + public Loader getLoader() { return loader; } - + public void onThumbnailClicked(Post post) { threadManagerListener.onThumbnailClicked(post); } - + public void onPostClicked(Post post) { if (loader != null && loader.getLoadable().isBoardMode()) { threadManagerListener.onOPClicked(post); } } - + public void onPostLongClicked(final Post post) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - + String[] items = null; - + String[] temp = activity.getResources().getStringArray(R.array.post_options); // Only add the delete option when the post is a saved reply if (DatabaseManager.getInstance().isSavedReply(post.board, post.no)) { @@ -161,7 +164,7 @@ public class ThreadManager implements Loader.LoaderListener { } else { items = temp; } - + builder.setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -187,66 +190,74 @@ public class ThreadManager implements Loader.LoaderListener { } } }); - + builder.create().show(); } - + public void openReply(boolean startInActivity) { if (loader == null) return; - + if (startInActivity) { ReplyActivity.setLoadable(loader.getLoadable()); Intent i = new Intent(activity, ReplyActivity.class); activity.startActivity(i); } else { ReplyFragment reply = ReplyFragment.newInstance(loader.getLoadable()); - reply.show(activity.getFragmentManager(), "replyDialog"); + reply.show(activity.getFragmentManager(), "replyDialog"); } } - + public void onPostLinkableClicked(PostLinkable linkable) { handleLinkableSelected(linkable); } - + public void scrollToPost(Post post) { threadManagerListener.onScrollTo(post); } - + + public void highlightPost(Post post) { + highlightedPost = post; + } + + public boolean isPostHightlighted(Post post) { + return highlightedPost != null && post.board.equals(highlightedPost.board) && post.no == highlightedPost.no; + } + private void copyToClipboard(String comment) { ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("Post text", comment); clipboard.setPrimaryClip(clip); } - + private void showPostInfo(Post post) { String text = ""; - + if (post.hasImage) { text += "File: " + post.filename + " \nSize: " + post.imageWidth + "x" + post.imageHeight + "\n\n"; } - + text += "Time: " + post.date ; - + if (!TextUtils.isEmpty(post.id)) { text += "\nId: " + post.id; } - + if (!TextUtils.isEmpty(post.email)) { text += "\nEmail: " + post.email; } - + if (!TextUtils.isEmpty(post.tripcode)) { text += "\nTripcode: " + post.tripcode; } - + if (!TextUtils.isEmpty(post.countryName)) { text += "\nCountry: " + post.countryName; } - + if (!TextUtils.isEmpty(post.capcode)) { text += "\nCapcode: " + post.capcode; } - + AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle(R.string.post_info) .setMessage(text) @@ -256,10 +267,10 @@ public class ThreadManager implements Loader.LoaderListener { } }) .create(); - + dialog.show(); } - + /** * When the user clicks a post: * a. when there's one linkable, open the linkable. @@ -269,7 +280,7 @@ public class ThreadManager implements Loader.LoaderListener { public void showPostLinkables(Post post) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); final ArrayList linkables = post.linkables; - + if (linkables.size() > 0) { if (linkables.size() == 1) { handleLinkableSelected(linkables.get(0)); @@ -278,20 +289,20 @@ public class ThreadManager implements Loader.LoaderListener { for (int i = 0; i < linkables.size(); i++) { keys[i] = linkables.get(i).key; } - + builder.setItems(keys, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { handleLinkableSelected(linkables.get(which)); } }); - + AlertDialog dialog = builder.create(); dialog.show(); } } } - + public void showPostReplies(Post post) { List p = new ArrayList(); for (int no : post.repliesFrom) { @@ -300,12 +311,12 @@ public class ThreadManager implements Loader.LoaderListener { p.add(r); } } - + if (p.size() > 0) { showPostsRepliesFragment(p); } } - + /** * Handle when a linkable has been clicked. * @param linkable the selected linkable. @@ -330,32 +341,32 @@ public class ThreadManager implements Loader.LoaderListener { .setTitle(R.string.open_link_confirmation) .setMessage(linkable.value) .create(); - + dialog.show(); } else { openLink(linkable); } } } - + /** - * When a linkable to a post has been clicked, + * When a linkable to a post has been clicked, * show a dialog with the referenced post in it. * @param linkable the clicked linkable. */ private void showPostReply(PostLinkable linkable) { String value = linkable.value; - + Post post = null; - + try { // Get post id String[] splitted = value.split("#p"); if (splitted.length == 2) { int id = Integer.parseInt(splitted[1]); - + post = findPostById(id); - + if (post != null) { List l = new ArrayList(); l.add(post); @@ -366,7 +377,7 @@ public class ThreadManager implements Loader.LoaderListener { e.printStackTrace(); } } - + /** * Open an url. * @param linkable Linkable with an url. @@ -374,53 +385,53 @@ public class ThreadManager implements Loader.LoaderListener { private void openLink(PostLinkable linkable) { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(linkable.value))); } - + private void showPostsRepliesFragment(List list) { - // Post popups are now queued up, more than 32 popups on top of each other makes the system crash! + // Post popups are now queued up, more than 32 popups on top of each other makes the system crash! popupQueue.add(list); - + if (currentPopupFragment != null) { currentPopupFragment.dismissNoCallback(); } - + PostRepliesFragment popup = PostRepliesFragment.newInstance(list, this); - + FragmentTransaction ft = activity.getFragmentManager().beginTransaction(); ft.add(popup, "postPopup"); ft.commit(); - + currentPopupFragment = popup; } - + public void onPostRepliesPop() { if (popupQueue.size() == 0) return; - + popupQueue.remove(popupQueue.size() - 1); - + if (popupQueue.size() > 0) { PostRepliesFragment popup = PostRepliesFragment.newInstance(popupQueue.get(popupQueue.size() - 1), this); - + FragmentTransaction ft = activity.getFragmentManager().beginTransaction(); ft.add(popup, "postPopup"); ft.commit(); - + currentPopupFragment = popup; } else { currentPopupFragment = null; } } - + public void closeAllPostFragments() { popupQueue.clear(); currentPopupFragment = null; } - + private void deletePost(final Post post) { final CheckBox view = new CheckBox(activity); view.setText(R.string.delete_image_only); int padding = activity.getResources().getDimensionPixelSize(R.dimen.general_padding); view.setPadding(padding, padding, padding, padding); - + new AlertDialog.Builder(activity) .setTitle(R.string.delete_confirm) .setView(view) @@ -437,7 +448,7 @@ public class ThreadManager implements Loader.LoaderListener { }) .show(); } - + private void doDeletePost(Post post, boolean onlyImageDelete) { SavedReply reply = DatabaseManager.getInstance().getSavedReply(post.board, post.no); if (reply == null) { @@ -447,19 +458,19 @@ public class ThreadManager implements Loader.LoaderListener { reply.password = "boom";*/ return; } - + final ProgressDialog dialog = ProgressDialog.show(activity, null, activity.getString(R.string.delete_wait)); - + ReplyManager.getInstance().sendDelete(reply, onlyImageDelete, new DeleteListener() { @Override public void onResponse(DeleteResponse response) { dialog.dismiss(); - + if (response.isNetworkError || response.isUserError) { int resId = 0; - + if (response.isTooSoonError) { - resId = R.string.delete_too_soon; + resId = R.string.delete_too_soon; } else if (response.isInvalidPassword) { resId = R.string.delete_password_incorrect; } else if (response.isTooOldError) { @@ -467,7 +478,7 @@ public class ThreadManager implements Loader.LoaderListener { } else { resId = R.string.delete_fail; } - + Toast.makeText(activity, resId, Toast.LENGTH_LONG).show(); } else if (response.isSuccessful) { Toast.makeText(activity, R.string.delete_success, Toast.LENGTH_SHORT).show(); @@ -477,7 +488,7 @@ public class ThreadManager implements Loader.LoaderListener { } }); } - + public interface ThreadManagerListener { public void onThreadLoaded(List result, boolean append); public void onThreadLoadError(VolleyError error); diff --git a/Chan/src/org/floens/chan/service/PinnedService.java b/Chan/src/org/floens/chan/service/PinnedService.java index c2a00d9a..34bf7959 100644 --- a/Chan/src/org/floens/chan/service/PinnedService.java +++ b/Chan/src/org/floens/chan/service/PinnedService.java @@ -19,24 +19,11 @@ public class PinnedService extends Service { private static final long FOREGROUND_INTERVAL = 10000L; private static final long BACKGROUND_INTERVAL = 60000L; - private static PinnedService instance; private static boolean activityInForeground = false; private Thread loadThread; private boolean running = true; - public PinnedService() { - instance = this; - } - - /** - * Get the PinnedService instance - * @return the instance or null - */ - public static PinnedService getInstance() { - return instance; - } - public static void onActivityStart() { Logger.test("onActivityStart"); activityInForeground = true; diff --git a/Chan/src/org/floens/chan/utils/ScrollerRunnable.java b/Chan/src/org/floens/chan/utils/ScrollerRunnable.java new file mode 100644 index 00000000..8d893b80 --- /dev/null +++ b/Chan/src/org/floens/chan/utils/ScrollerRunnable.java @@ -0,0 +1,116 @@ +package org.floens.chan.utils; + +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.ListView; + +public class ScrollerRunnable implements Runnable { + private static final int SCROLL_DURATION = 1; + + private static final int MOVE_DOWN_POS = 1; + private static final int MOVE_UP_POS = 2; + + private final ListView mList; + + private int mMode; + private int mTargetPos; + private int mLastSeenPos; + private final int mExtraScroll; + + public ScrollerRunnable(ListView listView) { + mList = listView; + mExtraScroll = ViewConfiguration.get(mList.getContext()).getScaledFadingEdgeLength(); + } + + public void start(int position) { + stop(); + + final int firstPos = mList.getFirstVisiblePosition(); + final int lastPos = firstPos + mList.getChildCount() - 1; + + int viewTravelCount = 0; + if (position <= firstPos) { + viewTravelCount = firstPos - position + 1; + mMode = MOVE_UP_POS; + } else if (position >= lastPos) { + viewTravelCount = position - lastPos + 1; + mMode = MOVE_DOWN_POS; + } else { + // Already on screen, nothing to do + return; + } + + mTargetPos = position; + mLastSeenPos = ListView.INVALID_POSITION; + + mList.post(this); + } + + void stop() { + mList.removeCallbacks(this); + } + + @Override + public void run() { + final int listHeight = mList.getHeight(); + final int firstPos = mList.getFirstVisiblePosition(); + + switch (mMode) { + case MOVE_DOWN_POS: { + final int lastViewIndex = mList.getChildCount() - 1; + final int lastPos = firstPos + lastViewIndex; + + if (lastViewIndex < 0) { + return; + } + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. +// mList.post(this); +// return; + } + + final View lastView = mList.getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = lastPos < mList.getCount() - 1 ? mExtraScroll : mList.getPaddingBottom(); + + mList.smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll, 0); + + mLastSeenPos = lastPos; + if (lastPos < mTargetPos) { + mList.post(this); + } + break; + } + + case MOVE_UP_POS: { + if (firstPos == mLastSeenPos) { + // No new views, let things keep going. +// mList.post(this); +// return; + } + + final View firstView = mList.getChildAt(0); + if (firstView == null) { + return; + } + final int firstViewTop = firstView.getTop(); + final int extraScroll = firstPos > 0 ? mExtraScroll : mList.getPaddingTop(); + + mList.smoothScrollBy(firstViewTop - extraScroll, 0); + + mLastSeenPos = firstPos; + + if (firstPos > mTargetPos) { + mList.post(this); + } + break; + } + + default: + break; + } + } +} diff --git a/Chan/src/org/floens/chan/view/PostView.java b/Chan/src/org/floens/chan/view/PostView.java index 68e70de0..0fe0f869 100644 --- a/Chan/src/org/floens/chan/view/PostView.java +++ b/Chan/src/org/floens/chan/view/PostView.java @@ -198,6 +198,8 @@ public class PostView extends LinearLayout implements View.OnClickListener, View } if (post.isSavedReply) { + full.setBackgroundColor(0xFFBCBCBC); + } else if (manager.isPostHightlighted(post)) { full.setBackgroundColor(0xFFD6BAD0); } else { full.setBackgroundColor(0x00000000); @@ -297,6 +299,11 @@ public class PostView extends LinearLayout implements View.OnClickListener, View full.setOnLongClickListener(this); } + public void setOnClickListeners(View.OnClickListener listener) { + commentView.setOnClickListener(listener); + full.setOnClickListener(listener); + } + public void onLinkableClick(PostLinkable linkable) { manager.onPostLinkableClicked(linkable); }