教你自定义竖直跑马灯效果(广告专用)

最近因为项目需要,写了一个跑马灯效果的控件,过程中也学到一些东西,在这里和大家分享一下。

首先让我们来看一下效果:


其中的图标,文字,甚至每行的整个布局都是可以自定义的,我们可以使用整个控件很方便做出自己想要的效果。

闲话少说,我们先来看在代码里面怎么使用这个控件,首先是xml文件里面,直接引用即可。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <com.example.kaiyicky.myapplication.VerticalScrollView
        android:id="@+id/mscroll"
        android:scrollbars="none"
        android:layout_width="match_parent"
        android:background="#ffffff"
        android:layout_marginTop="200dp"
        android:layout_height="60dp">
    </com.example.kaiyicky.myapplication.VerticalScrollView>
</LinearLayout>
可以看到我们的控件名字叫VerticalScrollView,注意我们一般要给这个控件一个固定的高度和宽度。

然后在Activity里面这样设置:

public class MainActivity extends FragmentActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取对象
        VerticalScrollView mscroll= (VerticalScrollView) findViewById(R.id.mscroll);
        mscroll.setDirection(-1);//设置方向
        //自定义行布局和数据
        mscroll.setTabAdapter(new TabAdapter(R.layout.vertical_scroll_item,
                new String[]{"哈哈", "呵呵", "哦哦"}){

            @Override
            public void getView(View v,String text,String icon) {
                TextView t = (TextView) v.findViewById(R.id.item_text);
                t.setText(text);
            }
        });
        //开始动画
        mscroll.startAutoScroll();
    }    
}

在获得控件对象以后,我们可以设置跑马灯的方向,-1代表从上到下,1代表从下到上

然后为mscroll设置adapter,这个adapter也是我们自定义的哦,不要以为是Android提供的,这里我借鉴了listview中setAdapter的思想,实现数据和布局的分离

这样我们就可以为自己的行设置想要的布局,同时可以获取布局中的控件设置需要的值,例如为textView设置文字或者icon

最后只要主动调用startAutoScroll()就可以是整个控件跑起来了

使用方式就是这么简单,接下来大家跟着我,看看整个控件是怎么写的。


我先说一下我的思路,我是通过继承ScrollView来写这个控件的,为什么选择它,因为ScrollView有一个smoothSrollTo()方法,可以使内部的布局滑动到一个特定的位置,从而实现图片里面的滑动效果。有了这个想法以后,我们要知道ScrollView里面一般只能有一个子控件,显然我们需要一个LinearLayout,然后根据数据的条数,主动渲染行布局,将生成的View逐个添加入LinearLayout就可以了。

可是还有一个问题,怎么实现无限的自动滚动呢?一开始我思路是创建一个子线程,里面有一个死循环,每隔一定时间,就调用smoothSrollTo()去移动LinearLayout的位置,这样就实现了无限循环。(注意你不能再UI线程里面写死循环啊,所以只能另起一个子线程了)。

后来觉得这样写会造成内存泄露,因为这个线程没有合适的终止条件,也就说我们跳转到另外一个Activity以后,这个线程也不会结束,这样的话内存就无法释放。

解决办法是,通过handler来实现死循环,我们知道调用handler的sendEmptyMessageDelayed()方法,可以延长一段时间以后发送一个信息。

然后在handleMessage里面会接收到这个信息,如果我们在handleMessage的最后,又再一次调用sendEmptyMessageDelayed()方法,这样就可以不断循环了。


OK,说道这里,大家可能觉得有点抽象,等下我们看代码的时候回明白。

上面解决了滑动问题和无限循环的问题,可是还有一个问题,就是我们怎么在循环过程中,例如我们循环展示的条目是{1,2,3,4,5}

怎么才能展示5之后,又回到头去展示1呢?或者说展示1以后,怎么回到尾去展示5呢?(这两个问题只是方向不同而已)

这里有一个简单的算法,我是这样做的。针对第一种情况,如图(图片是水平的,实际方向是竖直的,但原理一样):




我们看到,对于1-5,我创建了一个{1,2,3,4,5,1}的数组,也就是在末尾增加了一个和头部一样的数。

假设滚动方向是从1到5,到5以后,再往下滚就是1了,再滚动1以后,我马上调用ScrollTo()方法,使整个控件回到头部

由于这个方法使瞬时的(等于cpu渲染速度),肉眼没有办法分辨出这次移动

这样我们就又回到头部了,又可以再次向下滚,重复上述过程。


可能还有朋友没有想明白,假设我们不这样做,也就是数组只有{1,2,3,4,5},当我们滚动5的时候,如果马上让5瞬间到1,这样就丢失了5到1这个滚动过程了。

