项目地址:https://github.com/ksoichiro/Android-ObservableScrollView
项目依赖:com.github.ksoichiro:android-observablescrollview:1.6.0(版本号跟随官方版本变化)
效果图(下图来自上面github)
之前已经把这篇写好了,一不小心在编辑其他blog是把本篇搞掉了,于是乎苦逼的重写开始了,啥也不说了直接贴源码,带中文注释。
Scrollable
/**
* 该接口为滑动控件提供相应的api
*/
public interface Scrollable {
/**
* 设置滑动的回调监听这个方法啊已经过时了,已经有了更好的方法
*/
@Deprecated
void setScrollViewCallbacks(ObservableScrollViewCallbacks listener);
/**
* 添加一个滑动事件的回调监听
*/
void addScrollViewCallbacks(ObservableScrollViewCallbacks listener);
/**
* 移除滑动回调监听器
*/
void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener);
/**
* 清楚所有滑动监听器
*/
void clearScrollViewCallbacks();
/**
* 垂直滚动到指定位置
* @param y Vertical position to scroll to.
*/
void scrollVerticallyTo(int y);
/**
*过去当前视图位置所对应的Y值
*
* @return Current Y pixel.
*/
int getCurrentScrollY();
/**
* 设置触摸事件的父布局视图ViewGroup
*
* @param viewGroup ViewGroup object to dispatch motion events.
*/
void setTouchInterceptionViewGroup(ViewGroup viewGroup);
}
ScrollState
/**
* 滑动组件的滑动状态
*/
public enum ScrollState {
/**
* 组件停止滑动
* 这个状态并不代表滑动控件从为滑动
*/
STOP,
/**
*上滑动
*/
UP,
/**
* 向下滑动
*/
DOWN,
}
ScrollUtils
/**
* 滑动效果辅助类
*/
public final class ScrollUtils {
/**
* 私有构造防止new实例
*/
private ScrollUtils() {
}
/**
* 返回一个有效值,该值的范围在【minValue,MaxValue】
*/
public static float getFloat(final float value, final float minValue, final float maxValue) {
return Math.min(maxValue, Math.max(minValue, value));
}
/**
* 创建一个带透明度的color的颜色值,透明度范围0-255
* 背景色改变用到它
* @param alpha 透明度
* @param baseColor 传入颜色值
*/
public static int getColorWithAlpha(float alpha, int baseColor) {
int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24;
int rgb = 0x00ffffff & baseColor;
return a + rgb;
}
/**
* 为视图添加监听,不需要的时候需要移除监听器
* @param view 监听的目标视图
* @param runnable Runnable to be executed after the view is laid out.
*/
public static void addOnGlobalLayoutListener(final View view, final Runnable runnable) {
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
runnable.run();
}
});
}
/**
* 混合两种颜色。并指定透明度
* @param fromColor
* @param toColor .
* @param toAlpha
* @return Mixed color value in ARGB. Alpha is fixed value (255).
*/
public static int mixColors(int fromColor, int toColor, float toAlpha) {
float[] fromCmyk = cmykFromRgb(fromColor);
float[] toCmyk = cmykFromRgb(toColor);
float[] result = new float[4];
for (int i = 0; i < 4; i++) {
result[i] = Math.min(1, fromCmyk[i] * (1 - toAlpha) + toCmyk[i] * toAlpha);
}
return 0xff000000 + (0x00ffffff & ScrollUtils.rgbFromCmyk(result));
}
/**
* RGB颜色转换为CMYK颜色。
*
* @param rgbColor Target color.
* @return CMYK array.
*/
public static float[] cmykFromRgb(int rgbColor) {
int red = (0xff0000 & rgbColor) >> 16;
int green = (0xff00 & rgbColor) >> 8;
int blue = (0xff & rgbColor);
float black = Math.min(1.0f - red / 255.0f, Math.min(1.0f - green / 255.0f, 1.0f - blue / 255.0f));
float cyan = 1.0f;
float magenta = 1.0f;
float yellow = 1.0f;
if (black != 1.0f) {
// black 1.0 causes zero divide
cyan = (1.0f - (red / 255.0f) - black) / (1.0f - black);
magenta = (1.0f - (green / 255.0f) - black) / (1.0f - black);
yellow = (1.0f - (blue / 255.0f) - black) / (1.0f - black);
}
return new float[]{cyan, magenta, yellow, black};
}
/**
* CYMK颜色转换为RGB颜色。
* 这个方法不会检查是否非空cmyk或有4个元素的数组。
*
* @param cmyk 目标CYMK颜色。每个值应该在0.0到1.0 f之间, 应设置在这个秩序:青色,品红色,黄色,黑色。
* @return ARGB color. Alpha is fixed value (255).
*/
public static int rgbFromCmyk(float[] cmyk) {
float cyan = cmyk[0];
float magenta = cmyk[1];
float yellow = cmyk[2];
float black = cmyk[3];
int red = (int) ((1.0f - Math.min(1.0f, cyan * (1.0f - black) + black)) * 255);
int green = (int) ((1.0f - Math.min(1.0f, magenta * (1.0f - black) + black)) * 255);
int blue = (int) ((1.0f - Math.min(1.0f, yellow * (1.0f - black) + black)) * 255);
return ((0xff & red) << 16) + ((0xff & green) << 8) + (0xff & blue);
}
}
TouchInterceptionFrameLayout
/**
* 一个布局接触滑动事件的拦截,布局提供移动滚动视图的容器使用滚动的位置。
* 请注意,这个类覆盖或使用触摸事件API,如onTouchEvent onInterceptTouchEvent dispatchTouchEvent
* 所以要小心当你处理触摸这个事件。
*/
public class TouchInterceptionFrameLayout extends FrameLayout {
/**
* 触摸事件回调接口
*/
public interface TouchInterceptionListener {
/**
* 判断是否要拦截当前视图的触摸事件
*
* @param ev Motion event.
* @param moving 是否在Action_Move移动中
* @param diffX 如果实在移动(ACTION_MOVE)返回滑动的距离X
* @param diffY 如果实在移动(ACTION_MOVE)返回滑动的距离Y
* @return 返回是否拦截布局
*/
boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY);
/**
* 拦截ACTION_DOWN事件
*
* @param ev Motion event.
*/
void onDownMotionEvent(MotionEvent ev);
/**
* 拦截ACTION_MOVE事件并且携带参数滑动距离
*
* @param ev Motion event.
* @param diffX Difference between previous X and current X.
* @param diffY Difference between previous Y and current Y.
*/
void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY);
/**
* 拦截ACTION_UP或者ACTION_CANCEL事件
*
* @param ev Motion event.
*/
void onUpOrCancelMotionEvent(MotionEvent ev);
}
/**
* 是否拦截
*/
private boolean mIntercepting;
/**
* 是否拦截ACTION_DOWN
*/
private boolean mDownMotionEventPended;
/**
* 刚进入ACTION_DOWN是否直接拦截
*/
private boolean mBeganFromDownMotionEvent;
/**
* 子view取消touch 事件
*/
private boolean mChildrenEventsCanceled;
/**
* 记录初始数据得Point
*/
private PointF mInitialPoint;
private MotionEvent mPendingDownMotionEvent;
private TouchInterceptionListener mTouchInterceptionListener;
public TouchInterceptionFrameLayout(Context context) {
super(context);
}
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setScrollInterceptionListener(TouchInterceptionListener listener) {
mTouchInterceptionListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener == null) {
//如果拦截Listener都不存在直接不拦截
return false;
}
// 在这里,我们必须接触状态变量进行初始化
// 问我们是否应该拦截这个事件。
// 我们是否应该拦截保存后的事件处理。
// private static native int nativeGetAction(long nativePtr); native方法获取Action对应的int值
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialPoint = new PointF(ev.getX(), ev.getY());
// private static native long nativeCopy(long destNativePtr, long sourceNativePtr,boolean keepHistory); 该方法是 MotionEvent.obtainNoHistory(ev);调用最终通过这个nativeCopy方法复制一个event,但是不完全复制
mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
mDownMotionEventPended = true;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
mBeganFromDownMotionEvent = mIntercepting;
mChildrenEventsCanceled = false;
return mIntercepting;
case MotionEvent.ACTION_MOVE:
// 避免mInitialPoint没有被初始化抛出异常
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
//计算出滑动的距离以便于接口回调所需
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
return mIntercepting;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mIntercepting) {
mTouchInterceptionListener.onDownMotionEvent(ev);
duplicateTouchEventForChildren(ev);
return true;
}
break;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
if (mIntercepting) {
// If this layout didn't receive ACTION_DOWN motion event,
// we should generate ACTION_DOWN event with current position.
if (!mBeganFromDownMotionEvent) {
mBeganFromDownMotionEvent = true;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
mTouchInterceptionListener.onDownMotionEvent(event);
mInitialPoint = new PointF(ev.getX(), ev.getY());
diffX = diffY = 0;
}
// Children's touches should be canceled
if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL));
}
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
// If next mIntercepting become false,
// then we should generate fake ACTION_DOWN event.
// Therefore we set pending flag to true as if this is a down motion event.
mDownMotionEventPended = true;
// Whether or not this event is consumed by the listener,
// assume it consumed because we declared to intercept the event.
return true;
} else {
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev, event);
} else {
duplicateTouchEventForChildren(ev);
}
// If next mIntercepting become true,
// then we should generate fake ACTION_DOWN event.
// Therefore we set beganFromDownMotionEvent flag to false
// as if we haven't received a down motion event.
mBeganFromDownMotionEvent = false;
// Reserve children's click cancellation here if they've already canceled
mChildrenEventsCanceled = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mBeganFromDownMotionEvent = false;
if (mIntercepting) {
mTouchInterceptionListener.onUpOrCancelMotionEvent(ev);
}
// Children's touches should be canceled regardless of
// whether or not this layout intercepted the consecutive motion events.
if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev, event);
} else {
duplicateTouchEventForChildren(ev);
}
}
return true;
}
}
return super.onTouchEvent(ev);
}
private MotionEvent obtainMotionEvent(MotionEvent base, int action) {
MotionEvent ev = MotionEvent.obtainNoHistory(base);
ev.setAction(action);
return ev;
}
/**
* 重复的子视图的触摸事件。
* 我们想分发一个滑动事件给子视图,但是调用dispatchTouchEvent()会导致StackOverflowError。
* 因此自己定义了一个事件分发。
*/
private void duplicateTouchEventForChildren(MotionEvent ev, MotionEvent... pendingEvents) {
if (ev == null) {
return;
}
for (int i = getChildCount() - 1; 0 <= i; i--) {
View childView = getChildAt(i);
if (childView != null) {
Rect childRect = new Rect();
childView.getHitRect(childRect);
MotionEvent event = MotionEvent.obtainNoHistory(ev);
if (!childRect.contains((int) event.getX(), (int) event.getY())) {
continue;
}
float offsetX = -childView.getLeft();
float offsetY = -childView.getTop();
boolean consumed = false;
if (pendingEvents != null) {
for (MotionEvent pe : pendingEvents) {
if (pe != null) {
MotionEvent peAdjusted = MotionEvent.obtainNoHistory(pe);
peAdjusted.offsetLocation(offsetX, offsetY);
consumed |= childView.dispatchTouchEvent(peAdjusted);
}
}
}
event.offsetLocation(offsetX, offsetY);
consumed |= childView.dispatchTouchEvent(event);
if (consumed) {
break;
}
}
}
}
}
挑一个源码实现简单过一遍ObervableScrollView(源码太多建议忽略这些)
/**
* ScrollView that its scroll position can be observed.
*/
public class ObservableScrollView extends ScrollView implements Scrollable {
// Fields that should be saved onSaveInstanceState
private int mPrevScrollY;
private int mScrollY;
// 我不得不说这个代码有点Android源码的味道,如果你看过ViewPager相关源码你就发现
private ObservableScrollViewCallbacks mCallbacks;
private List<ObservableScrollViewCallbacks> mCallbackCollection;
private ScrollState mScrollState;
private boolean mFirstScroll;
private boolean mDragging;
private boolean mIntercepted;
private MotionEvent mPrevMoveEvent;
private ViewGroup mTouchInterceptionViewGroup;
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (hasNoCallbacks()) {
return;
}
mScrollY = t;
dispatchOnScrollChanged(t, mFirstScroll, mDragging);
if (mFirstScroll) {
mFirstScroll = false;
}
if (mPrevScrollY < t) {//计算滑动距离设置滑动方向
mScrollState = ScrollState.UP;
} else if (t < mPrevScrollY) {
mScrollState = ScrollState.DOWN;
}
mPrevScrollY = t;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (hasNoCallbacks()) {
return super.onInterceptTouchEvent(ev);
}
switch (ev.getActionMasked()) {//调用native层方法返回Action具体类型
case MotionEvent.ACTION_DOWN:
mFirstScroll = mDragging = true;
dispatchOnDownMotionEvent();
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (hasNoCallbacks()) {
return super.onTouchEvent(ev);
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIntercepted = false;
mDragging = false;
dispatchOnUpOrCancelMotionEvent(mScrollState);
break;
case MotionEvent.ACTION_MOVE:
if (mPrevMoveEvent == null) {
mPrevMoveEvent = ev;
}
float diffY = ev.getY() - mPrevMoveEvent.getY();
mPrevMoveEvent = MotionEvent.obtainNoHistory(ev);
if (getCurrentScrollY() - diffY <= 0) {
// 不能滚动了。
if (mIntercepted) {
//ViewGroup已经派出ACTION_DOWN事件,所以停止在这里。
return false;
}
// Apps can set the interception target other than the direct parent.
final ViewGroup parent;
if (mTouchInterceptionViewGroup == null) {
parent = (ViewGroup) getParent();
} else {
parent = mTouchInterceptionViewGroup;
}
// Get offset to parents. If the parent is not the direct parent,
// we should aggregate offsets from all of the parents.
float offsetX = 0;
float offsetY = 0;
for (View v = this; v != null && v != parent; v = (View) v.getParent()) {
offsetX += v.getLeft() - v.getScrollX();
offsetY += v.getTop() - v.getScrollY();
}
final MotionEvent event = MotionEvent.obtainNoHistory(ev);
event.offsetLocation(offsetX, offsetY);
if (parent.onInterceptTouchEvent(event)) {
mIntercepted = true;
// If the parent wants to intercept ACTION_MOVE events,
// we pass ACTION_DOWN event to the parent
// as if these touch events just have began now.
event.setAction(MotionEvent.ACTION_DOWN);
// Return this onTouchEvent() first and set ACTION_DOWN event for parent
// to the queue, to keep events sequence.
post(new Runnable() {
@Override
public void run() {
parent.dispatchTouchEvent(event);
}
});
return false;
}
// Even when this can't be scrolled anymore,
// simply returning false here may cause subView's click,
// so delegate it to super.
return super.onTouchEvent(ev);
}
break;
}
return super.onTouchEvent(ev);
}
@Override
public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) {
mCallbacks = listener;
}
/**
* 添加一个监听器,使用方法类似ViewPager addPageChangeListener
**/
@Override
public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) {
if (mCallbackCollection == null) {
mCallbackCollection = new ArrayList<>();
}
mCallbackCollection.add(listener);
}
/**
* 移除设置的监听器
**/
@Override
public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) {
if (mCallbackCollection != null) {
mCallbackCollection.remove(listener);
}
}
/**
* 清楚所有监听器
**/
@Override
public void clearScrollViewCallbacks() {
if (mCallbackCollection != null) {
mCallbackCollection.clear();
}
}
@Override
public void setTouchInterceptionViewGroup(ViewGroup viewGroup) {
mTouchInterceptionViewGroup = viewGroup;
}
/**
* 垂直滑动到指定Y位置
*/
@Override
public void scrollVerticallyTo(int y) {
scrollTo(0, y);
}
@Override
public int getCurrentScrollY() {
return mScrollY;
}
/**
* Action_Down时候回调
*/
private void dispatchOnDownMotionEvent() {
if (mCallbacks != null) {
mCallbacks.onDownMotionEvent();
}
if (mCallbackCollection != null) {
for (int i = 0; i < mCallbackCollection.size(); i++) {
ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i);
callbacks.onDownMotionEvent();
}
}
}
/**
* 滑动变化回调
**/
private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
if (mCallbacks != null) {
mCallbacks.onScrollChanged(scrollY, firstScroll, dragging);
}
if (mCallbackCollection != null) {
for (int i = 0; i < mCallbackCollection.size(); i++) {
ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i);
callbacks.onScrollChanged(scrollY, firstScroll, dragging);
}
}
}
/**
* ACTION_UP OR ACTION_CANCEL会执行相应的回调
**/
private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) {
if (mCallbacks != null) {
mCallbacks.onUpOrCancelMotionEvent(scrollState);
}
if (mCallbackCollection != null) {
for (int i = 0; i < mCallbackCollection.size(); i++) {
ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i);
callbacks.onUpOrCancelMotionEvent(scrollState);
}
}
}
/**
* 是否有回调监听器
* @return
*/
private boolean hasNoCallbacks() {
return mCallbacks == null && mCallbackCollection == null;
}
//.........略...............
}
其他自定义控件和这个ObservableScrollView都差不多,下面具体实战效果图(结合ActionBar实现):
核心代码块
@Override
public void onUpOrCancelMotionEvent(ScrollState scrollState) {
ActionBar ab = getSupportActionBar();
if (ab == null) {
return;
}
if (scrollState == ScrollState.UP) {
if (ab.isShowing()) {
ab.hide();
}
} else if (scrollState == ScrollState.DOWN) {
if (!ab.isShowing()) {
ab.show();
}
}
}
上图主要是动态调用actionBar的show和hide方法,关于show hide方法的具体实现在ActionBarImpl里面下面是具体实现代码(http://code1.okbase.net/codefile/ActionBarImpl.java_2014110527698_13.htm):
void show(boolean markHiddenBeforeMode) {
if (mCurrentShowAnim != null) {
mCurrentShowAnim.end();
}
if (mContainerView.getVisibility() == View.VISIBLE) {
if (markHiddenBeforeMode) mWasHiddenBeforeMode = false;
return;
}
mContainerView.setVisibility(View.VISIBLE);
if (mShowHideAnimationEnabled) {
mContainerView.setAlpha(0);
AnimatorSet anim = new AnimatorSet();
AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mContainerView, "alpha", 1));
if (mContentView != null) {
b.with(ObjectAnimator.ofFloat(mContentView, "translationY",
-mContainerView.getHeight(), 0));
mContainerView.setTranslationY(-mContainerView.getHeight());
b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", 0));
}
if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
mSplitView.setAlpha(0);
mSplitView.setVisibility(View.VISIBLE);
b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 1));
}
anim.addListener(mShowListener);
mCurrentShowAnim = anim;
anim.start();
} else {
mContainerView.setAlpha(1);
mContainerView.setTranslationY(0);
mShowListener.onAnimationEnd(null);
}
}
@Override
public void hide() {
if (mCurrentShowAnim != null) {
mCurrentShowAnim.end();
}
if (mContainerView.getVisibility() == View.GONE) {
return;
}
if (mShowHideAnimationEnabled) {
mContainerView.setAlpha(1);
mContainerView.setTransitioning(true);
AnimatorSet anim = new AnimatorSet();
AnimatorSet.Builder b = anim.play(ObjectAnimator.ofFloat(mContainerView, "alpha", 0));
if (mContentView != null) {
b.with(ObjectAnimator.ofFloat(mContentView, "translationY",
0, -mContainerView.getHeight()));
b.with(ObjectAnimator.ofFloat(mContainerView, "translationY",
-mContainerView.getHeight()));
}
if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) {
mSplitView.setAlpha(1);
b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 0));
}
anim.addListener(mHideListener);
mCurrentShowAnim = anim;
anim.start();
} else {
mHideListener.onAnimationEnd(null);
}
}
上面实战CityList的效果源码地址:http://download.csdn.net/detail/analyzesystem/9560909