ViewFlipper实现滚动布局

本文介绍了如何通过ViewFlipper实现类似淘宝头条的滚动效果,强调了查看源码对于理解机制的重要性。详细讲解了自定义ViewFlipper的实现过程,包括XML布局、代码实现,并分析了ViewFlipper类中的关键方法,如切换间隔设置、启动停止控制,以及内部使用的Handler和消息队列机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天在git上面看到了一个 仿淘宝头条的滚动效果,就看了下源码                

大家要养成看源码的好习惯,才能更好的理解

       然后自己照着写了下  然后顺便看了下 ViewFlipper 这个类

先上效果图吧:                               最后分析的有 系统的这个类中的方法 的执行 (大神略过,菜鸟请贴完代码后往下看.....)



自定义类继承 ViewFlipper

package com.dreamlive.upmarqueeview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ViewFlipper;

import java.util.List;

/**
 * 仿淘宝首页的 淘宝头条滚动的自定义View
 *
 * Created by xi on 2016/11/28.
 */
public class UPMarqueeView extends ViewFlipper {

    private Context mContext;
    private boolean isSetAnimDuration = false;

    /**
     * 要多久 烙下一个视图  不搞的话 默认是3000
     *  意思就是  切换的间隔
     */
    private int interval = 2000;
    /**
     * 动画时间
     *  因为设置的是 进入 和 出去 的动画为  透明 和 位移
     */
    private int animDuration = 500;

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

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        this.mContext = context;
        // 设置区间间隔
        setFlipInterval(interval);
        Animation animIn = AnimationUtils.loadAnimation(mContext, R.anim.anim_marquee_in);
        if (isSetAnimDuration) animIn.setDuration(animDuration);
        setInAnimation(animIn);
        Animation animOut = AnimationUtils.loadAnimation(mContext, R.anim.anim_marquee_out);
        if (isSetAnimDuration) animOut.setDuration(animDuration);
        setOutAnimation(animOut);
    }


    /**
     * 设置循环滚动的View数组
     *
     * @param views
     */
    public void setViews(final List<View> views) {
        if (views == null || views.size() == 0) return;
        removeAllViews();
        for ( int i = 0; i < views.size(); i++) {
            final int position=i;
            //设置监听回调
            views.get(i).setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onItemClickListener != null) {
                        onItemClickListener.onItemClick(position, views.get(position));
                    }
                }
            });
            addView(views.get(i));
        }
        startFlipping();
    }

    /**
     * 设置动画时长  默认为500ms
     * @param ms
     */
    public void setAnimDuration(int ms){
        this.animDuration = ms;
    }

    /**
     *  设置间隔时长区间  这里默认给的2s  系统源码里面默认给的3s
     * @param interval
     */
    public void setInterval(int interval) {
        this.interval = interval;
    }

    /**
     * 点击
     */
    private OnItemClickListener onItemClickListener;

    /**
     * 设置监听接口
     * @param onItemClickListener
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    /**
     * item_view的接口
     */
    public interface OnItemClickListener {
        void onItemClick(int position, View view);
    }
}

使用:

1.xml 布局

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

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="80dp">
        <TextView
            android:id="@+id/tbtv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_centerVertical="true"
            android:textSize="22sp"
            android:textColor="@color/red"
            android:text="@string/taobao" />

        <com.dreamlive.upmarqueeview.UPMarqueeView
            android:id="@+id/upview1"
            android:layout_marginLeft="20dp"
            android:layout_width="match_parent"
            android:layout_toRightOf="@+id/tbtv"
            android:layout_centerVertical="true"
            android:layout_marginTop="10dp"
            android:layout_height="match_parent"></com.dreamlive.upmarqueeview.UPMarqueeView>
    </RelativeLayout>
</LinearLayout>

item_view:

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

    <RelativeLayout
        android:id="@+id/rl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/title_tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="热议"
            android:textSize="9sp"
            android:padding="3dp"
            android:background="@drawable/textview_border"
            android:layout_marginRight="6dp"
            android:textColor="@color/red" />

        <TextView
            android:id="@+id/tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/title_tv1"
            android:ellipsize="end"
            android:textSize="14sp"
            android:maxLines="1" />
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rl2"
        android:layout_width="match_parent"
        android:layout_marginTop="5dp"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/title_tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="热评"
            android:padding="3dp"
            android:textSize="9sp"
            android:background="@drawable/textview_border"
            android:layout_marginRight="6dp"
            android:textColor="@color/red" />

        <TextView
            android:id="@+id/tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/title_tv2"
            android:ellipsize="end"
            android:textSize="14sp"
            android:maxLines="1" />
    </RelativeLayout>