OK,整个控件的思路和重点就介绍到这里,如果没有想清楚没有关系,我们上代码!


首先是属性和构造函数

/**
 * Created by kaiyi.cky on 2015/8/5.
 * 跑马灯控件
 */
public class VerticalScrollView extends ScrollView{
    private LinearLayout mLinearLayout;
    /**
     * 当前显示的序号
     */
    private int curIndex = 1;
    /**
     * 滚动方向
     * 1为向下,-1为向上
     */
    private int direction = 1;
    /**
     * 滚动间隔时间
     */
    private int spentTime = 2000;

    private final static int SCROLL_WHAT = 99;
    /**
     * 自定义Scroller来控制smoothScroll的动画时间
     */
    private CustomDurationScroller scroller = null;
    /**
     * 滚动动画时间比率,该比率乘以250ms是动画时间
     */
    private double mScrollFactor = 1d;
    MyHandler myHandler;
    /**
     * 用于数据与布局
     */
    TabAdapter mTabAdapter;

    public VerticalScrollView(Context context) {
        super(context);
        init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public void init(Context context){
        mLinearLayout = new LinearLayout(context);
        mLinearLayout.setOrientation(LinearLayout.VERTICAL);
        addView(mLinearLayout, new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        myHandler = new MyHandler();
        setViewPagerScroller();
    }
}
在构造函数里面,我为ScrollView添加了一个LinearLyout
然后又一个MyHandler,先看看整个对象是用来做什么的

 /**
     * 用于不断向主线程发送信息,使行状态改变
     * 在handleMessage()里面调用sendMessageDelayed()可以实现自动无限滚动
     */
    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case SCROLL_WHAT:
                    SetItemImeediate();
                    setCurrentItem(curIndex+direction);
                    myHandler.removeMessages(SCROLL_WHAT);
                    myHandler.sendEmptyMessageDelayed(SCROLL_WHAT, spentTime);
                    break;
            }
        }
    }

可以看到,这个handler接受到一个消息以后,做了一系列操作,其中我们先关注这两句

myHandler.removeMessages(SCROLL_WHAT);
myHandler.sendEmptyMessageDelayed(SCROLL_WHAT, spentTime);
我清楚之前接收的信息,然后又向handler自己发送了一条延时信息

可想而知,在spendTime(我们自己定义的延时,也就是每次滚动的间隔时间)时间以后,myHanlder又会接收到这条信息

在这条信息的最后,又会给自己发一条一样的信息

这里我们就实现了自动无限循环滚动。


可是第一条信息是什么时候发生的呢?聪明的你应该知道,是在

/**
     * 开启动画,必须主动调用
     */
    public void startAutoScroll(){
        myHandler.sendEmptyMessage(SCROLL_WHAT);
    }
这个方法里面!也就是说是我们主动调用的。


接着我们看真实的移动方法,注意到MyHanlder里面首先调用了SetItemImeediate()

/**
     * 瞬间设置当前行
     */
    private void SetItemImeediate(){
        if (direction==1&&curIndex == mTabAdapter.getSize()-1){
            //如果向下滚动并且滚动到最后一条,瞬间跳跃到第二条,实现循环效果
            View v = mLinearLayout.getChildAt(1);
            scrollTo((int) v.getX(), (int) v.getY());
            curIndex = 1;
        }else if (direction==-1&&curIndex == 0){
            //如果向上滚动并且滚动到第一条,瞬间跳跃到倒数第二条,实现循环效果
            View v = mLinearLayout.getChildAt(mTabAdapter.getSize()-2);
            scrollTo((int) v.getX(), (int) v.getY());
            curIndex = mTabAdapter.getSize()-2;
        }
    }

这个方法,就是思路里面提到的,瞬间跳动的方法,判断当前滚动到的条目是不是最后一条/第一条(根据滚动方向不同)

从而调用ScrollTo()方法,瞬间回到位置。


然后就是实现滚动的

/**
     * 滚动到index行
     * @param index
     */
    public void setCurrentItem(int index){
        View v = mLinearLayout.getChildAt(index);
        smoothScrollTo((int) v.getX(), (int) v.getY());
        curIndex = index;
    }

可以看到,我们只需要或等当前行对象,获取它的坐标X,Y,然后调用smoothScrollTo就可以滚动到它所在的位置了

smoothScrollTo()很方便地为我们实现了滚动动画,但是它也有缺点,就是不能定制动画时间。如果我们希望滚动快一点,慢一点都没有办法调控。

