Android Path Time ScrollBar(Path 时间轴)

来源:互联网 发布:点对点带宽测试软件 编辑:程序博客网 时间:2024/06/07 13:12
版本:1.0
日期:2014.4.22
版权:© 2014 kince 转载注明出处

  这是仿Path2.0UI的一个demo的截图,我最早是在农民伯伯的这篇博客中看到的【Andorid X 项目笔记】开源项目使用(6),他说这个程序是反编译Path的,但是这次我特地看了一下代码,发现其实不是这样的。原帖地址应该是这个:http://www.eoeandroid.com/forum.php?mod=viewthread&tid=187725,作者使用的是github上的一个开源项目:Android-ScrollBarPanel,它实现的效果如下:


  已经很接近Path的效果了,还有墨迹天气的实景也是使用了这种效果:

  而且,墨迹天气用的也是这个开源项目,效果什么基本都没改。所以下面重点说一下这个开源项目的实现。在看它的代码之前先来分析一下这个效果该如何实现,它就是在滚动条(scrollbar)的旁边动态显示一个View,这个View里面显示的内容会随着滚动条的位置变化而变化。一般像带滑动效果的容器控制都会有滚动条,比如ScrollView、ListView、GeidView等。那这个滚动条到底是什么呢?它是一个View的属性,该属性是继承view的, 目的设置滚动条显示,有以下设置none(隐藏),horizontal(水平),vertical (垂直),并不是所有的view设置就有效果, LinearLayout 设置也没有效果, 要想在超过一屏时拖动效果,在最外层加上ScrollView。而且可以自定义滚动条的样式和位置。但Path用的并不是自定义的滚动条,它是在滚动条旁边加的View,如图:

  若是在滚动条的旁边添加显示View,首先需要获取滚动条的位置,当在滑动的时候在显示滚动条的同时也让添加的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次。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;               }           }       } 
  了解完OnScrollListener这个接口再回头看一下代码,首先定义了一个回调:
   public static interface OnPositionChangedListener {          public void onPositionChanged(ExtendedListView listView, int position,                    View scrollBarPanel);     }
  这个用来在Activity中设置监听事件的,Activity的代码如下:
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);     }}
  接着看一下第三个构造方法,因为这个自定义的ListView定义了自己的属性,所以需要从attrs文件中来取出这些属性,自定义的属性包括三个部分,一是在ListView滑动时弹出的View,二是这个View弹出时的动画,三是这个View消失时的动画。然后开始设置这个弹出的View:
  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));     }
  
  先是调用下面这个方法,从xml文件中加载弹出View的布局,在这个地方需要说一下如果自定义的View不需要手动绘制的话,那么就可以使用LayoutInflater去在xml中加载一个已经配置好的视图,本例中就是使用这个方式。这样mScrollBarPanel就储存了弹出的View,然后设置为不可见,使用requestLayout()刷新一下视图。再接着就是加载两个弹出的动画,特别的,在mOutAnimation动画中设置了监听器,在动画结束的时候设置弹出的View不可见。
  回到第三个构造方法中,在第二行设置了super.setOnScrollListener(this),这个方法是效果实现的关键,为什么这么说,先看一下它的源代码。它是在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方法。就是这样一个流程。
  然后看一下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);          }     }
  上面已经说到,这个onScroll是随着滑动而一直调用的,而我们的需求就是在滑动的时候弹出一个View来,所以这个方法正是处理问题的关键位置。可以在这里绘制弹出View的视图,从上面的代码也可以看出,就是在这里进行弹出View大小的计算以及位置的设定等。
   最后就是之前说的自定义ViewGroup的问题了,重载onMeasure()、onLayout()、ondispatchDraw()方法了,这个在本例中也是有所体现的,不过都比较简单,相信都看得懂。但是这几个方法都是在View初始化的时候调用的,而且只是调用一次,这样并不适合动态的绘制视图。所以这也是为什么本例子继承了OnScrollListener,然后在其onScroll方法中去绘制视图,因为onScroll方法在滑动的时候会调用,所以在滑动的时候就会绘制视图了。因此也可以看出本例采用的是动态绘图的方式,不是显示隐藏的方式。
   




















3 0
原创粉丝点击