</LinearLayout>


2.代码

package com.dreamlive.upmarqueeviewdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.dreamlive.upmarqueeview.UPMarqueeView;

import java.util.ArrayList;
import java.util.List;

/**
 * 仿淘宝首页的 淘宝头条滚动的自定义View
 * Created by xi on 2016/11/28.
 */
public class MainActivity extends AppCompatActivity {


    private UPMarqueeView upview1;
    List<String> data = new ArrayList<>();
    List<View> views = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initParam();
        initdata();
        initView();
    }

    /**
     * 实例化控件
     */
    private void initParam() {
        upview1 = (UPMarqueeView) findViewById(R.id.upview1);
    }

    /**
     * 初始化界面程序
     */
    private void initView() {
        setView();
        upview1.setViews(views);
        /**
         * 设置item_view的监听
         */
        upview1.setOnItemClickListener(new UPMarqueeView.OnItemClickListener() {
            @Override
            public void onItemClick(int position, View view) {
                Toast.makeText(MainActivity.this, "你点击了第几个items" + position, Toast.LENGTH_SHORT).show();
            }
        });
    }

    /**
     * 初始化需要循环的View
     * 为了灵活的使用滚动的View,所以把滚动的内容让用户自定义
     * 假如滚动的是三条或者一条,或者是其他,只需要把对应的布局,和这个方法稍微改改就可以了,
     */
    private void setView() {
        for (int i = 0; i < data.size(); i = i + 2) {
            final int position = i;
            //设置滚动的单个布局
            LinearLayout moreView = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.item_view, null);
            //初始化布局的控件
            TextView tv1 = (TextView) moreView.findViewById(R.id.tv1);
            TextView tv2 = (TextView) moreView.findViewById(R.id.tv2);

            /**
             * 设置监听
             */
            moreView.findViewById(R.id.rl).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(MainActivity.this, position + "你点击了" + data.get(position).toString(), Toast.LENGTH_SHORT).show();
                }
            });
            /**
             * 设置监听
             */
            moreView.findViewById(R.id.rl2).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(MainActivity.this, position + "你点击了" + data.get(position).toString(), Toast.LENGTH_SHORT).show();
                }
            });
            //进行对控件赋值
            tv1.setText(data.get(i).toString());
            if (data.size() > i + 1) {
                //因为淘宝那儿是两条数据,但是当数据是奇数时就不需要赋值第二个,所以加了一个判断,还应该把第二个布局给隐藏掉
                tv2.setText(data.get(i + 1).toString());
            } else {
                moreView.findViewById(R.id.rl2).setVisibility(View.GONE);
            }

            //添加到循环滚动数组里面去
            views.add(moreView);
        }
    }

    /**
     * 初始化数据
     */
    private void initdata() {
        data = new ArrayList<>();
        data.add("家人给2岁孩子喝这个,孩子智力倒退10岁!!!");
        data.add("iPhone8最感人变化成真,必须买买买买!!!!");
        data.add("简直是白菜价!日本玩家33万甩卖15万张游戏王卡");
        data.add("iPhone7价格曝光了!看完感觉我的腰子有点疼...");
        data.add("主人内疚逃命时没带够,回废墟狂挖30小时!");
//        data.add("竟不是小米乐视!看水抢了骁龙821首发了!!!");

    }
}

资源文件:

in ----

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="300"
        android:fromYDelta="100%p"
        android:toYDelta="0"/>
    <alpha
        android:duration="500"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>
</set>

out----

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="400"
        android:fromYDelta="0"
        android:toYDelta="-100%p"/>
    <alpha
        android:duration="500"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>


在系统的 ViewFlipper 类中可以看到这样的代码  也就是一个广播  

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (Intent.ACTION_SCREEN_OFF.equals(action)) {
            mUserPresent = false;
            updateRunning();
        } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
            mUserPresent = true;
            updateRunning(false);
        }
    }
};

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();

    // Listen for broadcasts related to user-presence
    final IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_SCREEN_OFF);
    filter.addAction(Intent.ACTION_USER_PRESENT);

    // OK, this is gross but needed. This class is supported by the
    // remote views machanism and as a part of that the remote views
    // can be inflated by a context for another user without the app
    // having interact users permission - just for loading resources.
    // For exmaple, when adding widgets from a user profile to the
    // home screen. Therefore, we register the receiver as the current
    // user not the one the context is for.
    getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
            filter, null, getHandler());	// 这个方法是 Context的  注册广播的方法

    if (mAutoStart) {
        // Automatically start when requested
        startFlipping();
    }
}
和registerReceiver 一样 但它是对于特殊用户使用的   也就是系统使用的

