StickyScrollView解说

package com.example.jj.myapplication;

import java.util.ArrayList;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ScrollView;

/**
 * 
 * @author Emil  - sjolander.emil@gmail.com
 *
 */
public class StickyScrollView extends ScrollView {

	/**
	 * Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc
	 */
	public static final String STICKY_TAG = "sticky";

	/**
	 * Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc
	 */
	public static final String FLAG_NONCONSTANT = "-nonconstant";

	/**
	 * Flag for views that have aren't fully opaque
	 */
	public static final String FLAG_HASTRANSPARANCY = "-hastransparancy";
	
	/**
	 * Default height of the shadow peeking out below the stuck view.
	 */
	private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;

	private ArrayList<View> stickyViews;
	private View currentlyStickingView;
	private float stickyViewTopOffset;
	private int stickyViewLeftOffset;
	private boolean redirectTouchesToStickyView;
	private boolean clippingToPadding;
	private boolean clipToPaddingHasBeenSet;

	private int mShadowHeight;
	private Drawable mShadowDrawable;

	private final Runnable invalidateRunnable = new Runnable() {

		@Override
		public void run() {
			if(currentlyStickingView!=null){
				int l = getLeftForViewRelativeOnlyChild(currentlyStickingView);
				int t  = getBottomForViewRelativeOnlyChild(currentlyStickingView);
				int r = getRightForViewRelativeOnlyChild(currentlyStickingView);
				int b = (int) (getScrollY() + (currentlyStickingView.getHeight() + stickyViewTopOffset));
				invalidate(l,t,r,b);
			}
			//每隔16毫秒刷新一次
			postDelayed(this, 16);
		}
	};

	public StickyScrollView(Context context) {
		this(context, null);
	}

	public StickyScrollView(Context context, AttributeSet attrs) {
		this(context, attrs, android.R.attr.scrollViewStyle);
	}

	public StickyScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		setup();

		TypedArray a = context.obtainStyledAttributes(attrs,
		        R.styleable.StickyScrollView, defStyle, 0);

