work on the reply layout

replace all layout param animations with better methods
make the expanded more fill the screen height
various refactorings for the thread layouts
multisite
Floens 8 years ago
parent 18d06e2f57
commit 1e59ac664f
  1. 4
      Clover/app/src/main/java/org/floens/chan/Chan.java
  2. 7
      Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java
  3. 4
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHistoryManager.java
  4. 2
      Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java
  5. 2
      Clover/app/src/main/java/org/floens/chan/core/model/orm/Loadable.java
  6. 50
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  7. 45
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  8. 18
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  9. 11
      Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java
  10. 14
      Clover/app/src/main/java/org/floens/chan/ui/animation/AnimationUtils.java
  11. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  12. 197
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  13. 64
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  14. 63
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  15. 153
      Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java
  16. 6
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  17. 95
      Clover/app/src/main/res/layout/layout_reply_input.xml

@ -68,6 +68,10 @@ public class Chan extends Application implements UserAgentProvider {
return instance.graph; return instance.graph;
} }
public static <T> T inject(T instance) {
return Chan.instance.graph.inject(instance);
}
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();

@ -19,27 +19,34 @@ package org.floens.chan.chan;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
@Deprecated
public class ChanUrls { public class ChanUrls {
@Deprecated
public static String getCaptchaSiteKey() { public static String getCaptchaSiteKey() {
return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc";
} }
@Deprecated
public static String getBoardUrlDesktop(String board) { public static String getBoardUrlDesktop(String board) {
return scheme() + "://boards.4chan.org/" + board + "/"; return scheme() + "://boards.4chan.org/" + board + "/";
} }
@Deprecated
public static String getThreadUrlDesktop(String board, int no) { public static String getThreadUrlDesktop(String board, int no) {
return scheme() + "://boards.4chan.org/" + board + "/thread/" + no; return scheme() + "://boards.4chan.org/" + board + "/thread/" + no;
} }
@Deprecated
public static String getThreadUrlDesktop(String board, int no, int postNo) { public static String getThreadUrlDesktop(String board, int no, int postNo) {
return scheme() + "://boards.4chan.org/" + board + "/thread/" + no + "#p" + postNo; return scheme() + "://boards.4chan.org/" + board + "/thread/" + no + "#p" + postNo;
} }
@Deprecated
public static String getCatalogUrlDesktop(String board) { public static String getCatalogUrlDesktop(String board) {
return scheme() + "://boards.4chan.org/" + board + "/catalog"; return scheme() + "://boards.4chan.org/" + board + "/catalog";
} }
@Deprecated
private static String scheme() { private static String scheme() {
return ChanSettings.networkHttps.get() ? "https" : "http"; return ChanSettings.networkHttps.get() ? "https" : "http";
} }

@ -42,10 +42,6 @@ public class DatabaseHistoryManager {
this.databaseLoadableManager = databaseLoadableManager; this.databaseLoadableManager = databaseLoadableManager;
} }
public void add(History history) {
databaseManager.runTaskSync(addHistory(history));
}
public Callable<Void> load() { public Callable<Void> load() {
return new Callable<Void>() { return new Callable<Void>() {
@Override @Override

@ -43,6 +43,7 @@ import org.floens.chan.ui.controller.SitesSetupController;
import org.floens.chan.ui.controller.ViewThreadController; import org.floens.chan.ui.controller.ViewThreadController;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.ui.layout.FilterLayout; import org.floens.chan.ui.layout.FilterLayout;
import org.floens.chan.ui.layout.ReplyLayout;
import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
import org.floens.chan.ui.service.WatchNotifier; import org.floens.chan.ui.service.WatchNotifier;
import org.floens.chan.ui.view.MultiImageView; import org.floens.chan.ui.view.MultiImageView;
@ -97,6 +98,7 @@ import dagger.Provides;
SiteSetupController.class, SiteSetupController.class,
SitesSetupController.class, SitesSetupController.class,
BoardSetupController.class, BoardSetupController.class,
ReplyLayout.class,
Chan4.class, Chan4.class,
}, },

@ -167,7 +167,7 @@ public class Loadable implements SiteReference, BoardReference {
} }
/** /**
* Compares the mode, board and no. * Compares the mode, site, board and no.
*/ */
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {

@ -23,23 +23,21 @@ import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.ui.captcha.CaptchaCallback; import org.floens.chan.ui.captcha.CaptchaCallback;
import org.floens.chan.ui.captcha.CaptchaLayoutInterface; import org.floens.chan.ui.captcha.CaptchaLayoutInterface;
import org.floens.chan.ui.helper.ImagePickDelegate;
import java.io.File; import java.io.File;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -48,7 +46,6 @@ import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getReadableFileSize; import static org.floens.chan.utils.AndroidUtils.getReadableFileSize;
import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getRes;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
@ -65,20 +62,9 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
private ReplyPresenterCallback callback; private ReplyPresenterCallback callback;
@Inject private ReplyManager replyManager;
ReplyManager replyManager; private WatchManager watchManager;
private DatabaseManager databaseManager;
@Inject
BoardManager boardManager;
@Inject
WatchManager watchManager;
@Inject
HttpCallManager httpCallManager;
@Inject
DatabaseManager databaseManager;
private boolean bound = false; private boolean bound = false;
private Loadable loadable; private Loadable loadable;
@ -92,9 +78,17 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
private boolean captchaInited; private boolean captchaInited;
private int selectedQuote = -1; private int selectedQuote = -1;
public ReplyPresenter(ReplyPresenterCallback callback) { @Inject
public ReplyPresenter(ReplyManager replyManager,
WatchManager watchManager,
DatabaseManager databaseManager) {
this.replyManager = replyManager;
this.watchManager = watchManager;
this.databaseManager = databaseManager;
}
public void create(ReplyPresenterCallback callback) {
this.callback = callback; this.callback = callback;
getGraph().inject(this);
} }
public void bindLoadable(Loadable loadable) { public void bindLoadable(Loadable loadable) {
@ -164,6 +158,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
public void onMoreClicked() { public void onMoreClicked() {
moreOpen = !moreOpen; moreOpen = !moreOpen;
callback.setExpanded(moreOpen);
callback.openNameOptions(moreOpen); callback.openNameOptions(moreOpen);
if (!loadable.isThreadMode()) { if (!loadable.isThreadMode()) {
callback.openSubject(moreOpen); callback.openSubject(moreOpen);
@ -176,6 +171,10 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
} }
} }
public boolean isExpanded() {
return moreOpen;
}
public void onAttachClicked() { public void onAttachClicked() {
if (!pickingFile) { if (!pickingFile) {
if (previewOpen) { if (previewOpen) {
@ -329,6 +328,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
previewOpen = false; previewOpen = false;
selectedQuote = -1; selectedQuote = -1;
callback.openMessage(false, true, "", false); callback.openMessage(false, true, "", false);
callback.setExpanded(false);
callback.openSubject(false); callback.openSubject(false);
callback.openNameOptions(false); callback.openNameOptions(false);
callback.openFileName(false); callback.openFileName(false);
@ -428,12 +428,12 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
void openMessage(boolean open, boolean animate, String message, boolean autoHide); void openMessage(boolean open, boolean animate, String message, boolean autoHide);
void openMessageWebview(String rawMessage);
void onPosted(); void onPosted();
void setCommentHint(String hint); void setCommentHint(String hint);
void setExpanded(boolean expanded);
void openNameOptions(boolean open); void openNameOptions(boolean open);
void openSubject(boolean open); void openSubject(boolean open);

@ -25,16 +25,15 @@ import org.floens.chan.chan.ChanLoader;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.exception.ChanLoaderException; import org.floens.chan.core.exception.ChanLoaderException;
import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.History;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.Pin;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.History;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.Pin;
import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.pool.ChanLoaderFactory; import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
@ -42,7 +41,6 @@ import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse; import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
@ -59,7 +57,6 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCellInterface.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutPresenterCallback { public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCellInterface.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutPresenterCallback {
@ -79,22 +76,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private static final int POST_OPTION_OPEN_BROWSER = 13; private static final int POST_OPTION_OPEN_BROWSER = 13;
private static final int POST_OPTION_FILTER_TRIPCODE = 14; private static final int POST_OPTION_FILTER_TRIPCODE = 14;
@Inject
WatchManager watchManager;
@Inject
DatabaseManager databaseManager;
@Inject
ReplyManager replyManager;
@Inject
HttpCallManager httpCallManager;
@Inject
ChanLoaderFactory chanLoaderFactory;
private ThreadPresenterCallback threadPresenterCallback; private ThreadPresenterCallback threadPresenterCallback;
private WatchManager watchManager;
private DatabaseManager databaseManager;
private ChanLoaderFactory chanLoaderFactory;
private Loadable loadable; private Loadable loadable;
private ChanLoader chanLoader; private ChanLoader chanLoader;
@ -103,10 +88,17 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private PostsFilter.Order order = PostsFilter.Order.BUMP; private PostsFilter.Order order = PostsFilter.Order.BUMP;
private boolean historyAdded = false; private boolean historyAdded = false;
public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { @Inject
this.threadPresenterCallback = threadPresenterCallback; public ThreadPresenter(WatchManager watchManager,
DatabaseManager databaseManager,
ChanLoaderFactory chanLoaderFactory) {
this.watchManager = watchManager;
this.databaseManager = databaseManager;
this.chanLoaderFactory = chanLoaderFactory;
}
getGraph().inject(this); public void create(ThreadPresenterCallback threadPresenterCallback) {
this.threadPresenterCallback = threadPresenterCallback;
} }
public void bindLoadable(Loadable loadable) { public void bindLoadable(Loadable loadable) {
@ -116,6 +108,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
Pin pin = watchManager.findPinByLoadable(loadable); Pin pin = watchManager.findPinByLoadable(loadable);
// TODO this isn't true anymore, because all loadables come from one location.
if (pin != null) { if (pin != null) {
// Use the loadable from the pin. // Use the loadable from the pin.
// This way we can store the list position in the pin loadable, // This way we can store the list position in the pin loadable,
@ -704,7 +697,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
history.loadable = loadable; history.loadable = loadable;
PostImage image = chanLoader.getThread().op.image; PostImage image = chanLoader.getThread().op.image;
history.thumbnailUrl = image == null ? "" : image.thumbnailUrl.toString(); history.thumbnailUrl = image == null ? "" : image.thumbnailUrl.toString();
databaseManager.getDatabaseHistoryManager().add(history); databaseManager.runTask(databaseManager.getDatabaseHistoryManager().addHistory(history));
} }
} }

@ -17,9 +17,11 @@
*/ */
package org.floens.chan.core.site; package org.floens.chan.core.site;
import android.support.v4.util.ArrayMap;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.Post;
import java.util.Map; import java.util.Map;
@ -48,4 +50,18 @@ public interface SiteEndpoints {
HttpUrl report(Post post); HttpUrl report(Post post);
HttpUrl login(); HttpUrl login();
static Map<String, String> makeArgument(String key, String value) {
Map<String, String> map = new ArrayMap<>(1);
map.put(key, value);
return map;
}
static Map<String, String> makeArgument(String key1, String value1,
String key2, String value2) {
Map<String, String> map = new ArrayMap<>(2);
map.put(key1, value1);
map.put(key2, value2);
return map;
}
} }

@ -14,6 +14,8 @@ import java.util.Map;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import static org.floens.chan.core.site.SiteEndpoints.makeArgument;
public class FutabaChanReader implements ChanReader { public class FutabaChanReader implements ChanReader {
private final ChanParser chanParser; private final ChanParser chanParser;
@ -235,15 +237,14 @@ public class FutabaChanReader implements ChanReader {
if (countryCode != null && countryName != null) { if (countryCode != null && countryName != null) {
Map<String, String> arg = new HashMap<>(1); Map<String, String> arg = new HashMap<>(1);
arg.put("country_code", countryCode); HttpUrl countryUrl = endpoints.icon(builder, "country",
HttpUrl countryUrl = endpoints.icon(builder, "country", arg); makeArgument("country_code", countryCode));
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName));
} }
if (trollCountryCode != null && countryName != null) { if (trollCountryCode != null && countryName != null) {
Map<String, String> arg = new HashMap<>(1); HttpUrl countryUrl = endpoints.icon(builder, "troll_country",
arg.put("troll_country_code", trollCountryCode); makeArgument("troll_country_code", trollCountryCode));
HttpUrl countryUrl = endpoints.icon(builder, "troll_country", arg);
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName)); builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName));
} }

@ -35,10 +35,17 @@ public class AnimationUtils {
return (int) (a + (b - a) * x); return (int) (a + (b - a) * x);
} }
// a lot of these are deprecated, they animate the height with the layout params themselves,
// causing a measure loop for each frame. android just isn't designed for this, and it always
// lags. there are better ways to easily animate layouts, such as enabling the layoutAnimations
// flag.
@Deprecated
public static void setHeight(View view, boolean expand, boolean animated) { public static void setHeight(View view, boolean expand, boolean animated) {
setHeight(view, expand, animated, -1); setHeight(view, expand, animated, -1);
} }
@Deprecated
public static void setHeight(View view, boolean expand, boolean animated, int knownWidth) { public static void setHeight(View view, boolean expand, boolean animated, int knownWidth) {
if (animated) { if (animated) {
animateHeight(view, expand, knownWidth); animateHeight(view, expand, knownWidth);
@ -49,14 +56,17 @@ public class AnimationUtils {
private static Map<View, ValueAnimator> layoutAnimations = new HashMap<>(); private static Map<View, ValueAnimator> layoutAnimations = new HashMap<>();
@Deprecated
public static int animateHeight(final View view, boolean expand) { public static int animateHeight(final View view, boolean expand) {
return animateHeight(view, expand, -1); return animateHeight(view, expand, -1);
} }
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth) { public static int animateHeight(final View view, final boolean expand, int knownWidth) {
return animateHeight(view, expand, knownWidth, 300); return animateHeight(view, expand, knownWidth, 300);
} }
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration) { public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration) {
return animateHeight(view, expand, knownWidth, duration, null); return animateHeight(view, expand, knownWidth, duration, null);
} }
@ -68,6 +78,7 @@ public class AnimationUtils {
* You can call this even when a height animation is currently running, it will resolve any issues.<br> * You can call this even when a height animation is currently running, it will resolve any issues.<br>
* <b>This does cause some lag on complex views because requestLayout is called on each frame.</b> * <b>This does cause some lag on complex views because requestLayout is called on each frame.</b>
*/ */
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration, final LayoutAnimationProgress progressCallback) { public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration, final LayoutAnimationProgress progressCallback) {
final int fromHeight; final int fromHeight;
int toHeight; int toHeight;
@ -89,6 +100,7 @@ public class AnimationUtils {
return toHeight; return toHeight;
} }
@Deprecated
public static void animateLayout(final boolean vertical, final View view, final int from, final int to, int duration, final boolean wrapAfterwards, final LayoutAnimationProgress callback) { public static void animateLayout(final boolean vertical, final View view, final int from, final int to, int duration, final boolean wrapAfterwards, final LayoutAnimationProgress callback) {
ValueAnimator running = layoutAnimations.remove(view); ValueAnimator running = layoutAnimations.remove(view);
if (running != null) { if (running != null) {
@ -153,7 +165,9 @@ public class AnimationUtils {
layoutAnimations.put(view, valueAnimator); layoutAnimations.put(view, valueAnimator);
} }
@Deprecated
public interface LayoutAnimationProgress { public interface LayoutAnimationProgress {
@Deprecated
void onLayoutAnimationProgress(View view, boolean vertical, int from, int to, int value, float progress); void onLayoutAnimationProgress(View view, boolean vertical, int from, int to, int value, float progress);
} }

@ -69,7 +69,7 @@ public abstract class ThreadController extends Controller implements ThreadLayou
navigationItem.handlesToolbarInset = true; navigationItem.handlesToolbarInset = true;
threadLayout = (ThreadLayout) LayoutInflater.from(context).inflate(R.layout.layout_thread, null); threadLayout = (ThreadLayout) LayoutInflater.from(context).inflate(R.layout.layout_thread, null);
threadLayout.setCallback(this); threadLayout.create(this);
swipeRefreshLayout = new SwipeRefreshLayout(context) { swipeRefreshLayout = new SwipeRefreshLayout(context) {
@Override @Override
@ -93,7 +93,7 @@ public abstract class ThreadController extends Controller implements ThreadLayou
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
threadLayout.getPresenter().unbindLoadable(); threadLayout.destroy();
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }
@ -259,6 +259,7 @@ public abstract class ThreadController extends Controller implements ThreadLayou
} else { } else {
navigationController.pushController(filtersController); navigationController.pushController(filtersController);
} }
// TODO cleanup
Filter filter = new Filter(); Filter filter = new Filter();
filter.type = FilterType.TRIPCODE.flag; filter.type = FilterType.TRIPCODE.flag;
filter.pattern = tripcode; filter.pattern = tripcode;

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.ui.layout; package org.floens.chan.ui.layout;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.text.Editable; import android.text.Editable;
@ -25,7 +27,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@ -37,8 +39,8 @@ import android.widget.Toast;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.captcha.CaptchaCallback; import org.floens.chan.ui.captcha.CaptchaCallback;
import org.floens.chan.ui.captcha.CaptchaLayout; import org.floens.chan.ui.captcha.CaptchaLayout;
@ -50,29 +52,33 @@ import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.ui.view.SelectionListeningEditText; import org.floens.chan.ui.view.SelectionListeningEditText;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.ui.animation.AnimationUtils;
import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.ImageDecoder;
import java.io.File; import java.io.File;
import javax.inject.Inject;
import static org.floens.chan.Chan.inject;
import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
public class ReplyLayout extends LoadView implements View.OnClickListener, AnimationUtils.LayoutAnimationProgress, ReplyPresenter.ReplyPresenterCallback, TextWatcher, ImageDecoder.ImageDecoderCallback, SelectionListeningEditText.SelectionChangedListener { public class ReplyLayout extends LoadView implements View.OnClickListener, ReplyPresenter.ReplyPresenterCallback, TextWatcher, ImageDecoder.ImageDecoderCallback, SelectionListeningEditText.SelectionChangedListener {
private ReplyPresenter presenter; @Inject
ReplyPresenter presenter;
private ReplyLayoutCallback callback; private ReplyLayoutCallback callback;
private boolean newCaptcha; private boolean newCaptcha;
private View replyInputLayout;
private FrameLayout captchaContainer;
private ImageView captchaHardReset;
private CaptchaLayoutInterface authenticationLayout; private CaptchaLayoutInterface authenticationLayout;
private boolean openingName; private boolean openingName;
private boolean blockSelectionChange = false; private boolean blockSelectionChange = false;
// Reply views:
private View replyInputLayout;
private TextView message; private TextView message;
private EditText name; private EditText name;
private EditText subject; private EditText subject;
@ -81,79 +87,97 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
private LinearLayout nameOptions; private LinearLayout nameOptions;
private SelectionListeningEditText comment; private SelectionListeningEditText comment;
private TextView commentCounter; private TextView commentCounter;
private LinearLayout previewContainer;
private CheckBox spoiler; private CheckBox spoiler;
private ImageView preview; private ImageView preview;
private TextView previewMessage; private TextView previewMessage;
private ImageView more;
private DropdownArrowDrawable moreDropdown;
private ImageView attach; private ImageView attach;
private ImageView more;
private ImageView submit; private ImageView submit;
private DropdownArrowDrawable moreDropdown;
// Captcha views:
private FrameLayout captchaContainer;
private ImageView captchaHardReset;
private Runnable closeMessageRunnable = new Runnable() { private Runnable closeMessageRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
AnimationUtils.animateHeight(message, false, getWidth()); message.setVisibility(View.GONE);
} }
}; };
public ReplyLayout(Context context) { public ReplyLayout(Context context) {
super(context); this(context, null);
} }
public ReplyLayout(Context context, AttributeSet attrs) { public ReplyLayout(Context context, AttributeSet attrs) {
super(context, attrs); this(context, attrs, 0);
} }
public ReplyLayout(Context context, AttributeSet attrs, int defStyleAttr) { public ReplyLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyle);
} }
@Override @Override
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
inject(this);
setAnimateLayout(true, true);
final LayoutInflater inflater = LayoutInflater.from(getContext());
presenter = new ReplyPresenter(this);
// Inflate reply input
replyInputLayout = LayoutInflater.from(getContext()).inflate(R.layout.layout_reply_input, this, false); replyInputLayout = inflater.inflate(R.layout.layout_reply_input, this, false);
message = (TextView) replyInputLayout.findViewById(R.id.message); message = replyInputLayout.findViewById(R.id.message);
name = (EditText) replyInputLayout.findViewById(R.id.name); name = replyInputLayout.findViewById(R.id.name);
subject = (EditText) replyInputLayout.findViewById(R.id.subject); subject = replyInputLayout.findViewById(R.id.subject);
options = (EditText) replyInputLayout.findViewById(R.id.options); options = replyInputLayout.findViewById(R.id.options);
fileName = (EditText) replyInputLayout.findViewById(R.id.file_name); fileName = replyInputLayout.findViewById(R.id.file_name);
nameOptions = (LinearLayout) replyInputLayout.findViewById(R.id.name_options); nameOptions = replyInputLayout.findViewById(R.id.name_options);
comment = (SelectionListeningEditText) replyInputLayout.findViewById(R.id.comment); comment = replyInputLayout.findViewById(R.id.comment);
commentCounter = replyInputLayout.findViewById(R.id.comment_counter);
spoiler = replyInputLayout.findViewById(R.id.spoiler);
preview = replyInputLayout.findViewById(R.id.preview);
previewMessage = replyInputLayout.findViewById(R.id.preview_message);
attach = replyInputLayout.findViewById(R.id.attach);
more = replyInputLayout.findViewById(R.id.more);
submit = replyInputLayout.findViewById(R.id.submit);
// Setup reply layout views
comment.addTextChangedListener(this); comment.addTextChangedListener(this);
comment.setSelectionChangedListener(this); comment.setSelectionChangedListener(this);
commentCounter = (TextView) replyInputLayout.findViewById(R.id.comment_counter);
previewContainer = (LinearLayout) replyInputLayout.findViewById(R.id.preview_container);
spoiler = (CheckBox) replyInputLayout.findViewById(R.id.spoiler);
preview = (ImageView) replyInputLayout.findViewById(R.id.preview);
previewMessage = (TextView) replyInputLayout.findViewById(R.id.preview_message);
preview.setOnClickListener(this); preview.setOnClickListener(this);
more = (ImageView) replyInputLayout.findViewById(R.id.more);
moreDropdown = new DropdownArrowDrawable(dp(16), dp(16), true, getAttrColor(getContext(), R.attr.dropdown_dark_color), getAttrColor(getContext(), R.attr.dropdown_dark_pressed_color)); moreDropdown = new DropdownArrowDrawable(dp(16), dp(16), true,
getAttrColor(getContext(), R.attr.dropdown_dark_color),
getAttrColor(getContext(), R.attr.dropdown_dark_pressed_color));
more.setImageDrawable(moreDropdown); more.setImageDrawable(moreDropdown);
setRoundItemBackground(more); setRoundItemBackground(more);
more.setOnClickListener(this); more.setOnClickListener(this);
attach = (ImageView) replyInputLayout.findViewById(R.id.attach);
theme().imageDrawable.apply(attach); theme().imageDrawable.apply(attach);
setRoundItemBackground(attach); setRoundItemBackground(attach);
attach.setOnClickListener(this); attach.setOnClickListener(this);
submit = (ImageView) replyInputLayout.findViewById(R.id.submit);
theme().sendDrawable.apply(submit); theme().sendDrawable.apply(submit);
setRoundItemBackground(submit); setRoundItemBackground(submit);
submit.setOnClickListener(this); submit.setOnClickListener(this);
captchaContainer = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.layout_reply_captcha, this, false); // Inflate captcha layout
captchaHardReset = (ImageView) captchaContainer.findViewById(R.id.reset); captchaContainer = (FrameLayout) inflater.inflate(R.layout.layout_reply_captcha, this, false);
captchaHardReset = captchaContainer.findViewById(R.id.reset);
// Setup captcha layout views
captchaContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
theme().refreshDrawable.apply(captchaHardReset); theme().refreshDrawable.apply(captchaHardReset);
setRoundItemBackground(captchaHardReset); setRoundItemBackground(captchaHardReset);
captchaHardReset.setOnClickListener(this); captchaHardReset.setOnClickListener(this);
setView(replyInputLayout); setView(replyInputLayout);
// Presenter
presenter.create(this);
} }
public void setCallback(ReplyLayoutCallback callback) { public void setCallback(ReplyLayoutCallback callback) {
@ -177,29 +201,17 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
removeCallbacks(closeMessageRunnable); removeCallbacks(closeMessageRunnable);
} }
@Override
public LayoutParams getLayoutParamsForView(View view) {
if (view == replyInputLayout || (view == captchaContainer && !newCaptcha)) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
} else if (view == captchaContainer && newCaptcha) {
return new LayoutParams(LayoutParams.MATCH_PARENT, dp(300));
} else {
// Loadbar
return new LayoutParams(LayoutParams.MATCH_PARENT, dp(100));
}
}
@Override
public void onLayoutAnimationProgress(View view, boolean vertical, int from, int to, int value, float progress) {
if (view == nameOptions) {
moreDropdown.setRotation(openingName ? progress : 1f - progress);
}
}
public boolean onBack() { public boolean onBack() {
return presenter.onBack(); return presenter.onBack();
} }
private void setWrap(boolean wrap) {
setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT,
wrap ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT
));
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (v == more) { if (v == more) {
@ -215,6 +227,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
} }
} }
@SuppressLint("ClickableViewAccessibility")
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
return true; return true;
@ -222,20 +235,24 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
@Override @Override
public void setPage(ReplyPresenter.Page page, boolean animate) { public void setPage(ReplyPresenter.Page page, boolean animate) {
setAnimateLayout(animate, true);
switch (page) { switch (page) {
case LOADING: case LOADING:
setView(null); setWrap(true);
View progressBar = setView(null);
progressBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dp(100)));
break; break;
case INPUT: case INPUT:
setView(replyInputLayout); setView(replyInputLayout);
setWrap(!presenter.isExpanded());
break; break;
case AUTHENTICATION: case AUTHENTICATION:
setWrap(false);
if (authenticationLayout == null) { if (authenticationLayout == null) {
if (newCaptcha) { if (newCaptcha) {
authenticationLayout = new CaptchaLayout(getContext()); authenticationLayout = new CaptchaLayout(getContext());
} else { } else {
authenticationLayout = (CaptchaLayoutInterface) LayoutInflater.from(getContext()).inflate(R.layout.layout_captcha_legacy, captchaContainer, false); authenticationLayout = (CaptchaLayoutInterface) LayoutInflater.from(getContext())
.inflate(R.layout.layout_captcha_legacy, captchaContainer, false);
} }
captchaContainer.addView((View) authenticationLayout, 0); captchaContainer.addView((View) authenticationLayout, 0);
} }
@ -294,25 +311,13 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
public void openMessage(boolean open, boolean animate, String text, boolean autoHide) { public void openMessage(boolean open, boolean animate, String text, boolean autoHide) {
removeCallbacks(closeMessageRunnable); removeCallbacks(closeMessageRunnable);
message.setText(text); message.setText(text);
message.setVisibility(open ? View.VISIBLE : View.GONE);
if (animate) {
AnimationUtils.animateHeight(message, open, getWidth());
} else {
message.setVisibility(open ? VISIBLE : GONE);
message.getLayoutParams().height = open ? ViewGroup.LayoutParams.WRAP_CONTENT : 0;
message.requestLayout();
}
if (autoHide) { if (autoHide) {
postDelayed(closeMessageRunnable, 5000); postDelayed(closeMessageRunnable, 5000);
} }
} }
@Override
public void openMessageWebview(String rawMessage) {
// callback.
}
@Override @Override
public void onPosted() { public void onPosted() {
Toast.makeText(getContext(), R.string.reply_success, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), R.string.reply_success, Toast.LENGTH_SHORT).show();
@ -325,20 +330,34 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
comment.setHint(hint); comment.setHint(hint);
} }
@Override
public void setExpanded(boolean expanded) {
setWrap(!expanded);
comment.setMaxLines(expanded ? 15 : 6);
ValueAnimator animator = ValueAnimator.ofFloat(expanded ? 0f : 1f, expanded ? 1f : 0f);
animator.setInterpolator(new DecelerateInterpolator(2f));
animator.setDuration(400);
animator.addUpdateListener(animation ->
moreDropdown.setRotation((float) animation.getAnimatedValue()));
animator.start();
}
@Override @Override
public void openNameOptions(boolean open) { public void openNameOptions(boolean open) {
openingName = open; openingName = open;
AnimationUtils.animateHeight(nameOptions, open, comment.getWidth(), 300, this); nameOptions.setVisibility(open ? View.VISIBLE : View.GONE);
} }
@Override @Override
public void openSubject(boolean open) { public void openSubject(boolean open) {
AnimationUtils.animateHeight(subject, open, comment.getWidth()); subject.setVisibility(open ? View.VISIBLE : View.GONE);
} }
@Override @Override
public void openFileName(boolean open) { public void openFileName(boolean open) {
AnimationUtils.animateHeight(fileName, open, comment.getWidth()); fileName.setVisibility(open ? View.VISIBLE : View.GONE);
} }
@Override @Override
@ -346,6 +365,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
fileName.setText(name); fileName.setText(name);
} }
@SuppressLint("SetTextI18n")
@Override @Override
public void updateCommentCount(int count, int maxCount, boolean over) { public void updateCommentCount(int count, int maxCount, boolean over) {
commentCounter.setText(count + "/" + maxCount); commentCounter.setText(count + "/" + maxCount);
@ -354,13 +374,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
} }
public void focusComment() { public void focusComment() {
comment.requestFocus(); comment.post(() -> AndroidUtils.requestViewAndKeyboardFocus(comment));
comment.postDelayed(new Runnable() {
@Override
public void run() {
AndroidUtils.requestKeyboardFocus(comment);
}
}, 100);
} }
@Override @Override
@ -372,9 +386,11 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
} }
if (show) { if (show) {
ImageDecoder.decodeFileOnBackgroundThread(previewFile, dp(100), dp(100), this); ImageDecoder.decodeFileOnBackgroundThread(previewFile, dp(300), dp(200), this);
} else { } else {
AnimationUtils.animateLayout(false, previewContainer, previewContainer.getWidth(), 0, 300, false, null); spoiler.setVisibility(View.GONE);
preview.setVisibility(View.GONE);
previewMessage.setVisibility(View.GONE);
} }
} }
@ -386,7 +402,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
@Override @Override
public void openSpoiler(boolean show, boolean checked) { public void openSpoiler(boolean show, boolean checked) {
AnimationUtils.animateHeight(spoiler, show); spoiler.setVisibility(show ? View.VISIBLE : View.GONE);
spoiler.setChecked(checked); spoiler.setChecked(checked);
} }
@ -394,7 +410,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
public void onImageBitmap(File file, Bitmap bitmap) { public void onImageBitmap(File file, Bitmap bitmap) {
if (bitmap != null) { if (bitmap != null) {
preview.setImageBitmap(bitmap); preview.setImageBitmap(bitmap);
AnimationUtils.animateLayout(false, previewContainer, 0, dp(100), 300, false, null); preview.setVisibility(View.VISIBLE);
} else { } else {
openPreviewMessage(true, getString(R.string.reply_no_preview)); openPreviewMessage(true, getString(R.string.reply_no_preview));
} }
@ -402,6 +418,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
@Override @Override
public void onFilePickLoading() { public void onFilePickLoading() {
// TODO
} }
@Override @Override

@ -70,7 +70,7 @@ import static org.floens.chan.utils.AndroidUtils.fixSnackbarText;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
/** /**
* Wrapper around ThreadListLayout, so that it cleanly manages between loadbar and listview. * Wrapper around ThreadListLayout, so that it cleanly manages between a load bar and the list view.
*/ */
public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.ThreadPresenterCallback, PostPopupHelper.PostPopupHelperCallback, View.OnClickListener, ThreadListLayout.ThreadListLayoutCallback { public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.ThreadPresenterCallback, PostPopupHelper.PostPopupHelperCallback, View.OnClickListener, ThreadListLayout.ThreadListLayoutCallback {
private enum Visible { private enum Visible {
@ -82,9 +82,10 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
private ThreadLayoutCallback callback; @Inject
ThreadPresenter presenter;
private ThreadPresenter presenter; private ThreadLayoutCallback callback;
private LoadView loadView; private LoadView loadView;
private HidingFloatingActionButton replyButton; private HidingFloatingActionButton replyButton;
@ -102,40 +103,45 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
private Snackbar newPostsNotification; private Snackbar newPostsNotification;
public ThreadLayout(Context context) { public ThreadLayout(Context context) {
super(context); this(context, null);
init();
} }
public ThreadLayout(Context context, AttributeSet attrs) { public ThreadLayout(Context context, AttributeSet attrs) {
super(context, attrs); this(context, attrs, 0);
init();
} }
public ThreadLayout(Context context, AttributeSet attrs, int defStyleAttr) { public ThreadLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyle);
init();
getGraph().inject(this);
} }
public void setCallback(ThreadLayoutCallback callback) { public void create(ThreadLayoutCallback callback) {
this.callback = callback; this.callback = callback;
presenter = new ThreadPresenter(this); // View binding
loadView = findViewById(R.id.loadview);
replyButton = findViewById(R.id.reply_button);
loadView = (LoadView) findViewById(R.id.loadview); LayoutInflater layoutInflater = LayoutInflater.from(getContext());
replyButton = (HidingFloatingActionButton) findViewById(R.id.reply_button);
threadListLayout = (ThreadListLayout) LayoutInflater.from(getContext()).inflate(R.layout.layout_thread_list, this, false); // Inflate ThreadListLayout
threadListLayout.setCallbacks(presenter, presenter, presenter, presenter, this); threadListLayout = (ThreadListLayout) layoutInflater
.inflate(R.layout.layout_thread_list, this, false);
postPopupHelper = new PostPopupHelper(getContext(), presenter, this); // Inflate error layout
errorLayout = (LinearLayout) layoutInflater
.inflate(R.layout.layout_thread_error, this, false);
errorText = errorLayout.findViewById(R.id.text);
errorRetryButton = errorLayout.findViewById(R.id.button);
errorLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.layout_thread_error, this, false); // View setup
errorText = (TextView) errorLayout.findViewById(R.id.text); threadListLayout.setCallbacks(presenter, presenter, presenter, presenter, this);
postPopupHelper = new PostPopupHelper(getContext(), presenter, this);
errorText.setTypeface(AndroidUtils.ROBOTO_MEDIUM); errorText.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
errorRetryButton = (Button) errorLayout.findViewById(R.id.button);
errorRetryButton.setOnClickListener(this); errorRetryButton.setOnClickListener(this);
// Setup
replyButtonEnabled = ChanSettings.enableReplyFab.get(); replyButtonEnabled = ChanSettings.enableReplyFab.get();
if (!replyButtonEnabled) { if (!replyButtonEnabled) {
AndroidUtils.removeFromParentView(replyButton); AndroidUtils.removeFromParentView(replyButton);
@ -145,9 +151,15 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
theme().applyFabColor(replyButton); theme().applyFabColor(replyButton);
} }
presenter.create(this);
switchVisible(Visible.LOADING); switchVisible(Visible.LOADING);
} }
public void destroy() {
presenter.unbindLoadable();
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (v == errorRetryButton) { if (v == errorRetryButton) {
@ -377,8 +389,8 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
@Override @Override
public void confirmPostDelete(final Post post) { public void confirmPostDelete(final Post post) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams") final View view = LayoutInflater.from(getContext())
final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_post_delete, null); .inflate(R.layout.dialog_post_delete, null);
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(getContext())
.setTitle(R.string.delete_confirm) .setTitle(R.string.delete_confirm)
.setView(view) .setView(view)
@ -386,7 +398,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
CheckBox checkBox = (CheckBox) view.findViewById(R.id.image_only); CheckBox checkBox = view.findViewById(R.id.image_only);
presenter.deletePostConfirmed(post, checkBox.isChecked()); presenter.deletePostConfirmed(post, checkBox.isChecked());
} }
}) })
@ -531,10 +543,6 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
} }
} }
private void init() {
getGraph().inject(this);
}
@Override @Override
public void presentRepliesController(Controller controller) { public void presentRepliesController(Controller controller) {
callback.presentRepliesController(controller); callback.presentRepliesController(controller);

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.ui.layout; package org.floens.chan.ui.layout;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
@ -29,25 +31,27 @@ import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.animation.AnimationUtils;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCell;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
import org.floens.chan.ui.toolbar.Toolbar; import org.floens.chan.ui.toolbar.Toolbar;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.ui.animation.AnimationUtils;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
@ -96,17 +100,20 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
reply = (ReplyLayout) findViewById(R.id.reply); // View binding
reply.setCallback(this); reply = findViewById(R.id.reply);
searchStatus = findViewById(R.id.search_status);
recyclerView = findViewById(R.id.recycler_view);
searchStatus = (TextView) findViewById(R.id.search_status); // View setup
reply.setCallback(this);
searchStatus.setTypeface(ROBOTO_MEDIUM); searchStatus.setTypeface(ROBOTO_MEDIUM);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
} }
public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback,
ThreadStatusCell.Callback statusCellCallback, ThreadListLayoutPresenterCallback callback, PostCell.PostCellCallback postCellCallback,
ThreadStatusCell.Callback statusCellCallback,
ThreadListLayoutPresenterCallback callback,
ThreadListLayoutCallback threadListLayoutCallback) { ThreadListLayoutCallback threadListLayoutCallback) {
this.callback = callback; this.callback = callback;
this.threadListLayoutCallback = threadListLayoutCallback; this.threadListLayoutCallback = threadListLayoutCallback;
@ -136,12 +143,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
// As requested by the RecyclerView, make sure that the adapter isn't changed // As requested by the RecyclerView, make sure that the adapter isn't changed
// while in a layout pass. Postpone to the next frame. // while in a layout pass. Postpone to the next frame.
mainHandler.post(new Runnable() { mainHandler.post(() -> ThreadListLayout.this.callback.onListScrolledToBottom());
@Override
public void run() {
ThreadListLayout.this.callback.onListScrolledToBottom();
}
});
} }
} }
} }
@ -261,7 +263,34 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
public void openReply(boolean open) { public void openReply(boolean open) {
if (showingThread != null && replyOpen != open) { if (showingThread != null && replyOpen != open) {
this.replyOpen = open; this.replyOpen = open;
int height = AnimationUtils.animateHeight(reply, replyOpen, getWidth(), 500);
reply.measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
int height = reply.getMeasuredHeight();
final ViewPropertyAnimator viewPropertyAnimator = reply.animate();
viewPropertyAnimator.setListener(null);
viewPropertyAnimator.setInterpolator(new DecelerateInterpolator(2f));
viewPropertyAnimator.setDuration(600);
if (open) {
reply.setVisibility(View.VISIBLE);
reply.setTranslationY(-height);
viewPropertyAnimator.translationY(0f);
} else {
reply.setTranslationY(0f);
viewPropertyAnimator.translationY(-height);
viewPropertyAnimator.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
viewPropertyAnimator.setListener(null);
reply.setVisibility(View.GONE);
}
});
}
reply.onOpen(open); reply.onOpen(open);
if (open) { if (open) {
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + height, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + height, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
@ -534,11 +563,13 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
}; };
private void party() { private void party() {
if (showingThread.loadable.site.id() == 0) {
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
if (calendar.get(Calendar.MONTH) == Calendar.OCTOBER && calendar.get(Calendar.DAY_OF_MONTH) == 1) { if (calendar.get(Calendar.MONTH) == Calendar.OCTOBER && calendar.get(Calendar.DAY_OF_MONTH) == 1) {
recyclerView.addItemDecoration(PARTY); recyclerView.addItemDecoration(PARTY);
} }
} }
}
private void noParty() { private void noParty() {
recyclerView.removeItemDecoration(PARTY); recyclerView.removeItemDecoration(PARTY);

@ -19,16 +19,19 @@ package org.floens.chan.ui.view;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import org.floens.chan.ui.animation.AnimationUtils; import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.List;
/** /**
* Container for a view with an ProgressBar. Toggles between the view and a * Container for a view with an ProgressBar. Toggles between the view and a
@ -36,11 +39,10 @@ import org.floens.chan.ui.animation.AnimationUtils;
*/ */
public class LoadView extends FrameLayout { public class LoadView extends FrameLayout {
private int fadeDuration = 200; private int fadeDuration = 200;
private boolean animateLayout;
private boolean animateVertical;
private int layoutAnimationDuration = 500;
private Listener listener; private Listener listener;
private AnimatorSet animatorSet = new AnimatorSet();
public LoadView(Context context) { public LoadView(Context context) {
super(context); super(context);
} }
@ -62,15 +64,6 @@ public class LoadView extends FrameLayout {
this.fadeDuration = fadeDuration; this.fadeDuration = fadeDuration;
} }
public void setLayoutAnimationDuration(int layoutAnimationDuration) {
this.layoutAnimationDuration = layoutAnimationDuration;
}
public void setAnimateLayout(boolean animateLayout, boolean animateVertical) {
this.animateLayout = animateLayout;
this.animateVertical = animateVertical;
}
/** /**
* Set a listener that gives a call when a view gets removed * Set a listener that gives a call when a view gets removed
* *
@ -99,37 +92,47 @@ public class LoadView extends FrameLayout {
*/ */
public View setView(View newView, boolean animate) { public View setView(View newView, boolean animate) {
if (newView == null) { if (newView == null) {
FrameLayout progressBar = new FrameLayout(getContext()); newView = getViewForNull();
progressBar.addView(new ProgressBar(getContext()), new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER));
newView = progressBar;
} }
if (animate) { if (animate) {
// Fade all attached views out // Fast forward possible pending animations (keeping the views attached.)
animatorSet.cancel();
animatorSet = new AnimatorSet();
// Fade all attached views out.
// If the animation is cancelled, the views are not removed. If the animation ends,
// the views are removed.
List<Animator> animators = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i); final View child = getChildAt(i);
final ViewPropertyAnimator childAnimation = child.animate()
.setInterpolator(new LinearInterpolator()) // We don't add a listener to remove the view we also animate in.
.setDuration(fadeDuration) if (child == newView) {
.alpha(0f); continue;
childAnimation.setListener(new AnimatorListenerAdapter() { }
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(child, View.ALPHA, 0f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override @Override
public void onAnimationCancel(Animator animation) { public void onAnimationCancel(Animator animation) {
// Canceled because it is being animated in again. // If cancelled, don't remove the view, but re-run the animation.
// Don't let this listener call removeView on the in animation. animation.removeListener(this);
childAnimation.setListener(null);
} }
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
// Animation ended without interruptions, remove listener for future animations. removeAndCallListener(child);
childAnimation.setListener(null);
removeView(child);
if (listener != null) {
listener.onLoadViewRemoved(child);
} }
});
animators.add(objectAnimator);
} }
}).start();
if (newView.getParent() != this) {
if (newView.getParent() != null) {
AndroidUtils.removeFromParentView(newView);
}
addView(newView);
} }
// Assume no running animations // Assume no running animations
@ -138,70 +141,50 @@ public class LoadView extends FrameLayout {
} }
// Fade our new view in // Fade our new view in
newView.animate() ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(newView, View.ALPHA, 1f);
.setInterpolator(new LinearInterpolator()) animators.add(objectAnimator);
.setDuration(fadeDuration)
.alpha(1f)
.start();
// Assume view already attached to this view (fading out)
if (newView.getParent() == null) {
addView(newView, getLayoutParamsForView(newView));
}
// Animate this view its size to the new view size if the width or height is WRAP_CONTENT
if (animateLayout && getChildCount() >= 2) {
final int currentSize = animateVertical ? getHeight() : getWidth();
int newSize = animateVertical ? newView.getHeight() : newView.getWidth();
if (newSize == 0) {
if (animateVertical) {
if (newView.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
newView.measure(
View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.UNSPECIFIED);
newSize = newView.getMeasuredHeight();
} else {
newSize = newView.getLayoutParams().height;
}
} else {
if (newView.getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
newView.measure(
View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.EXACTLY));
newSize = newView.getMeasuredWidth();
} else {
newSize = newView.getLayoutParams().width;
}
}
}
if (animateVertical) { animatorSet.setDuration(fadeDuration);
newSize += getPaddingTop() + getPaddingBottom(); animatorSet.playTogether(animators);
animatorSet.start();
} else { } else {
newSize += getPaddingLeft() + getPaddingRight(); // Fast forward possible pending animations (end, so also remove them).
} animatorSet.end();
AnimationUtils.animateLayout(animateVertical, this, currentSize, newSize, layoutAnimationDuration, true, null);
}
} else {
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i); View child = getChildAt(i);
child.animate().cancel(); removeAndCallListener(child);
if (listener != null) {
listener.onLoadViewRemoved(child);
}
} }
removeAllViews();
newView.animate().cancel(); animatorSet = new AnimatorSet();
newView.setAlpha(1f); newView.setAlpha(1f);
addView(newView, getLayoutParamsForView(newView)); addView(newView);
} }
return newView; return newView;
} }
public LayoutParams getLayoutParamsForView(View view) { protected View getViewForNull() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); // TODO: figure out why this is needed for the thread list layout view.
FrameLayout progressBarContainer = new FrameLayout(getContext());
progressBarContainer.setLayoutParams(
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
View progressBar = new ProgressBar(getContext());
progressBar.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.CENTER));
progressBarContainer.addView(progressBar);
return progressBarContainer;
}
protected void removeAndCallListener(View child) {
removeView(child);
if (listener != null) {
listener.onLoadViewRemoved(child);
}
} }
public interface Listener { public interface Listener {

@ -69,6 +69,8 @@ public class AndroidUtils {
private static ConnectivityManager connectivityManager; private static ConnectivityManager connectivityManager;
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
public static void init() { public static void init() {
ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf"); ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf");
ROBOTO_MEDIUM_ITALIC = getTypeface("Roboto-MediumItalic.ttf"); ROBOTO_MEDIUM_ITALIC = getTypeface("Roboto-MediumItalic.ttf");
@ -232,11 +234,11 @@ public class AndroidUtils {
* be run on the ui thread. * be run on the ui thread.
*/ */
public static void runOnUiThread(Runnable runnable) { public static void runOnUiThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable); mainHandler.post(runnable);
} }
public static void runOnUiThread(Runnable runnable, long delay) { public static void runOnUiThread(Runnable runnable, long delay) {
new Handler(Looper.getMainLooper()).postDelayed(runnable, delay); mainHandler.postDelayed(runnable, delay);
} }
public static void requestKeyboardFocus(Dialog dialog, final View view) { public static void requestKeyboardFocus(Dialog dialog, final View view) {

@ -15,48 +15,54 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:baselineAligned="false"
android:minHeight="124dp" android:minHeight="124dp"
android:orientation="vertical" android:orientation="horizontal"
tools:ignore="Suspicious0dp,ContentDescription"> tools:ignore="ContentDescription,RtlHardcoded,RtlSymmetry">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
android:textColor="#fff44336"
android:visibility="gone" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal"> android:minHeight="124dp"
android:orientation="horizontal"
tools:ignore="ContentDescription,RtlHardcoded,RtlSymmetry">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:animateLayoutChanges="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingTop="8dp"> android:paddingTop="8dp">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="#fff44336"
android:visibility="gone" />
<LinearLayout <LinearLayout
android:id="@+id/name_options" android:id="@+id/name_options"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"> android:visibility="gone">
<EditText <EditText
android:id="@+id/name" android:id="@+id/name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_weight="2" android:layout_weight="2"
android:hint="@string/reply_name" android:hint="@string/reply_name"
android:inputType="textCapSentences|textAutoCorrect" android:inputType="textCapSentences|textAutoCorrect"
@ -66,7 +72,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<EditText <EditText
android:id="@+id/options" android:id="@+id/options"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/reply_options" android:hint="@string/reply_options"
android:singleLine="true" android:singleLine="true"
@ -77,22 +83,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<EditText <EditText
android:id="@+id/subject" android:id="@+id/subject"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:hint="@string/reply_subject" android:hint="@string/reply_subject"
android:inputType="textCapSentences|textAutoCorrect" android:inputType="textCapSentences|textAutoCorrect"
android:singleLine="true" android:singleLine="true"
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" /> android:visibility="gone" />
<EditText
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/reply_file_name"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -101,6 +98,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:id="@+id/comment" android:id="@+id/comment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="top"
android:imeActionLabel="@string/reply_submit" android:imeActionLabel="@string/reply_submit"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect" android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:maxLines="6" android:maxLines="6"
@ -120,45 +118,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</RelativeLayout> </RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/preview_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingTop="8dp"
android:visibility="gone">
<CheckBox <CheckBox
android:id="@+id/spoiler" android:id="@+id/spoiler"
android:layout_width="100dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:text="@string/reply_spoiler_image" android:text="@string/reply_spoiler_image"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone" /> android:visibility="gone" />
<ImageView <ImageView
android:id="@+id/preview" android:id="@+id/preview"
android:layout_width="100dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="150dp"
android:layout_weight="1" android:scaleType="centerInside"
android:scaleType="centerInside" /> android:visibility="gone" />
<TextView <TextView
android:id="@+id/preview_message" android:id="@+id/preview_message"
android:layout_width="100dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" /> android:textSize="12sp"
android:visibility="gone" />
<EditText
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/reply_file_name"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/buttons" android:id="@+id/buttons"
android:layout_width="52dp" android:layout_width="52dp"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp"> android:padding="8dp">
@ -168,11 +164,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_height="36dp" android:layout_height="36dp"
android:padding="10dp" /> android:padding="10dp" />
<Space
android:layout_width="36dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView <ImageView
android:id="@+id/attach" android:id="@+id/attach"
android:layout_width="36dp" android:layout_width="36dp"
@ -189,4 +180,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</LinearLayout> </LinearLayout>
</LinearLayout> </ScrollView>

Loading…
Cancel
Save