因为这个方法是ScrollView内部的,默认动画时间是250ms,后面我会讲到一个巧妙的方法去解决这个问题。


OK,滚动的实现就是这么简单,开始数据在哪里呢?行布局在哪里添加的呢?

前面说过,我借鉴了Adapter的思想实现数据分离,这里我先来看一下自定义的Adapter类

/**
 * Created by kaiyi.cky on 2015/8/6.
 * 用于使用者自定义处理VerticalScrollView的数据与布局
 */
public abstract class TabAdapter{
    final int mResId;
    private ArrayList<String> textArray = new ArrayList<String>();
    private ArrayList<String> iconArray = new ArrayList<String>();

    public TabAdapter(int resId, String[] texts){
        mResId = resId;
        setTexts(texts);
    }

    public TabAdapter(int resId,String[] texts, String[] icons){
        mResId = resId;
        setIconTexts(texts, icons);
    }

    /**
     * 返回index对应文字
     * @param index
     * @return
     */
    public String getText(int index){
        return textArray.get(index);
    }

    /**
     * 返回index对应icon地址
     * @param index
     * @return
     */
    public String getIcon(int index){
        return iconArray.isEmpty()?"":iconArray.get(index);
    }

    /**
     * 设置文字,不设置图标
     * @param texts
     * @throws Exception
     */
    private void setTexts(String[] texts){
        setIconTexts(texts, null);
    }

    /**
     * 设置文字和图标
     * 要求texts必须不为空
     * @param texts
     * @param icons
     */
    private void setIconTexts(String[] texts, String[] icons) {
        if(texts==null||texts.length<=0) return;
        if(icons!=null){
            if(icons.length<=0||texts.length!=icons.length) return;
        }

        textArray.clear();
        textArray.add(texts[texts.length - 1]);
        textArray.addAll(Arrays.asList(texts));
        textArray.add(texts[0]);

        iconArray.clear();
        if(icons!=null&&icons.length!=0) {
            iconArray.add(icons[icons.length - 1]);
            iconArray.addAll(Arrays.asList(icons));
            iconArray.add(icons[0]);
        }
    }

    /**
     * 返回数据条目总数
     * @return
     */
    public int getSize(){
        return textArray.size();
    }

    /**
     * 必须继承该方法,在该方法内可以自定义布局文字,图标等
     * @param v
     * @param text
     * @param icon
     */
    public abstract void getView(View v,String text,String icon);
}

这个类是一个抽象类,其中getView()是一个抽象方法,我们必须继承。

从构造函数我们可以看出,必须为其提供一个布局文件和一个数组作为文字数据

然后我对数据做了一些处理,其实就是添加了头尾,目的是实现循环

例如我们传入的数组是{1,2,3,4,5},在构造函数里面,我就要转换成{1,2,3,4,5,1}存储起来啊,这和前面说到的原理是一致的。

当然icon也就是图片地址,我们不一定要传,函数如下:

/**
     * 设置文字和图标
     * 要求texts必须不为空
     * @param texts
     * @param icons
     */
    private void setIconTexts(String[] texts, String[] icons) {
        if(texts==null||texts.length<=0) return;
        if(icons!=null){
            if(icons.length<=0||texts.length!=icons.length) return;
        }

        textArray.clear();
        textArray.add(texts[texts.length - 1]);//将最后一个元素加到头
        textArray.addAll(Arrays.asList(texts));
        textArray.add(texts[0]);//将第一个元素加到尾

        iconArray.clear();
        if(icons!=null&&icons.length!=0) {
            iconArray.add(icons[icons.length - 1]);
            iconArray.addAll(Arrays.asList(icons));
            iconArray.add(icons[0]);
        }
    }

OK,我们在回过头来看一下怎么使用这个Adapter,在Activity里面是这样使用的

//自定义行布局和数据
        mscroll.setTabAdapter(new TabAdapter(R.layout.vertical_scroll_item,
                new String[]{"哈哈", "呵呵", "哦哦"}){

            @Override
            public void getView(View v,String text,String icon) {
                TextView t = (TextView) v.findViewById(R.id.item_text);
                t.setText(text);
            }
        });
调用setAdapter()方法传入一个adapter就可以,记得要传行布局地址和数据数组哦!

然后我们在getView()方法里面,就可以获得由行布局渲染的View对象了,有了这个View对象,使用者就可以根据需要设置文字啊,或者别的信息


关键是setAdapter()方法做了什么,怎么getView()方法可以获得这个View对象呢?

看代码:

/**
     * 该函数必须在startAutoScroll()之前调用
     * @param mTabAdapter
     */
    public void setTabAdapter(TabAdapter mTabAdapter) {
        this.mTabAdapter = mTabAdapter;
        notifyDataSetChanged();
    }
接着看:

/**
     * 逐个添加布局
     */
    private void notifyDataSetChanged(){
        mLinearLayout.removeAllViews();
        for(int i=0;i<mTabAdapter.getSize();i++){
            if(mTabAdapter!=null){
                addTab(i);
            }
        }
        requestLayout();
    }
遍历数据,为每个数据创建布局,关键是addTab()方法

/**
     * 添加布局
     * @param index
     */
    private void addTab(int index){
        View v = LayoutInflater.from(getContext()).inflate(mTabAdapter.mResId,mLinearLayout,false);
        mTabAdapter.getView(v,mTabAdapter.getText(index),mTabAdapter.getIcon(index));
        mLinearLayout.addView(v);
    }
OK,看了吗?我调用LayoutInflater.from()渲染出行布局,然后调用了mTabApdater.getView()方法,将View传了进入!

就是这样我们调用了getView(),所以getView()就会被执行,我们自定义的东西就会执行!

最后将view添加到LinearLayout就可以了。


到此为止,整个控件,从自动无限滚动,循环滚动,和根据数据添加布局,这几个方面都说完了。

最后,让我们一起来关注怎么控制动画速度的问题。

我们注意到,在VerticalScrollView构造函数里面,调用了setViewPagerScroller()方法,这个方法使做什么的呢?

看看:

/**
     * 使用反射设置Scroller
     */
    private void setViewPagerScroller() {
        try {
            Field scrollerField = ScrollView.class.getDeclaredField("mScroller");
            scrollerField.setAccessible(true);
            scroller = new CustomDurationScroller(getContext());
            scroller.setScrollDurationFactor(mScrollFactor);
            scrollerField.set(this, scroller);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

通过反射设置Scroller,如果看过我以前的文章 http://blog.csdn.net/crazy__chen/article/details/45896961,一定指定Scroller是用来控制滑动的一个辅助类

而实际上ScrollView内部,也是通过这个类来提供滑动信息的,我们看一段ScrollView的源码:

public class ScrollView extends FrameLayout {
    static final int ANIMATED_SCROLL_GAP = 250;

    static final float MAX_SCROLL_FACTOR = 0.5f;

    private static final String TAG = "ScrollView";

    private long mLastScroll;

    private final Rect mTempRect = new Rect();
    private OverScroller mScroller;

我们看到在ScrollView内部,有一个私有的OverScroller类型的mScroller对象,就是这个对象来控制动画速度的。

问题是这个对象是私有的啊,我们没有办法获得,除了使用反射!

我们用反射获得这个属性以后,为这个属性重新设置一个值,这个值就是我们自己定义的CustomDurationScroller啦

我看看:

/**
     * 自定义Scroller来控制smoothScroll的动画时间
     */
    private class CustomDurationScroller extends OverScroller{
        private double scrollFactor = 1;
        public CustomDurationScroller(Context context) {
            super(context);
        }

        /**
         * 设置动画时长比,默认动画时间是250ms
         */
        public void setScrollDurationFactor(double scrollFactor) {
            this.scrollFactor = scrollFactor;
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, (int)(duration * scrollFactor));
        }
    }
这个CustomDurationScroller同样继承了OverScroller,只是添加了一个scrollFactor属性,这样我们就可以在startScroll()方法里面更改动画时间了

由于它拥有OverScroller的所有功能,所以不会对原理的ScrollView造成影响。

通过反射,我们就巧妙地解决了这个问题。

最后VerticalScrollView只要提供一个set方法来让使用者可以设置scrollFactor就可以了

public void setScrollFactor(double scrollFactor){
        mScrollFactor = scrollFactor>0?scrollFactor:1;
    }

OK,到此为止,所有的问题都迎刃而解啦,大家可以随意使用这个控件,定制自己需要的效果哦!

源码下载地址:http://download.csdn.net/detail/kangaroo835127729/8980817
转载请注明出处!http://blog.csdn.net/crazy__chen/article/details/47375419

最后贴上源码:

/**
 * Created by kaiyi.cky on 2015/8/5.
 * 跑马灯控件
 */
public class VerticalScrollView extends ScrollView{
    private LinearLayout mLinearLayout;
    /**
     * 当前显示的序号
     */
    private int curIndex = 1;
    /**
     * 滚动方向
     * 1为向下,-1为向上
     */
    private int direction = 1;
    /**
     * 滚动间隔时间
     */
    private int spentTime = 2000;

    private final static int SCROLL_WHAT = 99;
    /**
     * 自定义Scroller来控制smoothScroll的动画时间
     */
    private CustomDurationScroller scroller = null;
    /**
     * 滚动动画时间比率,该比率乘以250ms是动画时间
     */
    private double mScrollFactor = 1d;
    MyHandler myHandler;
    /**
     * 用于数据与布局
     */
    TabAdapter mTabAdapter;

    public VerticalScrollView(Context context) {
        super(context);
        init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public VerticalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public void init(Context context){
        mLinearLayout = new LinearLayout(context);
        mLinearLayout.setOrientation(LinearLayout.VERTICAL);
        addView(mLinearLayout, new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        myHandler = new MyHandler();
        setViewPagerScroller();
    }

    /**
     * 使用反射设置Scroller
     */
    private void setViewPagerScroller() {
        try {
            Field scrollerField = ScrollView.class.getDeclaredField("mScroller");
            scrollerField.setAccessible(true);
            scroller = new CustomDurationScroller(getContext());
            scroller.setScrollDurationFactor(mScrollFactor);
            scrollerField.set(this, scroller);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 自定义Scroller来控制smoothScroll的动画时间
     */
    private class CustomDurationScroller extends OverScroller{
        private double scrollFactor = 1;
        public CustomDurationScroller(Context context) {
            super(context);
        }

        /**
         * 设置动画时长比,默认动画时间是250ms
         */
        public void setScrollDurationFactor(double scrollFactor) {
            this.scrollFactor = scrollFactor;
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, (int)(duration * scrollFactor));
        }
    }

    /**
     * 用于不断向主线程发送信息,使行状态改变
     * 在handleMessage()里面调用sendMessageDelayed()可以实现自动无限滚动
     */
    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case SCROLL_WHAT:
                    SetItemImeediate();
                    setCurrentItem(curIndex+direction);
                    myHandler.removeMessages(SCROLL_WHAT);
                    myHandler.sendEmptyMessageDelayed(SCROLL_WHAT, spentTime);
                    break;
            }
        }
    }

    /**
     * 瞬间设置当前行
     */
    private void SetItemImeediate(){
        if (direction==1&&curIndex == mTabAdapter.getSize()-1){
            //如果向下滚动并且滚动到最后一条,瞬间跳跃到第二条,实现循环效果
            View v = mLinearLayout.getChildAt(1);
            scrollTo((int) v.getX(), (int) v.getY());
            curIndex = 1;
        }else if (direction==-1&&curIndex == 0){
            //如果向上滚动并且滚动到第一条,瞬间跳跃到倒数第二条,实现循环效果
            View v = mLinearLayout.getChildAt(mTabAdapter.getSize()-2);
            scrollTo((int) v.getX(), (int) v.getY());
            curIndex = mTabAdapter.getSize()-2;
        }
    }

    /**
     * 滚动到index行
     * @param index
     */
    public void setCurrentItem(int index){
        View v = mLinearLayout.getChildAt(index);
        smoothScrollTo((int) v.getX(), (int) v.getY());
        curIndex = index;
    }

    /**
     * 逐个添加布局
     */
    private void notifyDataSetChanged(){
        mLinearLayout.removeAllViews();
        for(int i=0;i<mTabAdapter.getSize();i++){
            if(mTabAdapter!=null){
                addTab(i);
            }
        }
        requestLayout();
    }

    /**
     * 开启动画,必须主动调用
     */
    public void startAutoScroll(){
        myHandler.sendEmptyMessage(SCROLL_WHAT);
    }

    /**
     * 添加布局
     * @param index
     */
    private void addTab(int index){
        View v = LayoutInflater.from(getContext()).inflate(mTabAdapter.mResId,mLinearLayout,false);
        mTabAdapter.getView(v,mTabAdapter.getText(index),mTabAdapter.getIcon(index));
        mLinearLayout.addView(v);
    }

    /**
     * 该函数必须在startAutoScroll()之前调用
     * @param mTabAdapter
     */
    public void setTabAdapter(TabAdapter mTabAdapter) {
        this.mTabAdapter = mTabAdapter;
        notifyDataSetChanged();
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return true;//返回true,防止手动滑动ScrollView
    }

    public int getSpentTime() {
        return spentTime;
    }

    public void setSpentTime(int spentTime) {
        this.spentTime = spentTime;
    }

    public int getDirection() {
        return direction;
    }

    public void setDirection(int direction) {
        this.direction = direction>0?1:-1;
    }

    public void setScrollFactor(double scrollFactor){
        mScrollFactor = scrollFactor>0?scrollFactor:1;
    }
}




  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值