/**
 * @hide
 * Same as {@link #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
 * but for a specific user.  This receiver will receiver broadcasts that
 * are sent to the requested user.  It
 * requires holding the {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}
 * permission.
 *
 * @param receiver The BroadcastReceiver to handle the broadcast.
 * @param user UserHandle to send the intent to.
 * @param filter Selects the Intent broadcasts to be received.
 * @param broadcastPermission String naming a permissions that a
 *      broadcaster must hold in order to send an Intent to you.  If null,
 *      no permission is required.
 * @param scheduler Handler identifying the thread that will receive
 *      the Intent.  If null, the main thread of the process will be used.
 *
 * @return The first sticky intent found that matches <var>filter</var>,
 *         or null if there are none.
 *
 * @see #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
 * @see #sendBroadcast
 * @see #unregisterReceiver
 */
@Nullable
public abstract Intent registerReceiverAsUser(BroadcastReceiver receiver,
        UserHandle user, IntentFilter filter, @Nullable String broadcastPermission,
        @Nullable Handler scheduler);

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mVisible = false;

    getContext().unregisterReceiver(mReceiver);
    updateRunning();
}

@Override
protected void onWindowVisibilityChanged(int visibility) {
    super.onWindowVisibilityChanged(visibility);
    mVisible = visibility == VISIBLE;
    updateRunning(false);
}

可以去设置  切换间隔  单位为 毫秒

/**
 * How long to wait before flipping to the next view
 *
 * @param milliseconds
 *            time in milliseconds
 */
@android.view.RemotableViewMethod
public void setFlipInterval(int milliseconds) {
    mFlipInterval = milliseconds;
}

可以去启动和停止

/**
 * Start a timer to cycle through child views
 */
public void startFlipping() {
    mStarted = true;
    updateRunning();
}

/**
 * No more flips
 */
public void stopFlipping() {
    mStarted = false;
    updateRunning();
}

内部用来启动和停止的方法

/**
 * Internal method to start or stop dispatching flip {@link Message} based
 * on {@link #mRunning} and {@link #mVisible} state.
 */
private void updateRunning() {
    updateRunning(true);
}

/**
 * Internal method to start or stop dispatching flip {@link Message} based
 * on {@link #mRunning} and {@link #mVisible} state.
 *
 * @param flipNow Determines whether or not to execute the animation now, in
 *            addition to queuing future flips. If omitted, defaults to
 *            true.
 */
private void updateRunning(boolean flipNow) {
    boolean running = mVisible && mStarted && mUserPresent;
    if (running != mRunning) {
        if (running) {
            showOnly(mWhichChild, flipNow);
            postDelayed(mFlipRunnable, mFlipInterval);
        } else {
            removeCallbacks(mFlipRunnable);
        }
        mRunning = running;
    }
    if (LOGD) {
        Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
                + ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
    }
}

可以看到它调用的是  View 的延时方法     而该方法使用的还是  handler  和   队列

/**
 * <p>Causes the Runnable to be added to the message queue, to be run
 * after the specified amount of time elapses.
 * The runnable will be run on the user interface thread.</p>
 *
 * @param action The Runnable that will be executed.
 * @param delayMillis The delay (in milliseconds) until the Runnable
 *        will be executed.
 *
 * @return true if the Runnable was successfully placed in to the
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.  Note that a
 *         result of true does not mean the Runnable will be processed --
 *         if the looper is quit before the delivery time of the message
 *         occurs then the message will be dropped.
 *
 * @see #post
 * @see #removeCallbacks
 */
public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}

运行的队列

/**
 * Returns the queue of runnable for this view.
 *
 * @return the queue of runnables for this view
 */
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

发送的延时

public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;

    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }

而系统    移除回调  是把actions 进行匹配遍历 置为null  

public void removeCallbacks(Runnable action) {
    synchronized (this) {
        final int count = mCount;
        int j = 0;

        final HandlerAction[] actions = mActions;
        for (int i = 0; i < count; i++) {
            if (actions[i].matches(action)) {
                // Remove this action by overwriting it within
                // this loop or nulling it out later.
                continue;
            }

            if (j != i) {
                // At least one previous entry was removed, so
                // this one needs to move to the "new" list.
                actions[j] = actions[i];
            }

            j++;
        }

        // The "new" list only has j entries.
        mCount = j;

        // Null out any remaining entries.
        for (; j < count; j++) {
            actions[j] = null;
        }
    }
}

此外在这里 还看到了 执行的方法是用的是     handler

public void executeActions(Handler handler) {
    synchronized (this) {
        final HandlerAction[] actions = mActions;
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        mActions = null;
        mCount = 0;
    }
}


到此为止了,下班喽~~





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值