Improve FastTextView and use it for PostCells

The icons in PostCell is now its own view instead of a spanned string.
Optimized ThreadStatusCell updating
Add String.format style logging
multisite
Floens 10 years ago
parent a5e96882cd
commit 0412b181f6
  1. 1
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  2. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java
  3. 331
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
  4. 22
      Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java
  5. 261
      Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java
  6. 25
      Clover/app/src/main/java/org/floens/chan/ui/text/FastTextViewMovementMethod.java
  7. 106
      Clover/app/src/main/java/org/floens/chan/ui/view/FastTextView.java
  8. 50
      Clover/app/src/main/java/org/floens/chan/utils/Logger.java
  9. 8
      Clover/app/src/main/java/org/floens/chan/utils/Time.java
  10. 16
      Clover/app/src/main/res/layout/cell_post.xml
  11. 2
      Clover/app/src/main/res/layout/cell_post_card.xml
  12. 6
      Clover/app/src/main/res/values/styles.xml

@ -113,7 +113,6 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
}
}
@Override
public int getItemCount() {
int size = displayList.size();

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

@ -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.<br>
* 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;
}
}
}

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

@ -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 <http://www.gnu.org/licenses/>.
*/
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.<br>
* 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<Long, StaticLayout> 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);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

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

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

@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<org.floens.chan.ui.cell.PostCell xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/post_cell"
android:layout_width="match_parent"
@ -40,7 +41,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_alignWithParentIfMissing="true"
android:gravity="top" />
<TextView
<org.floens.chan.ui.text.FastTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -50,8 +51,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_toRightOf="@id/thumbnail_view"
android:paddingRight="25dp" />
<TextView
<view
android:id="@+id/icons"
class="org.floens.chan.ui.cell.PostCell$PostIcons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
@ -59,7 +61,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_below="@id/title"
android:layout_toRightOf="@id/thumbnail_view" />
<TextView
<org.floens.chan.ui.text.FastTextView
android:id="@+id/comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -67,17 +69,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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" />
<TextView
<org.floens.chan.ui.text.FastTextView
android:id="@+id/replies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/comment"
android:layout_toRightOf="@id/thumbnail_view"
android:singleLine="true"
android:textColor="?attr/text_color_secondary" />
app:singleLine="true"
app:textColor="?attr/text_color_secondary" />
<View
android:id="@+id/divider"

@ -68,7 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:paddingRight="8dp"
android:paddingTop="8dp" />
<org.floens.chan.ui.view.FastTextView
<org.floens.chan.ui.text.FastTextView
android:id="@+id/comment"
android:layout_width="match_parent"
android:layout_height="0dp"

@ -166,4 +166,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item name="android:background">#ffffffff</item>
</style>
<declare-styleable name="FastTextView">
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="singleLine" format="boolean" />
</declare-styleable>
</resources>

Loading…
Cancel
Save