From 2bd2de93a921cc340f56ac34b529a122a9716670 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 4 Oct 2015 14:10:01 +0200 Subject: [PATCH] Some work on a better reply open animation --- .../chan/core/presenter/ReplyPresenter.java | 4 + .../floens/chan/ui/layout/ReplyLayout.java | 56 ++++- .../floens/chan/ui/layout/ThreadLayout.java | 2 +- .../chan/ui/layout/ThreadListLayout.java | 193 +++++++++++++----- .../org/floens/chan/utils/AndroidUtils.java | 4 +- .../src/main/res/drawable-mdpi/partyhat.png | Bin 0 -> 2365 bytes 6 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 Clover/app/src/main/res/drawable-mdpi/partyhat.png diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index c9aee818..e1fb2c62 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -125,6 +125,10 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, closeAll(); } + public Page getPage() { + return page; + } + public boolean onBack() { if (page == Page.LOADING) { return true; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index d5472db1..55475241 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -17,6 +17,9 @@ */ package org.floens.chan.ui.layout; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.text.Editable; @@ -25,6 +28,7 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; @@ -63,6 +67,8 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima private View replyInputLayout; private CaptchaLayout captchaLayout; + private ValueAnimator heightOffsetAnimator; + private boolean openingName; private boolean blockSelectionChange = false; private TextView message; @@ -105,7 +111,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima protected void onFinishInflate() { super.onFinishInflate(); - setAnimateLayout(true, true); +// setAnimateLayout(true, true); presenter = new ReplyPresenter(this); @@ -159,7 +165,25 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima removeCallbacks(closeMessageRunnable); } - @Override + public void open(final boolean open) { + int height = getMeasuredHeight(); + animateHeightOffset(open ? 0 : height, open ? height : 0); + } + + public int getHeightOffset() { + if (heightOffsetAnimator == null) { + return 0; + } else { + int animatedValue = (int) heightOffsetAnimator.getAnimatedValue(); + return animatedValue - getHeight(); + } + } + + public boolean wrapHeight() { + return presenter.getPage() == ReplyPresenter.Page.INPUT; + } + + /*@Override public LayoutParams getLayoutParamsForView(View view) { if (view == replyInputLayout) { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); @@ -167,7 +191,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima // Captcha and the loadbar return new LayoutParams(LayoutParams.MATCH_PARENT, getResources().getDimensionPixelSize(R.dimen.reply_height_loading)); } - } + }*/ @Override public void onLayoutAnimationProgress(View view, boolean vertical, int from, int to, int value, float progress) { @@ -195,7 +219,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima @Override public void setPage(ReplyPresenter.Page page, boolean animate) { - setAnimateLayout(animate, true); +// setAnimateLayout(animate, true); switch (page) { case LOADING: setView(null); @@ -210,6 +234,8 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima setView(captchaLayout); + animateHeightOffset(getHeight(), ((View) getParent()).getHeight()); + AndroidUtils.hideKeyboard(this); break; } @@ -403,6 +429,28 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima return callback.getThread(); } + private void animateHeightOffset(int from, final int to) { + requestLayout(); + + heightOffsetAnimator = ValueAnimator.ofInt(from, to); + heightOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + ((View) getParent()).invalidate(); + } + }); + heightOffsetAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + heightOffsetAnimator = null; + setVisibility(to != 0 ? VISIBLE : GONE); + } + }); + heightOffsetAnimator.setInterpolator(new DecelerateInterpolator(2f)); + heightOffsetAnimator.setDuration(300); + heightOffsetAnimator.start(); + } + public interface ReplyLayoutCallback { void highlightPostNo(int no); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index 12c5bdb2..601d3db5 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -188,7 +188,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T } @Override - public void replyLayoutOpen(boolean open) { + public void replyLayoutOpenChanged(boolean open) { showReplyButton(!open); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index 1165f545..fc8823d6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -18,13 +18,17 @@ package org.floens.chan.ui.layout; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.support.annotation.NonNull; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; -import android.widget.FrameLayout; +import android.view.ViewGroup; import android.widget.TextView; import org.floens.chan.R; @@ -43,7 +47,9 @@ import org.floens.chan.ui.toolbar.Toolbar; import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AnimationUtils; +import org.floens.chan.utils.Logger; +import java.util.Calendar; import java.util.List; import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; @@ -51,24 +57,30 @@ import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.getAttrColor; /** - * A layout that wraps around a {@link RecyclerView} to manage showing posts. + * A layout that wraps around a {@link RecyclerView}, and a {@link ReplyLayout} to manage showing posts. */ -public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLayoutCallback { +public class ThreadListLayout extends ViewGroup implements ReplyLayout.ReplyLayoutCallback { public static final int MAX_SMOOTH_SCROLL_DISTANCE = 20; + private RecyclerView recyclerView; private ReplyLayout reply; private TextView searchStatus; - private RecyclerView recyclerView; + private RecyclerView.LayoutManager layoutManager; private PostAdapter postAdapter; - private ChanThread showingThread; + private ThreadListLayoutPresenterCallback callback; private ThreadListLayoutCallback threadListLayoutCallback; - private boolean replyOpen; + private PostCellInterface.PostViewMode postViewMode; private int spanCount = 2; - private int background; + + private boolean replyOpen; private boolean searchOpen; + + private ChanThread showingThread; + + private int background; private int lastPostCount; private int recyclerViewTopPadding; @@ -125,18 +137,6 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa searchStatus.getPaddingRight(), searchStatus.getPaddingBottom()); } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int cardWidth = getResources().getDimensionPixelSize(R.dimen.grid_card_width); - spanCount = Math.max(1, Math.round(getMeasuredWidth() / cardWidth)); - - if (postViewMode == PostCellInterface.PostViewMode.CARD) { - ((GridLayoutManager) layoutManager).setSpanCount(spanCount); - } - } - public void setPostViewMode(PostCellInterface.PostViewMode postViewMode) { if (this.postViewMode != postViewMode) { this.postViewMode = postViewMode; @@ -196,6 +196,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa ((GridLayoutManager) layoutManager).scrollToPositionWithOffset(thread.loadable.listViewIndex, thread.loadable.listViewTop); break; } + + party(); } postAdapter.setThread(thread, filter); @@ -229,32 +231,32 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa } public void openReply(boolean open) { - if (showingThread != null && replyOpen != open) { + if (showingThread != null && replyOpen != open && !searchOpen) { this.replyOpen = open; - int height = AnimationUtils.animateHeight(reply, replyOpen, getWidth(), 500); + + reply.setVisibility(VISIBLE); + reply.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + reply.open(open); + + /*int height = AnimationUtils.animateHeight(reply, replyOpen, getWidth(), 500); if (open) { reply.focusComment(); recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + height, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); } else { AndroidUtils.hideKeyboard(reply); recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + topSpacing(), recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); - } - threadListLayoutCallback.replyLayoutOpen(open); + }*/ + threadListLayoutCallback.replyLayoutOpenChanged(open); attachToolbarScroll(!(open || searchOpen)); } } - public ReplyPresenter getReplyPresenter() { - return reply.getPresenter(); - } - - public void showError(String error) { - postAdapter.showError(error); - } - public void openSearch(boolean show) { - if (searchOpen != show) { + if (showingThread != null && searchOpen != show && !replyOpen) { searchOpen = show; int height = AnimationUtils.animateHeight(searchStatus, show); @@ -269,6 +271,14 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa } } + public ReplyPresenter getReplyPresenter() { + return reply.getPresenter(); + } + + public void showError(String error) { + postAdapter.showError(error); + } + public void setSearchStatus(String query, boolean setEmptyText, boolean hideKeyboard) { if (hideKeyboard) { AndroidUtils.hideKeyboard(this); @@ -312,27 +322,13 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa } public void cleanup() { - /*if (ChanBuild.DEVELOPER_MODE) { - Pin pin = ChanApplication.getWatchManager().findPinByLoadable(showingThread.loadable); - if (pin == null) { - for (Post post : showingThread.posts) { - if (post.comment instanceof SpannedString) { - SpannedString commentSpannable = (SpannedString) post.comment; - PostLinkable[] linkables = commentSpannable.getSpans(0, commentSpannable.length(), PostLinkable.class); - for (PostLinkable linkable : linkables) { - ChanApplication.getRefWatcher().watch(linkable, linkable.key + " " + linkable.value); - } - } - } - } - }*/ - postAdapter.cleanup(); reply.cleanup(); openReply(false); openSearch(false); showingThread = null; lastPostCount = 0; + noParty(); } public List getDisplayingPosts() { @@ -418,6 +414,76 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa return showingThread; } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Logger.test("ThreadListLayout.onMeasure called"); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + throw new IllegalArgumentException("ThreadListLayout must be measured with MeasureSpec.EXACTLY"); + } + + setMeasuredDimension(widthSize, heightSize); + + for (int i = 0, j = getChildCount(); i < j; i++) { + View child = getChildAt(i); + LayoutParams layoutParams = child.getLayoutParams(); + + if (child == recyclerView) { + child.measure( + MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY) + ); + } else if (child == searchStatus) { + child.measure( + MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST) + ); + } else if (child == reply) { + child.measure( + MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(heightSize, reply.wrapHeight() ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY) + ); + } else { + throw new IllegalArgumentException("Unknown child " + child); + } + } + + int cardWidth = getResources().getDimensionPixelSize(R.dimen.grid_card_width); + spanCount = Math.max(1, Math.round(getMeasuredWidth() / cardWidth)); + + if (postViewMode == PostCellInterface.PostViewMode.CARD) { + ((GridLayoutManager) layoutManager).setSpanCount(spanCount); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + Logger.test("ThreadListLayout.onLayout called"); + + for (int i = 0, j = getChildCount(); i < j; i++) { + View child = getChildAt(i); + LayoutParams layoutParams = child.getLayoutParams(); + + child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); + } + } + + @Override + protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) { + Logger.test("ThreadListLayout.drawChild called " + child); + + if (child == reply) { + canvas.translate(0f, reply.getHeightOffset()); + } + + return super.drawChild(canvas, child, drawingTime); + } + private void attachToolbarScroll(boolean attach) { Toolbar toolbar = threadListLayoutCallback.getToolbar(); if (toolbar != null && threadListLayoutCallback.collapseToolbar()) { @@ -459,6 +525,37 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa return -1; } + private Bitmap hat; + + private final RecyclerView.ItemDecoration PARTY = new RecyclerView.ItemDecoration() { + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (hat == null) { + hat = BitmapFactory.decodeResource(getResources(), R.drawable.partyhat); + } + + for (int i = 0, j = parent.getChildCount(); i < j; i++) { + View child = parent.getChildAt(i); + + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + int top = child.getBottom() + params.bottomMargin; + int left = child.getLeft() + params.leftMargin; + c.drawBitmap(hat, left - parent.getPaddingLeft() - dp(38), top - dp(125) - parent.getPaddingTop() + topSpacing(), null); + } + } + }; + + private void party() { + Calendar calendar = Calendar.getInstance(); + if (showingThread.loadable.isCatalogMode() && calendar.get(Calendar.MONTH) == Calendar.OCTOBER && calendar.get(Calendar.DAY_OF_MONTH) == 1) { + recyclerView.addItemDecoration(PARTY); + } + } + + private void noParty() { + recyclerView.removeItemDecoration(PARTY); + } + public interface ThreadListLayoutPresenterCallback { void showThread(Loadable loadable); @@ -468,7 +565,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa } public interface ThreadListLayoutCallback { - void replyLayoutOpen(boolean open); + void replyLayoutOpenChanged(boolean open); Toolbar getToolbar(); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index aa845d6e..9979597e 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -230,8 +230,8 @@ public class AndroidUtils { } public static void hideKeyboard(View view) { - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } public static String getReadableFileSize(long bytes, boolean si) { diff --git a/Clover/app/src/main/res/drawable-mdpi/partyhat.png b/Clover/app/src/main/res/drawable-mdpi/partyhat.png new file mode 100644 index 0000000000000000000000000000000000000000..60ed6c2df9f345163a1761ecdf08814982250b78 GIT binary patch literal 2365 zcmYM0eKgbk8^^cU%wkGSqj~t*ukI4r*P15sG^~^?SuyUmwLFbjMpR^p87-?6E5)LO z!mT1|W}Y@h(Vd4#HPqHk59BCB-BaiMet%pKpVzt0=bZQTxjyGyndDt0eYgo61On-A z-{#?`iQ+|rK{ft#SD1|^XotD^xPd^`!lm;Jh~})z-WJFKfee2cSYTPNqk*V6&w#i+ zF`;n@jMxwmn-LKmw=p^-&SKL>>_+=}0rL$A1bevM!_7Z&|Hmt*&kcDQdPK+qF^+F# z^@oqN|55K2^VNR^rPyPaeK*JiMPEy6U7Fc&CZ+-9H*&sF0LW3y|GDQ)DL=kV<@}s; zPx@H(#>ItmTrjXRjiUPdpp5`zdd-37lJYYR{bz^MW04!db!xAj>@{p^9rFn2*^~5r zjbB@6TD+r1O#5zQ+K!(J{(yq_o~klF6WAp8b?zxl4K=?C{!+D&Xwwzi34Nc#-sp{F z%;UNac6zOA#;c4415NUoW~WntV}LO|`GD$~{DT-L4U0o`=|P%f6IDqj!|9VXg+G7n z>?{@-dz^T1IMK>kCtRLWU2P?`6K+xw{1Zc>fyEFWjZ>lgSzA$e<3VRuNFzuIg+N7!%W{zYAx7n)6lHA=e}5mshq{-&MYx!PZWX$JrE=0jHzRvvIQOKORJ>9oUkQNy| z8%Ux&z^er;f3xBgxT06nX(ZC(ho@&wqP_M%zHZMw+rIG+vbm^X44Wy+vTX$c4{X3Z zB;B(hZ94<7y zG?X(NwYH^X!qOh`sU1W~JDRUrkvZ(kvl(&3$g;W$Q)ym$DR$xTs@8xR1x66**lc^9 zM~h8dBQNUv8qj})OWOt^1N6A?WYcfTK{LRPG!xQF2A~22E3UX*B z(4152H=wt*4_~ZEmb^9`4Pu*nAu z=kxU|oIC_RWnblux9DDunYPko^G`QA6?O?Hb@D`G|h1%i~84 zTV&shp*M?&0Bk5@oHP?#g%V>Vf5e6_%Qq}0U|i#c2Byk|;gfALpf(q&cn1Em(C{Ai z*+*YDBl}v-?FJH*za0N34Er=k$g13?uiMs5Enlds-G_+iy965#o~=`{dd`ydDncS% zDr1mZW3m5{Va@x4_0fNG0+#uA^qmN%ZQ$J<^h!(olys1@2Fo=(#TpnCjyE`C21ngB zUAt3nND!;V7Nxlr3w5tY|6A0M1)HVLzED{yx!!u^i}%_(WQL_J1@ji+1lNq1DyG(c z9X-~O$M%AQ<@XPjjjN0EH(2OsyVXQ}RK(v9gkE1M&cd3%lH{XM25P_p#(Fx~)fgjC z!)cZdRK8|HSFPo($r-|HpNq=p21nQNM;?V^$ zx2#0xzD$4V72%Y5v;!Uu<;=+|LHK+*o*$+{AM2D;*b z@Rv5MyWXw-zR9}%<=Cg>S39KeDMXK}yI!e=)s&3pa>DMs(Dice}wxCojYF#8tv+Q0xpIXoSp)#%L~8~8H$YAnYyL7Ag1^r8VMlp(Q(gR~tr^ah^_DDpP%{w& ztvPaG%(ywpgA?CFCT1^cEIfX}N`f_iG6ca7oh6R#HdUx!m1pTcQ2+@Z`sjD1nqQGv zU{n60J4u_9&@(Y`gNEsIuBqLNT%CVE+8Kg$-!X5iTGm)=?}XgfA)!n?Y86w~6~C*a zst#R>A$sE6k%6K-d?-Als0e=a&H1Cp{=aGb&Mwc-O{e(TpNUNi6<^k@I-u>IyF4o0 H>8bw$RC_^; literal 0 HcmV?d00001