    		final float density = context.getResources().getDisplayMetrics().density;
    		int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);

    		mShadowHeight = a.getDimensionPixelSize(
        		R.styleable.StickyScrollView_stuckShadowHeight,
        		defaultShadowHeightInPix);

    			int shadowDrawableRes = a.getResourceId(
        		R.styleable.StickyScrollView_stuckShadowDrawable, -1);

    		if (shadowDrawableRes != -1) {
      			mShadowDrawable = context.getResources().getDrawable(
          			shadowDrawableRes);
    		}

    		a.recycle();

	}

	/**
	 * Sets the height of the shadow drawable in pixels.
	 *
	 * @param height
	 */
	public void setShadowHeight(int height) {
		mShadowHeight = height;
	}
	

	public void setup(){
		stickyViews = new ArrayList<View>();
	}
	
	private int getLeftForViewRelativeOnlyChild(View v){
		int left = v.getLeft();
		while(v.getParent() != getChildAt(0)){
			v = (View) v.getParent();
			left += v.getLeft();
		}
		return left;
	}
	
	private int  getTopForViewRelativeOnlyChild(View v){
		//获取ScrollView里y轴的top位置,这里是循环获取的,否则只会获取当前这个布局下的y轴top位置
		int top = v.getTop();
		while(v.getParent() != getChildAt(0)){
			v = (View) v.getParent();
			top += v.getTop();
		}
		return top;
	}
	
	private int getRightForViewRelativeOnlyChild(View v){
		int right = v.getRight();
		while(v.getParent() != getChildAt(0)){
			v = (View) v.getParent();
			right += v.getRight();
		}
		return right;
	}
	
	private int getBottomForViewRelativeOnlyChild(View v){
		int bottom = v.getBottom();
		while(v.getParent() != getChildAt(0)){
			v = (View) v.getParent();
			bottom += v.getBottom();
		}
		return bottom;
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if(!clipToPaddingHasBeenSet){
			clippingToPadding = true;
		}
		notifyHierarchyChanged();
	}

	@Override
	public void setClipToPadding(boolean clipToPadding) {
		super.setClipToPadding(clipToPadding);
		clippingToPadding  = clipToPadding;
		clipToPaddingHasBeenSet = true;
	}

	@Override
	public void addView(View child) {
		super.addView(child);
		findStickyViews(child);
	}

	@Override
	public void addView(View child, int index) {
		super.addView(child, index);
		findStickyViews(child);
	}

	@Override
	public void addView(View child, int index, ViewGroup.LayoutParams params) {
		super.addView(child, index, params);
		findStickyViews(child);
	}

	@Override
	public void addView(View child, int width, int height) {
		super.addView(child, width, height);
		findStickyViews(child);
	}

	@Override
	public void addView(View child, ViewGroup.LayoutParams params) {
		super.addView(child, params);
		findStickyViews(child);
	}

	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);
		if(currentlyStickingView != null){
			canvas.save();
			//移动坐标的原点,以后画布局的位置都是基于该原点进行的。原点就是x、y的初始位置。
			canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() +   + (clippingToPadding ? getPaddingTop() : 0));

			//设置展示的范围
			canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0),
          			getWidth() - stickyViewLeftOffset,
          			currentlyStickingView.getHeight() + mShadowHeight + 1);

      			if (mShadowDrawable != null) {
        			int left = 0;
        			int right = currentlyStickingView.getWidth();
        			int top = currentlyStickingView.getHeight();
        			int bottom = currentlyStickingView.getHeight() + mShadowHeight;
        			mShadowDrawable.setBounds(left, top, right, bottom);
        			mShadowDrawable.draw(canvas);
      			}
			//设置展示的范围
			canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(), currentlyStickingView.getHeight());

			if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
				showView(currentlyStickingView);
				currentlyStickingView.draw(canvas);
				hideView(currentlyStickingView);
			}else{
				//画出该布局到ScrollView的顶部
				currentlyStickingView.draw(canvas);
			}
			canvas.restore();
		}
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		if(ev.getAction()==MotionEvent.ACTION_DOWN){
			redirectTouchesToStickyView = true;
		}

		if(redirectTouchesToStickyView){
			redirectTouchesToStickyView = currentlyStickingView != null;
			if(redirectTouchesToStickyView){
				//判断用户触摸的位置是否在画出来的布局位置
				redirectTouchesToStickyView = 
					ev.getY()<=(currentlyStickingView.getHeight()+stickyViewTopOffset) &&
					ev.getX() >= getLeftForViewRelativeOnlyChild(currentlyStickingView) && 
					ev.getX() <= getRightForViewRelativeOnlyChild(currentlyStickingView);
			}
		}else if(currentlyStickingView == null){
			redirectTouchesToStickyView = false;
		}
		if(redirectTouchesToStickyView){
			//如果触摸的位置是画出来的位置,那么将这个触摸的事件转移到被画的布局真正位置。
			//注:我们画出来的布局其实是跟着ScrollView的Y轴滑动走的,它只是一个展示不能监听点击事件,所以这里要转移点击事件。
			ev.offsetLocation(0, -1*((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
		}
		return super.dispatchTouchEvent(ev);
	}

	private boolean hasNotDoneActionDown = true;

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if(redirectTouchesToStickyView){
			ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
		} 
		
		if(ev.getAction()==MotionEvent.ACTION_DOWN){
			hasNotDoneActionDown = false;
		}
		
		if(hasNotDoneActionDown){
			MotionEvent down = MotionEvent.obtain(ev);
			down.setAction(MotionEvent.ACTION_DOWN);
			super.onTouchEvent(down);
			hasNotDoneActionDown = false;
		}
		
		if(ev.getAction()==MotionEvent.ACTION_UP || ev.getAction()==MotionEvent.ACTION_CANCEL){
			hasNotDoneActionDown = true;
		}
		
		return super.onTouchEvent(ev);
	}

	@Override
	protected void onScrollChanged(int l, int t, int oldl, int oldt) {
		super.onScrollChanged(l, t, oldl, oldt);
		doTheStickyThing();
	}

	/**
	 * 滑动的时候动态的判断哪些View需要展示在顶部
	 */
	private void doTheStickyThing() {
		View viewThatShouldStick = null;
		View approachingView = null;
		for(View v : stickyViews){
			//当前的滚动Y值是否已经超过了标志sticky view的显示位置
			int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop());
			if(viewTop<=0){
				//超过了就在这里赋值下,准备做view置顶显示
				if(viewThatShouldStick==null || viewTop>(getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))){
					viewThatShouldStick = v;
				}
			}else{
				//下一个即将显示的view
				if(approachingView == null || viewTop<(getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))){
					approachingView = v;
				}
			}
		}
		if(viewThatShouldStick!=null){
			stickyViewTopOffset = approachingView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingView) - getScrollY()  + (clippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());
			//如果当前sticky view已经发生改变了,停止之前的view刷新
			if(viewThatShouldStick != currentlyStickingView){
				if(currentlyStickingView!=null){
					stopStickingCurrentlyStickingView();
				}
				// only compute the left offset when we start sticking.
				stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
				//开始对这个view做置顶显示
				startStickingView(viewThatShouldStick);
			}
		}else if(currentlyStickingView!=null){
			stopStickingCurrentlyStickingView();
		}
	}

	private void startStickingView(View viewThatShouldStick) {
		currentlyStickingView = viewThatShouldStick;
		if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
			hideView(currentlyStickingView);
		}
		if(((String)currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)){
			post(invalidateRunnable);
		}
	}

	private void stopStickingCurrentlyStickingView() {
		if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
			showView(currentlyStickingView);
		}
		currentlyStickingView = null;
		removeCallbacks(invalidateRunnable);
	}

	/**
	 * Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy
	 */
	public void notifyStickyAttributeChanged(){
		notifyHierarchyChanged();
	}
	
	private void notifyHierarchyChanged(){
		if(currentlyStickingView!=null){
			stopStickingCurrentlyStickingView();
		}
		stickyViews.clear();
		findStickyViews(getChildAt(0));
		doTheStickyThing();
		invalidate();
	}

	/**
	 * 找到所有需要粘贴在顶部的view放在view集合中
	 */
	private void findStickyViews(View v) {
		if(v instanceof ViewGroup){
			ViewGroup vg = (ViewGroup)v;
			for(int i = 0 ; i<vg.getChildCount() ; i++){
				String tag = getStringTagForView(vg.getChildAt(i));
				if(tag!=null && tag.contains(STICKY_TAG)){
					stickyViews.add(vg.getChildAt(i));
				}else if(vg.getChildAt(i) instanceof ViewGroup){
					findStickyViews(vg.getChildAt(i));
				}
			}
		}else{
			String tag = (String) v.getTag();
			if(tag!=null && tag.contains(STICKY_TAG)){
				stickyViews.add(v);
			}
		}
	}
	
	private String getStringTagForView(View v){
		Object tagObject = v.getTag();
		return String.valueOf(tagObject);
	}

	private void hideView(View v) {
		if(Build.VERSION.SDK_INT>=11){
			v.setAlpha(0);
		}else{
			AlphaAnimation anim = new AlphaAnimation(1, 0);
			anim.setDuration(0);
			anim.setFillAfter(true);
			v.startAnimation(anim);
		}
	}

	private void showView(View v) {
		if(Build.VERSION.SDK_INT>=11){
			v.setAlpha(1);
		}else{
			AlphaAnimation anim = new AlphaAnimation(0, 1);
			anim.setDuration(0);
			anim.setFillAfter(true);
			v.startAnimation(anim);
		}
	}

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值