啥叫文字轮播,不废话看图:
![](https://img.haomeiwen.com/i1785445/53f863dc6445b9d8.gif)
这是我们 app 项目中常常遇到的需求了,一般大家都是找找别人写好的第三方控件来用的,我也是这样。但是随着时间长河的流逝,我们也必须相应的成长, 文字轮播这种常见的东西可是不能错过的
我找一些实现,总结下实现思路:
-
ViewAnimator 思路
使用 ViewAnimator 自身特性,对期中的子 view 实现动画切换 -
自定义 viewGroup 思路
在这个思路下,我们自定义一个容器,继承 FrameLayout ,根据数据数量自己 new 相应数量的 itemView 出来加入 FrameLayout ,动画是通过对当前 itemView 做一个出去的佛纳甘话,同时对下一个 itemView 做一个进入动画,使用 handle 实现延迟轮换 -
ViewFlipper 思路
ViewFlipper 思路和 ViewAnimator 一样,不过 ViewFlipper 使用上更灵活,这里我们根据数据流量动态往 ViewFlipper 里添加 itemView -
TextSwitcher 思路
这个不想写了,没啥意思,TextSwitcher 不熟的大家自己去看看好了,这里我贴一下参考实现得了:
TextSwitcher 不熟悉的看这个:
- 自定义 textView 思路
其实这个思路也好理解,我们继承 textView ,然后在 onDraw 绘制中自己话文字,自己做动画,动画的思路是先把上一个文字上移到顶,然后再绘制下一个文字,从下面开始一直移动到中间
ViewAnimator 思路
ViewAnimator 是个 viewGroup ,可以实现动画切换其中子 view 的效果。在 xml 布局种,我们把 ViewAnimator 当一个容器,里面写轮播的 view,写多少个 view 就有多少个轮播,然后设置切换的动画,用 handle 做定时延迟轮播,调 ViewAnimator.onNext 就可以切换到下一个 view
推荐参考实现:
实现思路:
- 先在 layout xml 中声明布局层级结构:
<ViewAnimator
android:layout_width="match_parent"
android:layout_height="200dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="欢迎"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="测试"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="本程序"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="!!!!!"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="hello,world"/>
</ViewAnimator>
- 代码种设置切换动画
viewAnimator.setOutAnimation(this, R.anim.slide_out_up);
viewAnimator.setInAnimation(this, R.anim.slide_in_down);
- handle 延迟循环显示下一个
public void showNext() {
viewAnimator.showNext();
}
public void showPrevious() {
viewAnimator.showPrevious();
}
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (autoPlayFlag) {
showNext();
}
handler.sendMessageDelayed(new Message(), TIME_INTERVAL);
}
};
我们在需要的位置发送 handle 事件就可以了
使用 ViewAnimator 有点和确定同样明显
- 优点:使用简单,没有难度
- 缺点:xml 中固定了 view个数,为了兼容不同数据量,我们需要再 ViewAnimator 基础上 2 次开发
这里有个例子就是继承 ViewAnimator 做了一个带切换动画的 button,思路不错
自定义 viewGroup 思路
在这个思路下,我们自定义一个容器,继承 FrameLayout ,根据数据数量自己 new 相应数量的 itemView 出来加入 FrameLayout ,动画是通过对当前 itemView 做一个出去的佛纳甘话,同时对下一个 itemView 做一个进入动画,使用 handle 实现延迟轮换
推荐参考实现:
思路如下
- 在设置数据时添加相应数量的 itemView 进去
public void setNoticeList(List<String> list) {
// 创建TextView
for (int i = 0; i < list.size(); i++) {
TextView textView = createTextView(list.get(i));
mNoticeList.add(textView);
addView(textView);
}
// 显示第一条公告
mCurrentNotice = 0;
mNoticeList.get(mCurrentNotice).setVisibility(VISIBLE);
// 启动轮播
start();
}
private TextView createTextView(String text) {
if (mLayoutParams == null) {
mLayoutParams = new LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
mLayoutParams.gravity = Gravity.CENTER_VERTICAL;
}
TextView textView = new TextView(getContext());
textView.setLayoutParams(mLayoutParams);
textView.setSingleLine();
textView.setEllipsize(TextUtils.TruncateAt.END);
textView.setTextColor(mTextColor);
textView.setVisibility(GONE);
textView.setText(text);
// 如果有设置字体大小,如果字体大小为null。
if (mTextSize > 0) {
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
}
return textView;
}
- 在 handle 里面启动 itemView 切换的动画
class NoticeRunnable implements Runnable {
@Override
public void run() {
// 隐藏当前的textView
TextView currentView = mNoticeList.get(mCurrentNotice);
currentView.setVisibility(GONE);
if(mExitAnimSet != null) {
currentView.startAnimation(mExitAnimSet);
}
mCurrentNotice++;
if(mCurrentNotice >= mNoticeList.size()) {
mCurrentNotice = 0;
}
// 显示下一个TextView
TextView nextView = mNoticeList.get(mCurrentNotice);
nextView.setVisibility(VISIBLE);
if(mEnterAnimSet != null) {
nextView.startAnimation(mEnterAnimSet);
}
mHandler.postDelayed(this, mNoticeDuration);
}
}
private void createEnterAnimation() {
mEnterAnimSet = new AnimationSet(false);
TranslateAnimation translateAnimation =
new TranslateAnimation(0,0,0,0, TranslateAnimation.RELATIVE_TO_PARENT, 1f,
TranslateAnimation.RELATIVE_TO_SELF, 0f);
AlphaAnimation alphaAnimation = new AlphaAnimation(0f,1f);
mEnterAnimSet.addAnimation(translateAnimation);
mEnterAnimSet.addAnimation(alphaAnimation);
mEnterAnimSet.setDuration(DEFAULT_ANIMATION_DURATION);
}
private void createExitAnimation() {
mExitAnimSet = new AnimationSet(false);
TranslateAnimation translateAnimation =
new TranslateAnimation(0,0,0,0, TranslateAnimation.RELATIVE_TO_SELF, 0f,
TranslateAnimation.RELATIVE_TO_PARENT, -1f);
AlphaAnimation alphaAnimation = new AlphaAnimation(1f,0f);
mExitAnimSet.addAnimation(translateAnimation);
mExitAnimSet.addAnimation(alphaAnimation);
mExitAnimSet.setDuration(DEFAULT_ANIMATION_DURATION);
}
这样写最练手,但是我是不推荐这样干的,基础差一些的容易出问题,而且 google 给我们提供了一些实现,我们何必非的自己实现呢,反正这样写会花点时间
ViewFlipper 思路
ViewFlipper 思路像是上面 1 和 2 的结合,ViewFlipper 对动画的控制更优秀一些,我们往 ViewFlipper 里面动态添加 itemView ,基本都是这个思路,区别是使用的容器不同
这里推荐一个成熟的库:
这个库非常完善了,也能满足大家的常用需求,是可以拿来直接用的,大家看图就明白了
![](https://img.haomeiwen.com/i4095412/39af64467c38346b.gif)
思路如下
他这里自定义了一个 ViewGroup 继承自 RelativeLayout,在 view 初始化时添加了一个 ViewFlipper 进来,之后操作的都是这个 ViewFlipper 了
- 自定义 ViewGroup 初始化时添加了 ViewFlipper
/**初始化控件*/
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
mViewFlipper = new ViewFlipper(getContext());//new 一个ViewAnimator
mViewFlipper.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
addView(mViewFlipper);
startViewAnimator();
//设置点击事件
mViewFlipper.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int position = mViewFlipper.getDisplayedChild();//当前显示的子视图的索引位置
if (mListener!=null){
mListener.onItemClick(mDatas.get(position),position);
}
}
});
- 根据数据添加 itemView
/**设置数据集合*/
public void setDatas(List<String> datas){
this.mDatas = datas;
if (DisplayUtils.notEmpty(mDatas)){
mViewFlipper.removeAllViews();
for (int i = 0; i < mDatas.size(); i++) {
TextView textView = new TextView(getContext());
textView.setText(mDatas.get(i));
//任意设置你的文字样式,在这里
textView.setSingleLine(isSingleLine);
textView.setTextColor(mTextColor);
textView.setTextSize(mTextSize);
textView.setGravity(mGravity);
mViewFlipper.addView(textView,i);//添加子view,并标识子view位置
}
}
}
- 添加动画
/**
* 设置进入动画和离开动画
*
* @param inAnimResId 进入动画的resID
* @param outAnimResID 离开动画的resID
*/
private void setInAndOutAnimation(@AnimRes int inAnimResId, @AnimRes int outAnimResID) {
Animation inAnim = AnimationUtils.loadAnimation(getContext(), inAnimResId);
inAnim.setDuration(animDuration);
mViewFlipper.setInAnimation(inAnim);
Animation outAnim = AnimationUtils.loadAnimation(getContext(), outAnimResID);
outAnim.setDuration(animDuration);
mViewFlipper.setOutAnimation(outAnim);
}
之后就是用 handle 来做延迟循环,上面复制好几遍了,这里是在不想再复制了,打个源码很简单,大家直接看。
吐槽下:这个库多了一道手,多加了一个视图层级出来,其实没必要在顶层加一个 viewGroup 了,直接继承 ViewFlipper 可好
自定义 textView 思路
不继承 textView 我们直接继承 view 都可以,只要不支持 wrap_content 就好办。 核心就是在 onDraw 中实现绘制的动画。
例子这里没有使用 ValueAnimator 动画,而是 1 个 px 变化就重绘一次,性能上欠考虑。
参考资料地址:
实现思路
- 根据文字,确定文字出屏幕的零界点
// 获取文字矩阵的尺寸
Rect indexBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), indexBound);
// 文字居中绘制 Y 的坐标
my = mHeight / 2 - (bound.top + bound.bottom) / 2
// 文字移动到最顶部
mY == 0 - bound.bottom
// 文字移动到最下部
mY = mHeight - indexBound.top;
- 在 onDraw 中实现绘制
// 文字首先绘制在最底部,mY 初始时 = 0
if (mY == 0) {
mY = getMeasuredHeight() - indexBound.top;
}
// 文字移动到最顶部时,更换数据,把文字移动到最底部
if (mY == 0 - indexBound.bottom) {
Log.i(TAG, "onDraw: " + getMeasuredHeight());
mY = getMeasuredHeight() - indexBound.top;//返回底部
mIndex++;//换下一组数据
}
// 文字移动到中间时,停止 handle 的重绘任务,延迟标准时间后再开始
if (mY == getMeasuredHeight() / 2 - (indexBound.top + indexBound.bottom) / 2) {
isMove = false;//停止移动
// handle 通知标准时间后开始重绘
}
// 处理完 Y 坐标,开始绘制文字
canvas.drawText(font, 0, font.length(), 10, mY, mPaintFront);
// 最后移动 1 个 px,以实现动画效果
mY -= 1
作者这里强调每移动 1个 px 就重绘一遍是为了确保动画的连贯性,但是这样系统也是 16 ms 才绘制一帧的,这样高频率的重绘并不是最优选择哦
详细的去看作何的文章吧,我更喜欢用 ValueAnimator 实现,这样更贴合 UI 线程的刷新频率。
说下感想,纯自己 draw 绘制的话效率太低,代码容易出错不说,若是需求换个方向对于我们也是一个大麻烦
网友评论