Java tutorial
/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * 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.kontalk.ui.view; import android.content.Context; import android.support.v4.content.ContextCompat; import android.support.v4.widget.TextViewCompat; import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.style.BackgroundColorSpan; import android.text.util.Linkify; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.TextView; import org.kontalk.R; import org.kontalk.message.TextComponent; import org.kontalk.util.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.rockerhieu.emojicon.EmojiconTextView; /** * Message component for {@link TextComponent}. * @author Daniele Ricci */ public class TextContentView extends EmojiconTextView implements MessageContentView<TextComponent> { // pool-related stuff private static final Object sPoolSync = new Object(); private static TextContentView sPool; private static int sPoolSize = 0; /** Global pool max size. */ private static final int MAX_POOL_SIZE = 50; /** Used for pooling. */ protected TextContentView next; /** * Maximum affordable size of a text message to make complex stuff * (e.g. emoji, linkify, etc.) */ private static final int MAX_AFFORDABLE_SIZE = 10240; // 10 KB private TextComponent mComponent; private boolean mEncryptionPlaceholder; private BackgroundColorSpan mHighlightColorSpan; // set in ctor private boolean mMeasureHack; public TextContentView(Context context) { super(context); init(context); } public TextContentView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public TextContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { int color = ContextCompat.getColor(context, R.color.highlight_color); mHighlightColorSpan = new BackgroundColorSpan(color); } /** * Hack for fixing extra space took by the TextView. * I still have to understand why this works and plain getHeight() doesn't. * http://stackoverflow.com/questions/7439748/why-is-wrap-content-in-multiple-line-textview-filling-parent * https://github.com/qklabs/qksms/blob/master/QKSMS/src/main/java/com/moez/QKSMS/ui/view/QKTextView.java */ void enableMeasureHack(boolean enabled) { mMeasureHack = enabled; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mMeasureHack) { int specModeW = MeasureSpec.getMode(widthMeasureSpec); if (specModeW != MeasureSpec.EXACTLY) { Layout layout = getLayout(); int linesCount = layout.getLineCount(); if (linesCount > 1) { float textRealMaxWidth = 0; for (int n = 0; n < linesCount; ++n) { textRealMaxWidth = Math.max(textRealMaxWidth, layout.getLineWidth(n)); } int w = Math.round(textRealMaxWidth); if (w < getMeasuredWidth()) { super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST), heightMeasureSpec); } } } } } private float getMaxLineWidth(Layout layout) { float max_width = 0.0f; int lines = layout.getLineCount(); for (int i = 0; i < lines; i++) { if (layout.getLineWidth(i) > max_width) { max_width = layout.getLineWidth(i); } } return max_width; } @Override public void bind(long databaseId, TextComponent component, Pattern highlight) { mComponent = component; SpannableStringBuilder formattedMessage = formatMessage(highlight); setTextStyle(this); // linkify! if (formattedMessage.length() < MAX_AFFORDABLE_SIZE) Linkify.addLinks(formattedMessage, Linkify.ALL); /* * workaround for bugs: * http://code.google.com/p/android/issues/detail?id=17343 * http://code.google.com/p/android/issues/detail?id=22493 * applies from Honeycomb to JB 4.2.2 afaik */ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB && android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) // from http://stackoverflow.com/a/12303155/1045199 formattedMessage.append("\u200b"); // was: \u2060 setText(formattedMessage); } @Override public void unbind() { recycle(); } @Override public TextComponent getComponent() { return mComponent; } /** Text is always below. */ @Override public int getPriority() { return 10; } public boolean isEncryptionPlaceholder() { return mEncryptionPlaceholder; } private SpannableStringBuilder formatMessage(final Pattern highlight) { SpannableStringBuilder buf; String textContent = mComponent.getContent(); buf = new SpannableStringBuilder(textContent); if (highlight != null) { Matcher m = highlight.matcher(buf.toString()); while (m.find()) buf.setSpan(mHighlightColorSpan, m.start(), m.end(), 0); } return buf; } private void clear() { mComponent = null; } public void recycle() { clear(); synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } } public static TextContentView obtain(LayoutInflater inflater, ViewGroup parent) { return obtain(inflater, parent, false); } /** * Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases. Inspired by {@link android.os.Message}. * @param encryptionPlaceholder true if the whole message is encrypted and this is a placeholder. */ public static TextContentView obtain(LayoutInflater inflater, ViewGroup parent, boolean encryptionPlaceholder) { synchronized (sPoolSync) { if (sPool != null) { TextContentView m = sPool; sPool = m.next; m.next = null; sPoolSize--; //m.mContext = context; m.mEncryptionPlaceholder = encryptionPlaceholder; return m; } } return create(inflater, parent, encryptionPlaceholder); } public static TextContentView create(LayoutInflater inflater, ViewGroup parent, boolean encryptionPlaceholder) { TextContentView view = (TextContentView) inflater.inflate(R.layout.message_content_text, parent, false); view.mEncryptionPlaceholder = encryptionPlaceholder; return view; } public static void setTextStyle(TextView textView) { Context context = textView.getContext(); String size = Preferences.getFontSize(context); int sizeId; if (size.equals("small")) sizeId = android.R.style.TextAppearance_Small; else if (size.equals("large")) sizeId = android.R.style.TextAppearance_Large; else sizeId = android.R.style.TextAppearance; TextViewCompat.setTextAppearance(textView, sizeId); //setEmojiconSize((int) getTextSize()); } }