Android Path Time ScrollBar(Path 时间轴)
在看它的代码之前先来分析一下这个效果该怎样实现,它就是在滚动栏(scrollbar)的旁边动态显示一个View。这个View里面显示的内容会随着滚动栏的位置变化而变化。一般像带滑动效果的容器控制都会有滚动栏,比方ScrollView、ListView、GeidView等。那这个滚动栏究竟是什么呢?它是一个View的属性,该属性是继承view的, 目的设置滚动栏显示。有以下设置none(隐藏)。horizontal(水平),vertical (垂直)。并非全部的view设置就有效果。 LinearLayout 设置也没有效果。 要想在超过一屏时拖动效果,在最外层加上ScrollView。并且能够自己定义滚动栏的样式和位置。但Path用的并非自己定义的滚动栏,它是在滚动栏旁边加的View。如图:
那究竟怎样实现呢。带着这些疑问看一下源代码:
package com.dafruits.android.library.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.ListView; import com.dafruits.android.library.R; public class ExtendedListView extends ListView implements OnScrollListener { public static interface OnPositionChangedListener { public void onPositionChanged(ExtendedListView listView, int position, View scrollBarPanel); } private OnScrollListener mOnScrollListener = null; private View mScrollBarPanel = null; private int mScrollBarPanelPosition = 0; private OnPositionChangedListener mPositionChangedListener; private int mLastPosition = -1; private Animation mInAnimation = null; private Animation mOutAnimation = null; private final Handler mHandler = new Handler(); private final Runnable mScrollBarPanelFadeRunnable = new Runnable() { @Override public void run() { if (mOutAnimation != null) { mScrollBarPanel.startAnimation(mOutAnimation); } } }; /* * keep track of Measure Spec */ private int mWidthMeasureSpec; private int mHeightMeasureSpec; public ExtendedListView(Context context) { this(context, null); } public ExtendedListView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.listViewStyle); } public ExtendedListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); super.setOnScrollListener(this); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExtendedListView); final int scrollBarPanelLayoutId = a.getResourceId(R.styleable.ExtendedListView_scrollBarPanel, -1); final int scrollBarPanelInAnimation = a.getResourceId(R.styleable.ExtendedListView_scrollBarPanelInAnimation, R.anim.in_animation); final int scrollBarPanelOutAnimation = a.getResourceId(R.styleable.ExtendedListView_scrollBarPanelOutAnimation, R.anim.out_animation); a.recycle(); if (scrollBarPanelLayoutId != -1) { setScrollBarPanel(scrollBarPanelLayoutId); } final int scrollBarPanelFadeDuration = ViewConfiguration.getScrollBarFadeDuration(); if (scrollBarPanelInAnimation > 0) { mInAnimation = AnimationUtils.loadAnimation(getContext(), scrollBarPanelInAnimation); } if (scrollBarPanelOutAnimation > 0) { mOutAnimation = AnimationUtils.loadAnimation(getContext(), scrollBarPanelOutAnimation); mOutAnimation.setDuration(scrollBarPanelFadeDuration); mOutAnimation.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mScrollBarPanel != null) { mScrollBarPanel.setVisibility(View.GONE); } } }); } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChanged(view, scrollState); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (null != mPositionChangedListener && null != mScrollBarPanel) { // Don't do anything if there is no itemviews if (totalItemCount > 0) { /* * from android source code (ScrollBarDrawable.java) */ final int thickness = getVerticalScrollbarWidth(); int height = Math.round((float) getMeasuredHeight() * computeVerticalScrollExtent() / computeVerticalScrollRange()); int thumbOffset = Math.round((float) (getMeasuredHeight() - height) * computeVerticalScrollOffset() / (computeVerticalScrollRange() - computeVerticalScrollExtent())); final int minLength = thickness * 2; if (height < minLength) { height = minLength; } thumbOffset += height / 2; /* * find out which itemviews the center of thumb is on */ final int count = getChildCount(); for (int i = 0; i < count; ++i) { final View childView = getChildAt(i); if (childView != null) { if (thumbOffset > childView.getTop() && thumbOffset < childView.getBottom()) { /* * we have our candidate */ if (mLastPosition != firstVisibleItem + i) { mLastPosition = firstVisibleItem + i; /* * inform the position of the panel has changed */ mPositionChangedListener.onPositionChanged(this, mLastPosition, mScrollBarPanel); /* * measure panel right now since it has just changed * * INFO: quick hack to handle TextView has ScrollBarPanel (to wrap text in * case TextView's content has changed) */ measureChild(mScrollBarPanel, mWidthMeasureSpec, mHeightMeasureSpec); } break; } } } /* * update panel position */ mScrollBarPanelPosition = thumbOffset - mScrollBarPanel.getMeasuredHeight() / 2; final int x = getMeasuredWidth() - mScrollBarPanel.getMeasuredWidth() - getVerticalScrollbarWidth(); mScrollBarPanel.layout(x, mScrollBarPanelPosition, x + mScrollBarPanel.getMeasuredWidth(), mScrollBarPanelPosition + mScrollBarPanel.getMeasuredHeight()); } } if (mOnScrollListener != null) { mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } } public void setOnPositionChangedListener(OnPositionChangedListener onPositionChangedListener) { mPositionChangedListener = onPositionChangedListener; } @Override public void setOnScrollListener(OnScrollListener onScrollListener) { mOnScrollListener = onScrollListener; } public void setScrollBarPanel(View scrollBarPanel) { mScrollBarPanel = scrollBarPanel; mScrollBarPanel.setVisibility(View.GONE); requestLayout(); } public void setScrollBarPanel(int resId) { setScrollBarPanel(LayoutInflater.from(getContext()).inflate(resId, this, false)); } public View getScrollBarPanel() { return mScrollBarPanel; } @Override protected boolean awakenScrollBars(int startDelay, boolean invalidate) { final boolean isAnimationPlayed = super.awakenScrollBars(startDelay, invalidate); if (isAnimationPlayed == true && mScrollBarPanel != null) { if (mScrollBarPanel.getVisibility() == View.GONE) { mScrollBarPanel.setVisibility(View.VISIBLE); if (mInAnimation != null) { mScrollBarPanel.startAnimation(mInAnimation); } } mHandler.removeCallbacks(mScrollBarPanelFadeRunnable); mHandler.postAtTime(mScrollBarPanelFadeRunnable, AnimationUtils.currentAnimationTimeMillis() + startDelay); } return isAnimationPlayed; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mScrollBarPanel != null && getAdapter() != null) { mWidthMeasureSpec = widthMeasureSpec; mHeightMeasureSpec = heightMeasureSpec; measureChild(mScrollBarPanel, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mScrollBarPanel != null) { final int x = getMeasuredWidth() - mScrollBarPanel.getMeasuredWidth() - getVerticalScrollbarWidth(); mScrollBarPanel.layout(x, mScrollBarPanelPosition, x + mScrollBarPanel.getMeasuredWidth(), mScrollBarPanelPosition + mScrollBarPanel.getMeasuredHeight()); } } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mScrollBarPanel != null && mScrollBarPanel.getVisibility() == View.VISIBLE) { drawChild(canvas, mScrollBarPanel, getDrawingTime()); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mHandler.removeCallbacks(mScrollBarPanelFadeRunnable); } }通过阅读源代码发现,这是一个自己定义的ListView,并且继承了OnScrollListener接口。这个接口是在AbsListView.java里面定义的。主要是负责滑动事件的处理,它的代码例如以下:
/** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ public interface OnScrollListener { /** * The view is not scrolling. Note navigating the list using the trackball counts as * being in the idle state since these transitions are not animated. */ public static int SCROLL_STATE_IDLE = 0; /** * The user is scrolling using touch, and their finger is still on the screen */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; /** * The user had previously been scrolling using touch and had performed a fling. The * animation is now coasting to a stop */ public static int SCROLL_STATE_FLING = 2; /** * Callback method to be invoked while the list view or grid view is being scrolled. If the * view is being scrolled, this method will be called before the next frame of the scroll is * rendered. In particular, it will be called before any calls to * {@link Adapter#getView(int, View, ViewGroup)}. * * @param view The view whose scroll state is being reported * * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChanged(AbsListView view, int scrollState); /** * Callback method to be invoked when the list or grid has been scrolled. This will be * called after the scroll has completed * @param view The view whose scroll state is being reported * @param firstVisibleItem the index of the first visible cell (ignore if * visibleItemCount == 0) * @param visibleItemCount the number of visible cells * @param totalItemCount the number of items in the list adaptor */ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount); }OnScrollListener定义了三个常量。分别表示当屏幕停止滚动时为0;当屏幕滚动且用户使用的触碰或手指还在屏幕上时为1;由于用户的操作。屏幕产生惯性滑动时为2。详细解释例如以下:
new OnScrollListener() { boolean isLastRow = false; @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { //滚动时一直回调,直到停止滚动时才停止回调。单击时回调一次。 //firstVisibleItem:当前能看见的第一个列表项ID(从0開始) //visibleItemCount:当前能看见的列表项个数(小半个也算) //totalItemCount:列表项共数 //推断是否滚到最后一行 if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) { isLastRow = true; } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //正在滚动时回调,回调2-3次,手指没抛则回调2次。了解完OnScrollListener这个接口再回头看一下代码,首先定义了一个回调:scrollState = 2的这次不回调 //回调顺序例如以下 //第1次:scrollState = SCROLL_STATE_TOUCH_SCROLL(1) 正在滚动 //第2次:scrollState = SCROLL_STATE_FLING(2) 手指做了抛的动作(手指离开屏幕前,用力滑了一下) //第3次:scrollState = SCROLL_STATE_IDLE(0) 停止滚动 //当屏幕停止滚动时为0;当屏幕滚动且用户使用的触碰或手指还在屏幕上时为1。 //由于用户的操作,屏幕产生惯性滑动时为2 //当滚到最后一行且停止滚动时,运行载入 if (isLastRow && scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { //载入元素 ...... isLastRow = false; } } }
public static interface OnPositionChangedListener { public void onPositionChanged(ExtendedListView listView, int position, View scrollBarPanel); }
package com.dafruits.android.samples; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.dafruits.android.library.widgets.ExtendedListView; import com.dafruits.android.library.widgets.ExtendedListView.OnPositionChangedListener; public class DemoScrollBarPanelActivity extends Activity implements OnPositionChangedListener { private ExtendedListView mListView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mListView = (ExtendedListView) findViewById(android.R.id.list); mListView.setAdapter(new DummyAdapter()); mListView.setCacheColorHint(Color.TRANSPARENT); mListView.setOnPositionChangedListener(this); } private class DummyAdapter extends BaseAdapter { private int mNumDummies = 100; @Override public int getCount() { return mNumDummies; } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(DemoScrollBarPanelActivity.this).inflate(R.layout.list_item, parent, false); } TextView textView = (TextView) convertView; textView.setText("" + position); return convertView; } } @Override public void onPositionChanged(ExtendedListView listView, int firstVisiblePosition, View scrollBarPanel) { ((TextView) scrollBarPanel).setText("Position " + firstVisiblePosition); } }
if (scrollBarPanelLayoutId != -1) { setScrollBarPanel(scrollBarPanelLayoutId); }看一下设置的方法。
public void setScrollBarPanel(View scrollBarPanel) { mScrollBarPanel = scrollBarPanel; mScrollBarPanel.setVisibility(View.GONE); requestLayout(); } public void setScrollBarPanel(int resId) { setScrollBarPanel(LayoutInflater.from(getContext()).inflate(resId, this, false)); }
它是在AbsListView中定义的。
/** * Set the listener that will receive notifications every time the list scrolls. * * @param l the scroll listener */ public void setOnScrollListener(OnScrollListener l) { mOnScrollListener = l; invokeOnItemScrollListener(); }设置这种方法后,会传递一个OnScrollListener对象给mOnScrollListener,然后调用invokeOnItemScrollListener()方法,它的代码例如以下:
/** * Notify our scroll listener (if there is one) of a change in scroll state */ void invokeOnItemScrollListener() { if (mFastScroller != null) { mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount); } if (mOnScrollListener != null) { mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); } onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. }假设mOnScrollListener不为空的话,就调用mOnScrollListener的onScroll方法。而onScroll方法正是OnScrollListener接口定义的抽象方法,由于我们在ListView中继承了OnScrollListener接口,重载了onScroll方法,所以将会调用我们自己实现的onScroll方法。就是这样一个流程。
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { Log.i("onScroll", "onScroll"); if (null != mPositionChangedListener && null != mScrollBarPanel) { // Don't do anything if there is no itemviews if (totalItemCount > 0) { /* * from android source code (ScrollBarDrawable.java) */ final int thickness = getVerticalScrollbarWidth(); int height = Math.round((float) getMeasuredHeight() * computeVerticalScrollExtent() / computeVerticalScrollRange()); int thumbOffset = Math .round((float) (getMeasuredHeight() - height) * computeVerticalScrollOffset() / (computeVerticalScrollRange() - computeVerticalScrollExtent())); final int minLength = thickness * 2; if (height < minLength) { height = minLength; } thumbOffset += height / 2; /* * find out which itemviews the center of thumb is on */ final int count = getChildCount(); for (int i = 0; i < count; ++i) { final View childView = getChildAt(i); if (childView != null) { if (thumbOffset > childView.getTop() && thumbOffset < childView.getBottom()) { /* * we have our candidate */ if (mLastPosition != firstVisibleItem + i) { mLastPosition = firstVisibleItem + i; /* * inform the position of the panel has changed */ mPositionChangedListener.onPositionChanged( this, mLastPosition, mScrollBarPanel); /* * measure panel right now since it has just * changed * * INFO: quick hack to handle TextView has * ScrollBarPanel (to wrap text in case * TextView's content has changed) */ measureChild(mScrollBarPanel, mWidthMeasureSpec, mHeightMeasureSpec); } break; } } } /* * update panel position */ mScrollBarPanelPosition = thumbOffset - mScrollBarPanel.getMeasuredHeight() / 2; final int x = getMeasuredWidth() - mScrollBarPanel.getMeasuredWidth() - getVerticalScrollbarWidth(); mScrollBarPanel.layout( x, mScrollBarPanelPosition, x + mScrollBarPanel.getMeasuredWidth(), mScrollBarPanelPosition + mScrollBarPanel.getMeasuredHeight()); } } if (mOnScrollListener != null) { mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } }
可是这几个方法都是在View初始化的时候调用的,并且仅仅是调用一次。这样并不适合动态的绘制视图。所以这也是为什么本样例继承了OnScrollListener,然后在其onScroll方法中去绘制视图。由于onScroll方法在滑动的时候会调用,所以在滑动的时候就会绘制视图了。
因此也能够看出本例採用的是动态画图的方式,不是显示隐藏的方式。