最近需要实现一个需求,在列表中,每个item支持显示仿淘宝的上下消息滚动的功能,最开始的考虑是直接使用android自带的ViewFlipper,添加自定的动画来完成,在测试的时候,发现如果每个item的消息太长的时候,列表滑动起来会非常卡,经过分析应该是每个item的消息列表加入到ViewFlipper的时候,都会创建一个新的View,并加入到布局中,从而在滑动的时候,该操作执行太频繁,导致卡顿。因此考虑自己实现一个ViewFlipper,最多通过两个View之间轮换即可,同时对View进行复用,不需要创建那么多的View。
效果图
github:https://github.com/newhope1106/ViewFlipper
一、分析问题关键点
1.每个item的消息上下滚动的效果?其本质是两个控件通过动画上下滚动,来实现轮播,最少需要两个控件,考虑到性能问题,可以对控件进行复用。
2.消息控件的布局未知,动画持续时间和消息滚动间隔未知?可以通过适配器提供消息控件,并且在适配器中用户自己设置消息值,而自实现的ViewFlipper只需要考虑自己的职责,也就是消息滚动效果即可。
二、实现
1.控件实现代码
public class TextViewFlipper extends FrameLayout { /** * 只保留2个控件,进行动画轮播 * */ private View mView1; private View mView2; private int mIndex = 0; private boolean mStarted; private boolean mRunning; private ValueAnimator mAnimator; private BaseFlipperAdapter mAdapter; private int mFlipInterval = BaseFlipperAdapter.DEFAULT_FLIP_INTERVAL; private int mAnimDuration = BaseFlipperAdapter.DEFAULT_DURATION; private final Runnable mFlipRunnable = new Runnable() { @Override public void run() { if (mRunning) { showNext(); postDelayed(mFlipRunnable, mFlipInterval); } } }; public TextViewFlipper(Context context) { this(context, null); } public TextViewFlipper(Context context, AttributeSet attrs) { super(context, attrs); } /** * 设置适配器,并且开始动画 * */ public void setFlipperAdapter(BaseFlipperAdapter adapter){ mAdapter = adapter; if(adapter != null){ initData(); } else { stopFlipping(); } } /** * 获取适配器 * */ public BaseFlipperAdapter getAdapter() { return mAdapter; } private void initData(){ mFlipInterval = mAdapter.getFlipInterval(); mAnimDuration = mAdapter.getAnimDuration(); int count = mAdapter.getCount(); if(count >= 1){ mView1 = mAdapter.getView(mView1, 0); } if(count > 1){ mView2 = mAdapter.getView(mView2, 1); } reset(); if(count >= 1 && mView1 != null){ mView1.setVisibility(VISIBLE); } } /** * 重置所有状态 * */ private void reset(){ mStarted = false; mRunning = false; mIndex = 0; removeCallbacks(mFlipRunnable); if(mAnimator != null){ mAnimator.cancel(); } removeAllViews(); if(mView1 != null){ mView1.setVisibility(GONE); mView1.setTranslationY(0); mView1.setAlpha(1.0f); addView(mView1); } if(mView2 != null){ mView2.setVisibility(GONE); mView2.setTranslationY(0); mView2.setAlpha(1.0f); addView(mView2); } } /** * 开始轮播 * */ public void startFlipping() { if(mAdapter==null || mAdapter.getCount() == 0){ return; } if(mAdapter.getCount() == 1 && !mAdapter.startWhenOnlyOne()){ return; } mStarted = true; updateRunning(); } /** * 结束轮播 * */ public void stopFlipping() { mStarted = false; updateRunning(); } private void updateRunning() { boolean running = mStarted; if (running != mRunning) { if (running) { showOnly(); postDelayed(mFlipRunnable, mFlipInterval); } else { removeCallbacks(mFlipRunnable); if(mAnimator != null){ mAnimator.cancel(); } } mRunning = running; } } /** * 开始动画 * */ private void showOnly() { if(mAnimator == null){ mAnimator = ValueAnimator.ofFloat(0f, 1.0f); mAnimator.setDuration(mAnimDuration); mAnimator.setRepeatCount(0); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float value = (float)valueAnimator.getAnimatedValue(); int count = getChildCount(); int childIndex = mIndex % 2; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (i == childIndex) { child.setAlpha(value); child.setTranslationY(getHeight()*(1-value)); child.setVisibility(View.VISIBLE); } else { if(child.getVisibility() == VISIBLE){ child.setAlpha(1 - value); child.setTranslationY(- getHeight()*value); } } } } }); } mAnimator.start(); } public void showNext() { setDisplayedChild(mIndex + 1); } /** * 设置特定View的值 * */ private void setDisplayedChild(int whichChild) { mIndex = whichChild; if (whichChild >= mAdapter.getCount()) { mIndex = 0; } else if (whichChild < 0) { mIndex = mAdapter.getCount() - 1; } if(mIndex % 2 == 0){ View tempView = mAdapter.getView(mView1, mIndex); //如果属于重新创建的控件,则需要重新添加 if(tempView.getParent() != this){ removeView(mView1); addView(tempView, 0); } mView1 = tempView; } else { View tempView = mAdapter.getView(mView2, mIndex); //如果属于重新创建的控件,则需要重新添加 if(tempView.getParent() != this){ removeView(mView2); addView(tempView, 1); } mView2 = tempView; } boolean hasFocus = getFocusedChild() != null; // This will clear old focus if we had it showOnly(); if (hasFocus) { // Try to retake focus if we had it requestFocus(FOCUS_FORWARD); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } }
以上通过两个View来实现对View进行复用,而无需重新创建,也无需创建多个View
2.适配器实现代码
public abstract class BaseFlipperAdapter { /** * 两次item切换的间隔 * */ public static final int DEFAULT_FLIP_INTERVAL = 3000; /** * 获取动画持续时间 * */ public static final int DEFAULT_DURATION = 500; /** * 获取数据个数 * @return 返回数据个数 * */ public abstract int getCount(); /** * 获取当前要显示的控件 * */ public abstract View getView(View convertView, int position); /** * 获取item切换的间隔 * */ public int getFlipInterval(){ return DEFAULT_FLIP_INTERVAL; } /** * 获取动画持续时间 * */ public int getAnimDuration(){ return DEFAULT_DURATION; } /** * 只有一个item的时候,是否开启动画,默认false * */ public boolean startWhenOnlyOne(){ return false; } }
以上适配器提供需要的接口数据
3.使用
public class DemoActivity extends Activity{ /**模拟数据*/ private List<List<String>> mockDatas = new ArrayList<>(); @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); initMockData(); setContentView(R.layout.activity_demo); ListView listView = findViewById(R.id.list_view); listView.setAdapter(new ListAdapter()); } /** * 生成模拟数据 * */ private void initMockData(){ for(int i=0; i<100; i++){ List<String> data = new ArrayList<>(); data.add(i + "家人给2岁孩子喝这个,孩子智力倒退10岁!!!"); data.add(i + "iPhone8最感人变化成真,必须买买买买!!!!"); data.add(i + "简直是白菜价!日本玩家33万甩卖15万张游戏王卡"); mockDatas.add(data); } } /** * 实现适配器 * */ private class FlipperAdapter extends BaseFlipperAdapter{ private List<String> mData; public void setData(List<String> data){ mData = data; } @Override public int getCount() { return mData == null ? 0 : mData.size(); } @Override public View getView(View convertView, int position) { if(convertView == null){ convertView = View.inflate(DemoActivity.this, R.layout.layout_flipper_item, null); } TextView textView = (TextView) convertView; textView.setText(mData.get(position)); return convertView; } } private class ListAdapter extends BaseAdapter { @Override public int getCount() { return mockDatas.size(); } @Override public Object getItem(int i) { return mockDatas.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int position, View convertView, ViewGroup viewGroup) { if(convertView == null){ convertView = getLayoutInflater().inflate(R.layout.list_view_item, null); } TextViewFlipper textViewFlipper = (TextViewFlipper)convertView.findViewById(R.id.text_view_flipper); FlipperAdapter adapter = (FlipperAdapter)textViewFlipper.getAdapter(); if(adapter == null){ adapter = new FlipperAdapter(); } adapter.setData(mockDatas.get(position)); textViewFlipper.setFlipperAdapter(adapter); textViewFlipper.startFlipping(); return convertView; } } }
以上demo是在listview中实现上下滚动功能,如果只是单个控件实现上下滚动功能也是支持的。
三、改进点
1. 以上没有把动画部分抽取出来,给用户自定义,依赖了特定的业务,并不完善。
2. 使用方面步骤有点多,成本略高,可以再加一层封装,来实现一些领域的业务,对外提供简单的接口