diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java index 47eeb2f9..8f53649c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java @@ -113,7 +113,6 @@ public class PostAdapter extends RecyclerView.Adapter { } } - @Override public int getItemCount() { int size = displayList.size(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java index 7b14de00..41f7cdcd 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java @@ -32,7 +32,7 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.ThemeHelper; -import org.floens.chan.ui.view.FastTextView; +import org.floens.chan.ui.text.FastTextView; import org.floens.chan.ui.view.FixedRatioThumbnailView; import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.ui.view.FloatingMenuItem; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java index 604abb47..f59cbec8 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java @@ -19,10 +19,14 @@ package org.floens.chan.ui.cell; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Typeface; -import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.support.annotation.NonNull; import android.text.Layout; @@ -32,12 +36,10 @@ import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.view.MotionEvent; @@ -45,7 +47,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; -import android.widget.TextView; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader; @@ -56,7 +57,8 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.ui.helper.PostHelper; +import org.floens.chan.ui.text.FastTextView; +import org.floens.chan.ui.text.FastTextViewMovementMethod; import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.ui.view.FloatingMenu; @@ -67,10 +69,9 @@ import org.floens.chan.utils.Time; import java.util.ArrayList; import java.util.List; -import java.util.Locale; +import static android.text.TextUtils.isEmpty; import static org.floens.chan.utils.AndroidUtils.dp; -import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import static org.floens.chan.utils.AndroidUtils.sp; @@ -78,22 +79,21 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin private static final int COMMENT_MAX_LENGTH_BOARD = 500; private ThumbnailView thumbnailView; - private TextView title; - private TextView icons; - private TextView comment; - private TextView replies; + private FastTextView title; + private PostIcons icons; + private FastTextView comment; + private FastTextView replies; private ImageView options; private View divider; private View filterMatchColor; private boolean commentClickable = false; - private CharSequence iconsSpannable; private int detailsSizePx; private int iconsTextSize; private int countrySizePx; private int paddingPx; private boolean threadMode; - private boolean ignoreNextOnClick; +// private boolean ignoreNextOnClick; private boolean bound = false; private Theme theme; @@ -106,14 +106,13 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin private OnClickListener selfClicked = new OnClickListener() { @Override public void onClick(View v) { - if (ignoreNextOnClick) { - ignoreNextOnClick = false; - } else { - callback.onPostClicked(post); - } +// if (ignoreNextOnClick) { +// ignoreNextOnClick = false; +// } else { + callback.onPostClicked(post); +// } } }; - private ImageLoader.ImageContainer countryIconRequest; public PostCell(Context context) { super(context); @@ -132,10 +131,10 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin super.onFinishInflate(); thumbnailView = (ThumbnailView) findViewById(R.id.thumbnail_view); - title = (TextView) findViewById(R.id.title); - icons = (TextView) findViewById(R.id.icons); - comment = (TextView) findViewById(R.id.comment); - replies = (TextView) findViewById(R.id.replies); + title = (FastTextView) findViewById(R.id.title); + icons = (PostIcons) findViewById(R.id.icons); + comment = (FastTextView) findViewById(R.id.comment); + replies = (FastTextView) findViewById(R.id.replies); options = (ImageView) findViewById(R.id.options); divider = findViewById(R.id.divider); filterMatchColor = findViewById(R.id.filter_match_color); @@ -148,7 +147,8 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin iconsTextSize = sp(textSizeSp); countrySizePx = sp(textSizeSp - 3); - icons.setTextSize(textSizeSp); + icons.setHeight(iconsTextSize); + icons.setSpacing(dp(4)); icons.setPadding(paddingPx, dp(4), paddingPx, 0); comment.setTextSize(textSizeSp); @@ -328,14 +328,16 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin if (ChanSettings.postFullDate.get()) { time = post.date; } else { + // Disabled for performance reasons // Force the relative date to use the english locale, and restore the previous one. - Configuration c = Resources.getSystem().getConfiguration(); + /*Configuration c = Resources.getSystem().getConfiguration(); Locale previousLocale = c.locale; c.locale = Locale.ENGLISH; Resources.getSystem().updateConfiguration(c, null); time = DateUtils.getRelativeTimeSpanString(post.time * 1000L, Time.get(), DateUtils.SECOND_IN_MILLIS, 0); c.locale = previousLocale; - Resources.getSystem().updateConfiguration(c, null); + Resources.getSystem().updateConfiguration(c, null);*/ + time = DateUtils.getRelativeTimeSpanString(post.time * 1000L, Time.get(), DateUtils.SECOND_IN_MILLIS, 0); } String noText = "No." + post.no; @@ -371,38 +373,21 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin title.setText(TextUtils.concat(titleParts.toArray(new CharSequence[titleParts.size()]))); - iconsSpannable = new SpannableString(""); - - if (post.sticky) { - iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.stickyIcon, iconsTextSize); - } - - if (post.closed) { - iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.closedIcon, iconsTextSize); - } - - if (post.deleted.get()) { - iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.trashIcon, iconsTextSize); - } - - if (post.archived) { - iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.archivedIcon, iconsTextSize); - } + icons.edit(); + icons.set(PostIcons.STICKY, post.sticky); + icons.set(PostIcons.CLOSED, post.closed); + icons.set(PostIcons.DELETED, post.deleted.get()); + icons.set(PostIcons.ARCHIVED, post.archived); - boolean waitingForCountry = false; - if (!TextUtils.isEmpty(post.country)) { - loadCountryIcon(theme); - waitingForCountry = true; - } - - if (iconsSpannable.length() > 0 || waitingForCountry) { - icons.setVisibility(VISIBLE); - icons.setText(iconsSpannable); + if (!isEmpty(post.country)) { + icons.set(PostIcons.COUNTRY, true); + icons.showCountry(post, theme, countrySizePx); } else { - icons.setVisibility(GONE); - icons.setText(""); + icons.set(PostIcons.COUNTRY, false); } + icons.apply(); + CharSequence commentText; if (post.comment.length() > COMMENT_MAX_LENGTH_BOARD && !threadMode) { commentText = post.comment.subSequence(0, COMMENT_MAX_LENGTH_BOARD); @@ -410,8 +395,8 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin commentText = post.comment; } - comment.setText(commentText); - comment.setVisibility(TextUtils.isEmpty(commentText) && !post.hasImage ? GONE : VISIBLE); + comment.setText(new SpannableString(commentText)); + comment.setVisibility(isEmpty(commentText) && !post.hasImage ? GONE : VISIBLE); if (commentClickable != threadMode) { commentClickable = threadMode; @@ -419,12 +404,12 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin PostViewMovementMethod movementMethod = new PostViewMovementMethod(); comment.setMovementMethod(movementMethod); comment.setOnClickListener(selfClicked); - title.setMovementMethod(movementMethod); +// title.setMovementMethod(movementMethod); } else { comment.setOnClickListener(null); comment.setClickable(false); comment.setMovementMethod(null); - title.setMovementMethod(null); +// title.setMovementMethod(null); } } @@ -458,18 +443,15 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin private void unbindPost(Post post) { bound = false; - if (countryIconRequest != null) { - countryIconRequest.cancelRequest(); - countryIconRequest = null; - } + icons.cancelCountryRequest(); setPostLinkableListener(post, null); } private void setPostLinkableListener(Post post, PostLinkable.Callback callback) { if (post.comment instanceof Spanned) { - Spanned commentSpannable = (Spanned) post.comment; - PostLinkable[] linkables = commentSpannable.getSpans(0, commentSpannable.length(), PostLinkable.class); + Spanned commentSpanned = (Spanned) post.comment; + PostLinkable[] linkables = commentSpanned.getSpans(0, commentSpanned.length(), PostLinkable.class); for (PostLinkable linkable : linkables) { if (callback == null) { while (linkable.hasCallback(this)) { @@ -481,34 +463,14 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin } } } - } - } - private void loadCountryIcon(final Theme theme) { - countryIconRequest = Chan.getVolleyImageLoader().get(post.countryUrl, new ImageLoader.ImageListener() { - @Override - public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { - if (response.getBitmap() != null) { - CharSequence countryIcon = PostHelper.addIcon(new BitmapDrawable(getRes(), response.getBitmap()), iconsTextSize); - - SpannableString countryText = new SpannableString(post.countryName); - countryText.setSpan(new StyleSpan(Typeface.ITALIC), 0, countryText.length(), 0); - countryText.setSpan(new ForegroundColorSpan(theme.detailsColor), 0, countryText.length(), 0); - countryText.setSpan(new AbsoluteSizeSpan(countrySizePx), 0, countryText.length(), 0); - - iconsSpannable = TextUtils.concat(iconsSpannable, countryIcon, countryText); - - if (!isImmediate) { - icons.setVisibility(VISIBLE); - icons.setText(iconsSpannable); - } + if (callback == null) { + if (commentSpanned instanceof Spannable) { + Spannable commentSpannable = (Spannable) commentSpanned; + commentSpannable.removeSpan(BACKGROUND_SPAN); } } - - @Override - public void onErrorResponse(VolleyError error) { - } - }); + } } @Override @@ -527,17 +489,17 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin * A MovementMethod that searches for PostLinkables.
* See {@link PostLinkable} for more information. */ - private class PostViewMovementMethod extends LinkMovementMethod { + private class PostViewMovementMethod implements FastTextViewMovementMethod { @Override - public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer, @NonNull MotionEvent event) { + public boolean onTouchEvent(@NonNull FastTextView widget, @NonNull Spannable buffer, @NonNull MotionEvent event) { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); + x -= widget.getPaddingLeft(); + y -= widget.getPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); @@ -550,22 +512,26 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { - ignoreNextOnClick = true; link[0].onClick(widget); buffer.removeSpan(BACKGROUND_SPAN); + widget.invalidate(); } else if (action == MotionEvent.ACTION_DOWN && link[0] instanceof PostLinkable) { buffer.setSpan(BACKGROUND_SPAN, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]), 0); + widget.invalidate(); } else if (action == MotionEvent.ACTION_CANCEL) { buffer.removeSpan(BACKGROUND_SPAN); + widget.invalidate(); } return true; } else { buffer.removeSpan(BACKGROUND_SPAN); + widget.invalidate(); + return false; } } - return true; + return false; } } @@ -580,4 +546,177 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin ds.setUnderlineText(false); } } + + private static Bitmap stickyIcon; + private static Bitmap closedIcon; + private static Bitmap trashIcon; + private static Bitmap archivedIcon; + + static { + Resources res = AndroidUtils.getRes(); + stickyIcon = BitmapFactory.decodeResource(res, R.drawable.sticky_icon); + closedIcon = BitmapFactory.decodeResource(res, R.drawable.closed_icon); + trashIcon = BitmapFactory.decodeResource(res, R.drawable.trash_icon); + archivedIcon = BitmapFactory.decodeResource(res, R.drawable.archived_icon); + } + + public static class PostIcons extends View { + private static final int STICKY = 0x1; + private static final int CLOSED = 0x2; + private static final int DELETED = 0x4; + private static final int ARCHIVED = 0x8; + private static final int COUNTRY = 0x10; + + private int height; + private int spacing; + private int icons; + private int previousIcons; + private RectF drawRect = new RectF(); + + private Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Rect textRect = new Rect(); + private ImageLoader.ImageContainer countryIconRequest; + private Bitmap countryIcon; + private String countryName; + private int countryTextColor; + private int countryTextSize; + + public PostIcons(Context context) { + super(context); + init(); + } + + public PostIcons(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public PostIcons(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + textPaint.setTypeface(Typeface.create((String) null, Typeface.ITALIC)); + setVisibility(View.GONE); + } + + public void setHeight(int height) { + this.height = height; + } + + public void setSpacing(int spacing) { + this.spacing = spacing; + } + + public void edit() { + previousIcons = icons; + } + + public void apply() { + if (previousIcons != icons) { + if (previousIcons == 0 || icons == 0) { + setVisibility(icons == 0 ? View.GONE : View.VISIBLE); + requestLayout(); + } + + invalidate(); + } + } + + public void showCountry(final Post post, final Theme theme, int textSize) { + countryName = post.countryName; + countryTextColor = theme.detailsColor; + countryTextSize = textSize; + + countryIconRequest = Chan.getVolleyImageLoader().get(post.countryUrl, new ImageLoader.ImageListener() { + @Override + public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { + if (response.getBitmap() != null) { + countryIcon = response.getBitmap(); + + invalidate(); + } + } + + @Override + public void onErrorResponse(VolleyError error) { + } + }); + } + + public void cancelCountryRequest() { + if (countryIconRequest != null) { + countryIconRequest.cancelRequest(); + countryIconRequest = null; + countryIcon = null; + countryName = null; + countryTextColor = 0; + } + } + + public void set(int icon, boolean enable) { + if (enable) { + icons |= icon; + } else { + icons &= ~icon; + } + } + + public boolean get(int icon) { + return (icons & icon) == icon; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureHeight = icons == 0 ? 0 : (height + getPaddingTop() + getPaddingBottom()); + + setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY)); + } + + @Override + protected void onDraw(Canvas canvas) { + if (icons != 0) { + canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + + int offset = 0; + + if (get(STICKY)) { + offset += drawBitmap(canvas, stickyIcon, offset); + } + + if (get(CLOSED)) { + offset += drawBitmap(canvas, closedIcon, offset); + } + + if (get(DELETED)) { + offset += drawBitmap(canvas, trashIcon, offset); + } + + if (get(ARCHIVED)) { + offset += drawBitmap(canvas, archivedIcon, offset); + } + + if (get(COUNTRY) && countryIcon != null) { + offset += drawBitmap(canvas, countryIcon, offset); + + textPaint.setColor(countryTextColor); + textPaint.setTextSize(countryTextSize); + textPaint.getTextBounds(countryName, 0, countryName.length(), textRect); + float y = height / 2f - textRect.exactCenterY(); + canvas.drawText(countryName, offset, y, textPaint); + } + + canvas.restore(); + } + } + + private int drawBitmap(Canvas canvas, Bitmap bitmap, int offset) { + int width = (int) (((float) height / bitmap.getHeight()) * bitmap.getWidth()); + drawRect.set(offset, 0f, offset + width, height); + canvas.drawBitmap(bitmap, null, drawRect, null); + return width + spacing; + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java index 5e3b84d5..38bc8e3c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java @@ -51,11 +51,10 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen @Override public boolean handleMessage(Message msg) { if (msg.what == MESSAGE_INVALIDATE) { - if (running) { + if (running && update()) { schedule(); } - update(); return true; } else { return false; @@ -86,14 +85,18 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen this.error = error; } - public void update() { + public boolean update() { if (error != null) { text.setText(error + "\n" + getContext().getString(R.string.thread_refresh_bar_inactive)); + return false; } else { ChanThread chanThread = callback.getChanThread(); if (chanThread == null) { - return; // Recyclerview not clearing immediately or view didn't receive onDetachedFromWindow + return false; // Recyclerview not clearing immediately or view didn't receive onDetachedFromWindow } + + boolean update = false; + String statusText = ""; if (chanThread.archived) { @@ -111,6 +114,7 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen } else { statusText += getContext().getString(R.string.thread_refresh_countdown, time) + "\n"; } + update = true; } Post op = chanThread.op; @@ -128,14 +132,15 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen text.setText(TextUtils.concat(statusText, replies, " / ", images, " / ", String.valueOf(op.uniqueIps) + "P")); } + + return update; } } private void schedule() { running = true; - Message message = handler.obtainMessage(1); if (!handler.hasMessages(MESSAGE_INVALIDATE)) { - handler.sendMessageDelayed(message, UPDATE_INTERVAL); + handler.sendMessageDelayed(handler.obtainMessage(MESSAGE_INVALIDATE), UPDATE_INTERVAL); } } @@ -160,8 +165,9 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (hasWindowFocus) { - update(); - schedule(); + if (update()) { + schedule(); + } } else { unschedule(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java new file mode 100644 index 00000000..dea7f1de --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java @@ -0,0 +1,261 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.ui.text; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.Spannable; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.LruCache; +import android.view.MotionEvent; +import android.view.View; + +import org.floens.chan.R; +import org.floens.chan.ui.cell.PostCell; +import org.floens.chan.utils.Logger; + +import static org.floens.chan.utils.AndroidUtils.sp; + +/** + * A simple implementation of a TextView that caches the used StaticLayouts for performance.
+ * This view was made for {@link PostCell} and may have untested behaviour with other layouts. + */ +public class FastTextView extends View { + private static final String TAG = "FastTextView"; + private static LruCache textCache = new LruCache<>(1000); + + private TextPaint paint; + private boolean singleLine; + + private CharSequence text; + + private boolean update = false; + private StaticLayout layout; + private int width; + private FastTextViewMovementMethod movementMethod; + + public FastTextView(Context context) { + this(context, null); + } + + public FastTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FastTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FastTextView); + setTextColor(a.getColor(R.styleable.FastTextView_textColor, 0xff000000)); + setTextSize(a.getDimensionPixelSize(R.styleable.FastTextView_textSize, 15)); + singleLine = a.getBoolean(R.styleable.FastTextView_singleLine, false); + a.recycle(); + } + + public void setText(CharSequence text) { + if (!TextUtils.equals(this.text, text)) { + this.text = text; + + update = true; + invalidate(); + requestLayout(); + } + } + + public void setTextSize(float size) { + int sizeSp = sp(size); + if (paint.getTextSize() != sizeSp) { + paint.setTextSize(sizeSp); + update = true; + invalidate(); + } + } + + public void setTextColor(int color) { + if (paint.getColor() != color) { + paint.setColor(color); + update = true; + invalidate(); + } + } + + public void setMovementMethod(FastTextViewMovementMethod movementMethod) { + if (this.movementMethod != movementMethod) { + this.movementMethod = movementMethod; + + if (movementMethod != null) { + setFocusable(true); + setClickable(true); + setLongClickable(true); + } else { + setFocusable(false); + setClickable(false); + setLongClickable(false); + } + + update = true; + invalidate(); + } + } + + public StaticLayout getLayout() { + return layout; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = false; + + if (movementMethod != null && text instanceof Spannable && layout != null && isEnabled()) { + handled |= movementMethod.onTouchEvent(this, (Spannable) layout.getText(), event); + } + + return handled || super.onTouchEvent(event); + } + + @Override + protected void onDraw(Canvas canvas) { + updateLayout(); + + if (layout != null) { + canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + layout.draw(canvas); + canvas.restore(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + +// Logger.test("%X %s %s", System.identityHashCode(this), MeasureSpec.toString(widthMeasureSpec), MeasureSpec.toString(heightMeasureSpec)); + + if ((widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && !singleLine) { + throw new IllegalArgumentException("FasTextView only supports wrapping widths on a single line"); + } + + int width = 0; + if (widthMode == MeasureSpec.EXACTLY) { + width = widthSize; + } else if ((widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && !TextUtils.isEmpty(text)) { + width = (int) Layout.getDesiredWidth(text, paint) + getPaddingLeft() + getPaddingRight(); + if (widthMode == MeasureSpec.AT_MOST) { + width = Math.min(width, widthSize); + } + } + + if (width > 0) { + if (this.width != width) { + this.width = width; + update = true; + } + + updateLayout(); + + if (layout != null) { + int height; + if (heightMode == MeasureSpec.EXACTLY) { + height = heightSize; + } else { + height = layout.getHeight() + getPaddingTop() + getPaddingBottom(); + if (heightMode == MeasureSpec.AT_MOST) { + height = Math.min(height, heightSize); + } + } + + setMeasuredDimension(width, height); + } else { + int height; + if (heightMode == MeasureSpec.EXACTLY) { + height = heightSize; + } else { + height = 0; + } + + setMeasuredDimension(width, height); + } + } else { + // Width is 0, ignore + Logger.w(TAG, "Width = 0"); + setMeasuredDimension(0, 0); + } + } + + private void updateLayout() { + if (!TextUtils.isEmpty(text)) { + if (update) { + int layoutWidth = width - getPaddingLeft() - getPaddingRight(); + if (layoutWidth > 0) { +// long start = Time.startTiming(); + + // The StaticLayouts are cached with the static textCache LRU map + + // Use .toString() to make sure we take the hashcode from the string representation + // and not from any spannables that are in it + long cacheKey = text.toString().hashCode(); + cacheKey = 31 * cacheKey + paint.getColor(); + cacheKey = 31 * cacheKey + Float.floatToIntBits(paint.getTextSize()); + cacheKey = 31 * cacheKey + layoutWidth; + + StaticLayout cached = textCache.get(cacheKey); + if (cached == null) { +// Logger.test("staticlayout cache miss: text = %s", text); + cached = getStaticLayout(layoutWidth); + textCache.put(cacheKey, cached); + } else { +// Logger.test("staticlayout cache hit"); + // Make sure the layout has the actual text, color, size and width, hashcodes almost never collide + Paint cachedPaint = cached.getPaint(); + if (!text.toString().equals(cached.getText().toString()) || + cachedPaint.getColor() != paint.getColor() || + cachedPaint.getTextSize() != paint.getTextSize() || + cached.getWidth() != layoutWidth) { + Logger.w(TAG, "Cache miss with the same hashcode %x: \"%s\" \"%s\"!", cacheKey, text.toString(), cached.getText().toString()); + cached = getStaticLayout(layoutWidth); + } + } + + layout = cached; +// Time.endTiming(Integer.toHexString(System.identityHashCode(this)) + " staticlayout for width = " + layoutWidth + "\t", start); + } else { + layout = null; + } + } + } else { + layout = null; + } + update = false; + } + + private StaticLayout getStaticLayout(int layoutWidth) { +// Logger.test("new staticlayout width=%d", layoutWidth); + return new StaticLayout(text, paint, layoutWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextViewMovementMethod.java b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextViewMovementMethod.java new file mode 100644 index 00000000..49a648d5 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextViewMovementMethod.java @@ -0,0 +1,25 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.floens.chan.ui.text; + +import android.text.Spannable; +import android.view.MotionEvent; + +public interface FastTextViewMovementMethod { + boolean onTouchEvent(FastTextView widget, Spannable text, MotionEvent event); +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/FastTextView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/FastTextView.java deleted file mode 100644 index d99b7a0f..00000000 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/FastTextView.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.ui.view; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.text.Layout; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; - -import static org.floens.chan.utils.AndroidUtils.sp; - -public class FastTextView extends View { - private TextPaint paint; - - private CharSequence text; - - private boolean update = false; - private StaticLayout layout; - - public FastTextView(Context context) { - super(context); - init(); - } - - public FastTextView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public FastTextView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - private void init() { - paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - } - - public void setText(CharSequence text) { - if (!TextUtils.equals(this.text, text)) { - this.text = text; - - if (text == null) { - layout = null; - } else { - update = true; - } - } - } - - public void setTextSize(float size) { - int sizeSp = sp(size); - if (paint.getTextSize() != sizeSp) { - paint.setTextSize(sizeSp); - update = true; - } - } - - public void setTextColor(int color) { - if (paint.getColor() != color) { - paint.setColor(color); - update = true; - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - update = true; - } - - @Override - protected void onDraw(Canvas canvas) { - if (update) { - int width = getWidth() - getPaddingLeft() - getPaddingRight(); - layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, 1, 0, false); - update = false; - } - - if (layout != null) { - canvas.save(); - canvas.translate(getPaddingLeft(), getPaddingTop()); - layout.draw(canvas); - canvas.restore(); - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/Logger.java b/Clover/app/src/main/java/org/floens/chan/utils/Logger.java index 8045c8d8..04991109 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/Logger.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/Logger.java @@ -30,25 +30,41 @@ public class Logger { } public static void v(String tag, String message) { - Log.v(TAG + TAG_SPACER + tag, message); + if (debugEnabled()) { + Log.v(TAG + TAG_SPACER + tag, message); + } } public static void v(String tag, String message, Throwable throwable) { - Log.v(TAG + TAG_SPACER + tag, message, throwable); + if (debugEnabled()) { + Log.v(TAG + TAG_SPACER + tag, message, throwable); + } + } + + public static void v(String tag, String format, Object... args) { + if (debugEnabled()) { + Log.v(TAG + TAG_SPACER + tag, String.format(format, args)); + } } public static void d(String tag, String message) { - if (ChanBuild.DEVELOPER_MODE) { + if (debugEnabled()) { Log.d(TAG + TAG_SPACER + tag, message); } } public static void d(String tag, String message, Throwable throwable) { - if (ChanBuild.DEVELOPER_MODE) { + if (debugEnabled()) { Log.d(TAG + TAG_SPACER + tag, message, throwable); } } + public static void d(String tag, String format, Object... args) { + if (debugEnabled()) { + Log.d(TAG + TAG_SPACER + tag, String.format(format, args)); + } + } + public static void i(String tag, String message) { Log.i(TAG + TAG_SPACER + tag, message); } @@ -57,6 +73,10 @@ public class Logger { Log.i(TAG + TAG_SPACER + tag, message, throwable); } + public static void i(String tag, String format, Object... args) { + Log.i(TAG + TAG_SPACER + tag, String.format(format, args)); + } + public static void w(String tag, String message) { Log.w(TAG + TAG_SPACER + tag, message); } @@ -65,6 +85,10 @@ public class Logger { Log.w(TAG + TAG_SPACER + tag, message, throwable); } + public static void w(String tag, String format, Object... args) { + Log.w(TAG + TAG_SPACER + tag, String.format(format, args)); + } + public static void e(String tag, String message) { Log.e(TAG + TAG_SPACER + tag, message); } @@ -73,6 +97,10 @@ public class Logger { Log.e(TAG + TAG_SPACER + tag, message, throwable); } + public static void e(String tag, String format, Object... args) { + Log.e(TAG + TAG_SPACER + tag, String.format(format, args)); + } + public static void wtf(String tag, String message) { Log.wtf(TAG + TAG_SPACER + tag, message); } @@ -81,15 +109,25 @@ public class Logger { Log.wtf(TAG + TAG_SPACER + tag, message, throwable); } + public static void wtf(String tag, String format, Object... args) { + Log.wtf(TAG + TAG_SPACER + tag, String.format(format, args)); + } + public static void test(String message) { - if (ChanBuild.DEVELOPER_MODE) { + if (debugEnabled()) { Log.i(TAG + TAG_SPACER + "test", message); } } public static void test(String message, Throwable throwable) { - if (ChanBuild.DEVELOPER_MODE) { + if (debugEnabled()) { Log.i(TAG + TAG_SPACER + "test", message, throwable); } } + + public static void test(String format, Object... args) { + if (debugEnabled()) { + Log.i(TAG + TAG_SPACER + "test", String.format(format, args)); + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/Time.java b/Clover/app/src/main/java/org/floens/chan/utils/Time.java index 1e858ef5..a95ed9b0 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/Time.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/Time.java @@ -22,7 +22,11 @@ public class Time { return System.currentTimeMillis(); } - public static long get(long other) { - return System.currentTimeMillis() - other; + public static long startTiming() { + return System.nanoTime(); + } + + public static void endTiming(String tag, long start) { + Logger.test(tag + " took " + ((System.nanoTime() - start) / 1_000_000.0) + "ms"); } } diff --git a/Clover/app/src/main/res/layout/cell_post.xml b/Clover/app/src/main/res/layout/cell_post.xml index da763761..6df46358 100644 --- a/Clover/app/src/main/res/layout/cell_post.xml +++ b/Clover/app/src/main/res/layout/cell_post.xml @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> . android:layout_alignWithParentIfMissing="true" android:gravity="top" /> - . android:layout_toRightOf="@id/thumbnail_view" android:paddingRight="25dp" /> - . android:layout_below="@id/title" android:layout_toRightOf="@id/thumbnail_view" /> - . android:layout_alignWithParentIfMissing="true" android:layout_below="@id/icons" android:layout_toRightOf="@id/thumbnail_view" - android:textColor="?attr/text_color_primary" /> + app:textColor="?attr/text_color_primary" /> - + app:singleLine="true" + app:textColor="?attr/text_color_secondary" /> . android:paddingRight="8dp" android:paddingTop="8dp" /> - . #ffffffff + + + + + +