diff --git a/base/src/main/java/com/heyongrui/base/widget/CollapsedTextView.java b/base/src/main/java/com/heyongrui/base/widget/CollapsedTextView.java new file mode 100644 index 0000000..52db667 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/CollapsedTextView.java @@ -0,0 +1,455 @@ +package com.heyongrui.base.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.ImageSpan; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.core.content.ContextCompat; + +import com.heyongrui.base.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class CollapsedTextView extends AppCompatTextView implements View.OnClickListener { + /** + * 末尾省略号 + */ + private static final String ELLIPSE = "..."; + /** + * 默认的折叠行数 + */ + public static final int COLLAPSED_LINES = 4; + /** + * 在文本末尾 + */ + public static final int END = 0; + /** + * 在文本下方 + */ + public static final int BOTTOM = 1; + + /** + * 提示文字展示的位置 + */ + @IntDef({END, BOTTOM}) + @Retention(RetentionPolicy.SOURCE) + public @interface TipsGravityMode { + } + + /** + * 折叠的行数 + */ + private int mCollapsedLines; + /** + * 折叠时的文本 + */ + private String mExpandedText; + /** + * 展开时的文本 + */ + private String mCollapsedText; + /** + * 折叠时的图片资源 + */ + private Drawable mExpandedDrawable; + /** + * 展开时的图片资源 + */ + private Drawable mCollapsedDrawable; + /** + * 原始的文本 + */ + private CharSequence mOriginalText; + /** + * TextView中文字可显示的宽度 + */ + private int mShowWidth; + /** + * 是否是展开的 + */ + private boolean mIsExpanded; + /** + * 提示文字位置 + */ + private int mTipsGravity; + /** + * 提示文字颜色 + */ + private int mTipsColor; + /** + * 提示文字是否显示下划线 + */ + private boolean mTipsUnderline; + /** + * 提示是否可点击 + */ + private boolean mTipsClickable; + /** + * 提示文本的点击事件 + */ + private ExpandedClickableSpan mClickableSpan = new ExpandedClickableSpan(); + /** + * TextView的点击事件监听 + */ + private OnClickListener mListener; + /** + * 是否响应TextView的点击事件 + */ + private boolean mIsResponseListener = true; + + public CollapsedTextView(Context context) { + this(context, null); + } + + public CollapsedTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + // 使点击有效 + setMovementMethod(LinkMovementMethod.getInstance()); + } + + /** + * 初始化属性 + */ + private void init(Context context, AttributeSet attrs) { + if (attrs != null) { + TypedArray typed = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView); + mCollapsedLines = typed.getInt(R.styleable.CollapsedTextView_collapsedLines, COLLAPSED_LINES); + setExpandedText(typed.getString(R.styleable.CollapsedTextView_expandedText)); + setCollapsedText(typed.getString(R.styleable.CollapsedTextView_collapsedText)); + setExpandedDrawable(typed.getDrawable(R.styleable.CollapsedTextView_expandedDrawable)); + setCollapsedDrawable(typed.getDrawable(R.styleable.CollapsedTextView_collapsedDrawable)); + mTipsGravity = typed.getInt(R.styleable.CollapsedTextView_tipsGravity, END); + mTipsColor = typed.getColor(R.styleable.CollapsedTextView_tipsColor, 0); + mTipsUnderline = typed.getBoolean(R.styleable.CollapsedTextView_tipsUnderline, false); + mTipsClickable = typed.getBoolean(R.styleable.CollapsedTextView_tipsClickable, true); + typed.recycle(); + } + } + + /** + * 设置折叠行数 + * + * @param collapsedLines 折叠行数 + */ + public void setCollapsedLines(@IntRange(from = 0) int collapsedLines) { + this.mCollapsedLines = collapsedLines; + } + + /** + * 设置折叠时的提示文本 + * + * @param expandedText 提示文本 + */ + public void setExpandedText(String expandedText) { + this.mExpandedText = TextUtils.isEmpty(expandedText) ? getContext().getString(R.string.unfold) : expandedText; + } + + /** + * 设置展开时的提示文本 + * + * @param collapsedText 提示文本 + */ + public void setCollapsedText(String collapsedText) { + this.mCollapsedText = TextUtils.isEmpty(collapsedText) ? getContext().getString(R.string.collapse) : collapsedText; + } + + /** + * 设置折叠时的提示图片 + * + * @param resId 图片资源 + */ + public void setExpandedDrawableRes(@DrawableRes int resId) { + setExpandedDrawable(ContextCompat.getDrawable(getContext(), resId)); + } + + /** + * 设置折叠时的提示图片 + * + * @param expandedDrawable 图片 + */ + public void setExpandedDrawable(Drawable expandedDrawable) { + if (expandedDrawable != null) { + this.mExpandedDrawable = expandedDrawable; + this.mExpandedDrawable.setBounds(0, 0, mExpandedDrawable.getIntrinsicWidth(), mExpandedDrawable.getIntrinsicHeight()); + } + } + + /** + * 设置展开时的提示图片 + * + * @param resId 图片资源 + */ + public void setCollapsedDrawableRes(@DrawableRes int resId) { + setCollapsedDrawable(ContextCompat.getDrawable(getContext(), resId)); + } + + /** + * 设置展开时的提示图片 + * + * @param collapsedDrawable 图片 + */ + public void setCollapsedDrawable(Drawable collapsedDrawable) { + if (collapsedDrawable != null) { + this.mCollapsedDrawable = collapsedDrawable; + this.mCollapsedDrawable.setBounds(0, 0, mCollapsedDrawable.getIntrinsicWidth(), mCollapsedDrawable.getIntrinsicHeight()); + } + } + + /** + * 设置提示的位置 + * + * @param tipsGravity END 表示在文字末尾,BOTTOM 表示在文字下方 + */ + public void setTipsGravity(@TipsGravityMode int tipsGravity) { + this.mTipsGravity = tipsGravity; + } + + /** + * 设置文字提示的颜色 + * + * @param tipsColor 颜色 + */ + public void setTipsColor(@ColorInt int tipsColor) { + this.mTipsColor = tipsColor; + } + + /** + * 设置提示文字是否有下划线 + * + * @param tipsUnderline true 表示有下划线 + */ + public void setTipsUnderline(boolean tipsUnderline) { + this.mTipsUnderline = tipsUnderline; + } + + /** + * 设置提示文字是否可点击 + * + * @param tipsClickable true 表示可点击 + */ + public void setTipsClickable(boolean tipsClickable) { + this.mTipsClickable = tipsClickable; + } + + @Override + public void setText(final CharSequence text, final BufferType type) { + // 如果text为空或mCollapsedLines为0则直接显示 + if (TextUtils.isEmpty(text) || mCollapsedLines == 0) { + super.setText(text, type); + } else if (mIsExpanded) { + // 保存原始文本,去掉文本末尾的空字符 + this.mOriginalText = trimFrom(text); + formatExpandedText(type); + } else { + // 获取TextView中文字显示的宽度,需要在layout之后才能获取到,避免重复获取 + if (mShowWidth == 0) { + getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + mShowWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + formatCollapsedText(type, text); + } + }); + } else { + formatCollapsedText(type, text); + } + } + } + + /** + * 格式化折叠时的文本 + * + * @param type ref android.R.styleable#TextView_bufferType + */ + private void formatCollapsedText(BufferType type, CharSequence text) { + // 保存原始文本,去掉文本末尾的空字符 + this.mOriginalText = trimFrom(text); + // 获取 layout,用于计算行数 + Layout layout = getLayout(); + // 调用 setText 用于重置 Layout + if (layout == null || !layout.getText().equals(mOriginalText)) { + super.setText(mOriginalText, type); + layout = getLayout(); + } + // 获取 paint,用于计算文字宽度 + TextPaint paint = getPaint(); + + int line = layout.getLineCount(); + if (line <= mCollapsedLines) { + super.setText(mOriginalText, type); + } else { + // 最后一行的开始字符位置 + int lastLineStart = layout.getLineStart(mCollapsedLines - 1); + // 最后一行的结束字符位置 + int lastLineEnd = layout.getLineVisibleEnd(mCollapsedLines - 1); + // 计算后缀的宽度 + int expandedTextWidth; + if (mTipsGravity == END) { + expandedTextWidth = (int) paint.measureText(ELLIPSE + " " + mExpandedText); + } else { + expandedTextWidth = (int) paint.measureText(ELLIPSE + " "); + } + // 最后一行的宽 + float lastLineWidth = layout.getLineWidth(mCollapsedLines - 1); + // 如果大于屏幕宽度则需要减去部分字符 + if (lastLineWidth + expandedTextWidth > mShowWidth) { + int cutCount = paint.breakText(mOriginalText, lastLineStart, lastLineEnd, false, expandedTextWidth, null); + lastLineEnd -= cutCount; + } + // 因设置的文本可能是带有样式的文本,如SpannableStringBuilder,所以根据计算的字符数从原始文本中截取 + SpannableStringBuilder spannable = new SpannableStringBuilder(); + // 截取文本,还是因为原始文本的样式原因不能直接使用paragraphs中的文本 + CharSequence ellipsizeText = mOriginalText.subSequence(0, lastLineEnd); + spannable.append(ellipsizeText); + spannable.append(ELLIPSE); + // 设置样式 + setSpan(spannable); + super.setText(spannable, type); + } + } + + /** + * 格式化展开式的文本,直接在后面拼接即可 + * + * @param type + */ + private void formatExpandedText(BufferType type) { + SpannableStringBuilder spannable = new SpannableStringBuilder(mOriginalText); + setSpan(spannable); + super.setText(spannable, type); + } + + /** + * 设置提示的样式 + * + * @param spannable 需修改样式的文本 + */ + private void setSpan(SpannableStringBuilder spannable) { + Drawable drawable; + // 根据提示文本需要展示的文字拼接不同的字符 + if (mTipsGravity == END) { + spannable.append(" "); + } else { + spannable.append("\n"); + } + int tipsLen; + // 判断是展开还是收起 + if (mIsExpanded) { + spannable.append(mCollapsedText); + drawable = mCollapsedDrawable; + tipsLen = mCollapsedText.length(); + } else { + spannable.append(mExpandedText); + drawable = mExpandedDrawable; + tipsLen = mExpandedText.length(); + } + // 设置点击事件 + spannable.setSpan(mClickableSpan, spannable.length() - tipsLen, + spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + // 如果提示的图片资源不为空,则使用图片代替提示文本 + if (drawable != null) { + spannable.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE), + spannable.length() - tipsLen, spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + spannable.append(" "); + } + } + + public CharSequence trimFrom(CharSequence sequence) { + int len = sequence.length(); + int first = 0; + int last; + for (last = len - 1; last > first; last--) { + if (!matches(sequence.charAt(last))) { + break; + } + } + return sequence.subSequence(first, last + 1); + } + + private boolean matches(char c) { + switch (c) { + case '\t': + case '\n': + case '\013': + case '\f': + case '\r': + case ' ': + case '\u0085': + case '\u1680': + case '\u2028': + case '\u2029': + case '\u205f': + case '\u3000': + return true; + case '\u2007': + return false; + default: + return c >= '\u2000' && c <= '\u200a'; + } + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + // 保存TextView的点击监听事件 + this.mListener = l; + super.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + if (!mIsResponseListener) { + // 如果不响应TextView的点击事件,将属性置为true + mIsResponseListener = true; + } else if (mListener != null) { + // 如果响应TextView的点击事件且监听不为空,则响应点击事件 + mListener.onClick(v); + } + } + + /** + * 提示的点击事件 + */ + private class ExpandedClickableSpan extends ClickableSpan { + + @Override + public void onClick(View widget) { + // 是否可点击 + if (mTipsClickable) { + mIsResponseListener = false; + mIsExpanded = !mIsExpanded; + setText(mOriginalText); + } + } + + @Override + public void updateDrawState(TextPaint ds) { + // 设置提示文本的颜色和是否需要下划线 + ds.setColor(mTipsColor == 0 ? ds.linkColor : mTipsColor); + ds.setUnderlineText(mTipsUnderline); + } + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetBallView.java b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetBallView.java index 78fab78..fd22482 100644 --- a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetBallView.java +++ b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetBallView.java @@ -406,13 +406,16 @@ private void processTouch() { int left = (int) (centerX + planetModel.getLoc2DX()) - child.getMeasuredWidth() / 2; int top = (int) (centerY + planetModel.getLoc2DY()) - child.getMeasuredHeight() / 2; // 从View的Tag里取出位置之前的位置信息,平移新旧位置差值 - int[] originLocation = (int[]) child.getTag(); - if (originLocation != null && originLocation.length > 0) { - child.setTranslationX((float) (left - originLocation[0])); - child.setTranslationY((float) (top - originLocation[1])); - // 小于移动速度,刷新 - if (Math.abs(mAngleX) <= speed && Math.abs(mAngleY) <= speed) { - child.invalidate(); + Object childTag = child.getTag(); + if (null != childTag && childTag instanceof int[]) { + int[] originLocation = (int[]) childTag; + if (originLocation != null && originLocation.length > 0) { + child.setTranslationX((float) (left - originLocation[0])); + child.setTranslationY((float) (top - originLocation[1])); + // 小于移动速度,刷新 + if (Math.abs(mAngleX) <= speed && Math.abs(mAngleY) <= speed) { + child.invalidate(); + } } } } diff --git a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetModel.java b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetModel.java index 6339f33..8d2dd3a 100644 --- a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetModel.java +++ b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetModel.java @@ -102,7 +102,7 @@ public float getScale() { public void setScale(float scale) { this.scale = scale; - if (this.mView != null) { + if (null != this.mView && this.mView instanceof PlanetView) { ((PlanetView) this.mView).setScale(scale); } } diff --git a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetView.java b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetView.java index 2371b6a..8be7ab7 100644 --- a/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetView.java +++ b/base/src/main/java/com/heyongrui/base/widget/planetball/view/PlanetView.java @@ -59,6 +59,16 @@ public PlanetView(Context context) { init(context); } + public PlanetView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + init(context); + } + + public PlanetView(Context context, AttributeSet attributeSet, int i) { + super(context, attributeSet, i); + init(context); + } + private void init(Context context) { starPaint = new Paint(Paint.ANTI_ALIAS_FLAG); starPaint.setColor(0xFFFF0000); @@ -90,20 +100,23 @@ private void init(Context context) { radiusIncrement = starMin / 16.0f; } - public PlanetView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - init(context); - } - - public PlanetView(Context context, AttributeSet attributeSet, int i) { - super(context, attributeSet, i); - init(context); - } - public void setScale(float scale) { this.scale = scale; } + /** + * 设置文本昵称颜色 + * + * @param textColor int[],渐变颜色数组(eg:new int[]{0x33333333, PlanetView.COLOR_BEST_MATCH, PlanetView.COLOR_MOST_ACTIVE, 0x33333333}) + * @param position int[],对应color数组位置(eg:new float[]{0.0f, 0.15f, 0.85f, 1.0f}) + */ + public void setTextColor(int[] textColor, float[] position) { + int startX = sp2px(getContext(), 50.0f); + signPaint.setShader(new LinearGradient((float) startX, 0.0f, 0.0f, 0.0f, + textColor, position, TileMode.CLAMP)); + invalidate(); + } + /** * 设置星星颜色 * diff --git a/base/src/main/res/values/attrs.xml b/base/src/main/res/values/attrs.xml index 20c1464..bdd640e 100644 --- a/base/src/main/res/values/attrs.xml +++ b/base/src/main/res/values/attrs.xml @@ -110,4 +110,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 7f9d2ef..2fbb601 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -32,4 +32,6 @@ 没有搜索到相关内容 手机暂无应用支持打开此文件 添加 + 收起 + 展开 diff --git a/main/src/main/java/com/heyongrui/main/planetball/view/PlanetBallActivity.java b/main/src/main/java/com/heyongrui/main/planetball/view/PlanetBallActivity.java index 2850350..7fa63fb 100644 --- a/main/src/main/java/com/heyongrui/main/planetball/view/PlanetBallActivity.java +++ b/main/src/main/java/com/heyongrui/main/planetball/view/PlanetBallActivity.java @@ -1,14 +1,18 @@ package com.heyongrui.main.planetball.view; import android.content.Context; +import android.graphics.Color; import android.os.Bundle; +import android.os.Handler; import android.view.View; import android.view.ViewGroup; import com.alibaba.android.arouter.facade.annotation.Route; import com.blankj.utilcode.util.ConvertUtils; +import com.blankj.utilcode.util.ToastUtils; import com.heyongrui.base.assist.ConfigConstants; import com.heyongrui.base.base.BaseActivity; +import com.heyongrui.base.widget.firefly.FireflyView; import com.heyongrui.base.widget.planetball.adapter.PlanetAdapter; import com.heyongrui.base.widget.planetball.view.PlanetBallView; import com.heyongrui.base.widget.planetball.view.PlanetView; @@ -19,6 +23,10 @@ @Route(path = ConfigConstants.PATH_PLANET_BALL) public class PlanetBallActivity extends BaseActivity { + + private FireflyView fireflyView; + private Handler mHandler = new Handler(); + @Override protected int getLayoutId() { return R.layout.activity_planet_ball; @@ -27,7 +35,13 @@ protected int getLayoutId() { @Override protected void init(Bundle savedInstanceState) { PlanetBallView planetBall = findViewById(R.id.planet_ball); - planetBall.setAdapter(new PlanetAdapter() { + initPlanetBallView(planetBall); + + fireflyView = findViewById(R.id.firefly); + } + + private void initPlanetBallView(PlanetBallView planetBallView) { + planetBallView.setAdapter(new PlanetAdapter() { @Override public int getCount() { return 50; @@ -35,11 +49,8 @@ public int getCount() { @Override public View getView(Context context, int position, ViewGroup parent) { - PlanetView planetView = new PlanetView(context); - planetView.setSign(getRandomNick()); - int starColor = position % 2 == 0 ? PlanetView.COLOR_FEMALE : PlanetView.COLOR_MALE; - boolean hasShadow = false; + boolean isShining = false; String str = ""; if (position % 12 == 0) { @@ -52,15 +63,22 @@ public View getView(Context context, int position, ViewGroup parent) { str = "最新人"; starColor = PlanetView.COLOR_MOST_NEW; } else if (position % 18 == 0) { - hasShadow = true; + isShining = true; str = "最闪耀"; } else { str = "描述"; } + + PlanetView planetView = new PlanetView(context); + int[] textColor = new int[]{0x33333333, getRandomColor(), getRandomColor(), 0x33333333}; + float[] colorPosition = new float[]{0.0f, 0.15f, 0.85f, 1.0f}; + planetView.setTextColor(textColor, colorPosition); planetView.setStarColor(starColor); - planetView.setHasShadow(hasShadow); + String nickName = getRandomNick(); + planetView.setSign(nickName); + planetView.setHasShadow(isShining); planetView.setMatch(position * 2 + "%", str); - if (hasShadow) { + if (isShining) { planetView.setMatchColor(starColor); } else { planetView.setMatchColor(PlanetView.COLOR_MOST_ACTIVE); @@ -71,6 +89,7 @@ public View getView(Context context, int position, ViewGroup parent) { ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(starWidth, starHeight); planetView.setPadding(0, starPaddingTop, 0, 0); planetView.setLayoutParams(layoutParams); + planetView.setOnClickListener(view -> ToastUtils.showShort(nickName + "\n" + position * 2 + "%")); return planetView; } @@ -91,10 +110,19 @@ public void onThemeColorChanged(View view, int themeColor) { }); } + /** + * 获取随机颜色 + */ + private int getRandomColor() { + Random random = new Random(); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return Color.argb(255, r, g, b); + } + /** * 获取随机昵称 - * - * @return 随机昵称 */ private String getRandomNick() { Random random = new Random(); @@ -108,8 +136,6 @@ private String getRandomNick() { /** * 获取随机单个汉字 - * - * @return 随机单个汉字 */ private String getRandomSingleCharacter() { String str = ""; @@ -129,4 +155,48 @@ private String getRandomSingleCharacter() { return str; } + /** + * 控制萤火虫粒子动画是否播放 + */ + private void toggleAnimationEffect(boolean isStart) { + if (isStart) { + if (fireflyView != null && !fireflyView.isPlaying()) { + fireflyView.setVisibility(View.VISIBLE); + fireflyView.startAnimation(); + } + } else { + if (fireflyView != null && fireflyView.isPlaying()) { + fireflyView.stopAnimation(); +// fireflyView.setZOrderMediaOverlay(true); + fireflyView.setVisibility(View.GONE); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + if (null != mHandler) { + mHandler.removeCallbacksAndMessages(null); + mHandler.postDelayed(() -> toggleAnimationEffect(true), 1000); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (null != mHandler) { + mHandler.removeCallbacksAndMessages(null); + mHandler.postDelayed(() -> toggleAnimationEffect(false), 1000); + } + } + + @Override + protected void onDestroy() { + if (null != mHandler) { + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + } + super.onDestroy(); + } } diff --git a/main/src/main/res/layout/activity_planet_ball.xml b/main/src/main/res/layout/activity_planet_ball.xml index cbc46de..1898ea1 100644 --- a/main/src/main/res/layout/activity_planet_ball.xml +++ b/main/src/main/res/layout/activity_planet_ball.xml @@ -5,6 +5,17 @@ android:layout_height="match_parent" android:background="@color/black"> + + -