Android自定义控件——FloatLayout

本文介绍一个好多App都有的布局容器,如图

这种效果在微博,美团,点评上面都有使用,是一种很不错的交互方式。

实现原理:

      自定义一个Layout,可以是LinearLayout,RelativeLayout

      容器总共有三个部分,HearderLayout最上面的部分,FloatLayout滑动的时候浮动的部分,ContentLayout下面的内容部分,这里我们使用了一个ViewPager+Fragment来代替,以保证满足使用时候的更多可能性。

      在初始化容器的时候给容器测算大小,关键是在ContentLayout的大小,ContentLayout的高度是父容器的高度减去FloatLayout的高度,也就是向上滑动的时候当HeaderLayout完全滑出父控件之后,此时的Contentayout的高度加上FloatLayout的高度正好等于父容器的高度

      在滑动的时候做事件分发和拦截,主要是处理什么时候滑动内部的ListView或者ScrollView,又在什么时候滑动整个容器。

      当HeaderLayout没有完全隐藏的时候就滑动整个容器,当HeaderLayout隐藏的时候滑动内部的ListView或者ScrollView,

      当向上滚动的时候,HeaderLayout完全隐藏时,整个容器就不再滚动了,接下来滚动的是ContentLayout的内容,所以就造成了FloatLayout悬浮在顶部的效果。



滚动重写了父容器的scrollTo来保证容器滚动的范围,滚动的范围在整个容器减掉HeaderLayout的高度的范围之内,不能太上,也不能太下。

FloatLayout.java

/**
 * 自定义的有悬浮layout的容器,类似微博,美团,点评的效果
 * 
 * @author mingwei
 * 
 */
public class FloatLayout extends LinearLayout {

	private RelativeLayout mHeaderLayout;
	private LinearLayout mFloatLayout;
	private ViewPager mContent;

	private int mHeaderHeight;
	private boolean isHeaderHidden;
	private ViewGroup mInnerScrollview;

	private OverScroller mScroller;
	private VelocityTracker mVelocityTracker;
	private int mTouchSlop;
	private int mMaximumVelocity, mMinimumVelocity;

	private float mLastY;
	private boolean isDragging;
	private boolean isMove = false;

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

	public FloatLayout(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public FloatLayout(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		mScroller = new OverScroller(context);
		mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
		mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
		mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();

	}

	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
		mHeaderLayout = (RelativeLayout) findViewById(R.id.float_layout_top);
		mFloatLayout = (LinearLayout) findViewById(R.id.float_layout_float);
		mContent = (ViewPager) findViewById(R.id.float_layout_content);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		ViewGroup.LayoutParams layoutParams = mContent.getLayoutParams();
		layoutParams.height = getMeasuredHeight() - mFloatLayout.getMeasuredHeight();
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		mHeaderHeight = mHeaderLayout.getMeasuredHeight();
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		int action = ev.getAction();
		float y = ev.getY();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastY = y;
			break;
		case MotionEvent.ACTION_MOVE:
			float moveY = y - mLastY;
			getCurrentScrollView();
			if (mInnerScrollview instanceof ScrollView) {
				if (mInnerScrollview.getScrollY() == 0 && isHeaderHidden && moveY > 0 && !isMove) {
					isMove = true;
					return dispatchInnerChild(ev);
				}
			} else if (mInnerScrollview instanceof ListView) {
				ListView listView = (ListView) mInnerScrollview;
				View viewItem = listView.getChildAt(listView.getFirstVisiblePosition());
				if (viewItem != null && viewItem.getTop() == 0 && isHeaderHidden && moveY > 0 && !isMove) {
					isMove = true;
					return dispatchInnerChild(ev);
				}
			}
			break;
		}
		return super.dispatchTouchEvent(ev);
	}

	private boolean dispatchInnerChild(MotionEvent ev) {
		ev.setAction(MotionEvent.ACTION_CANCEL);
		MotionEvent newMotionEvent = MotionEvent.obtain(ev);
		dispatchTouchEvent(ev);
		newMotionEvent.setAction(MotionEvent.ACTION_DOWN);
		return dispatchTouchEvent(newMotionEvent);
	}

	/**
	 * 事件拦截,来处理什么时候应该滑动那个部分的容器
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		int action = ev.getAction();
		float y = ev.getY();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastY = y;
			break;
		case MotionEvent.ACTION_MOVE:
			float moveY = y - mLastY;
			getCurrentScrollView();
			if (Math.abs(moveY) > mTouchSlop) {
				isDragging = true;
				if (mInnerScrollview instanceof ScrollView) {
					if (!isHeaderHidden || (mInnerScrollview.getScrollY() == 0 && isHeaderHidden && moveY > 0)) {
						initVelocityTracker();
						mVelocityTracker.addMovement(ev);
						mLastY = y;
						return true;
					}
				} else if (mInnerScrollview instanceof ListView) {
					ListView listView = (ListView) mInnerScrollview;
					View viewItem = listView.getChildAt(listView.getFirstVisiblePosition());
					if (!isHeaderHidden || (viewItem != null && viewItem.getTop() == 0 && moveY > 0)) {
						initVelocityTracker();
						mVelocityTracker.addMovement(ev);
						mLastY = y;
						return true;
					}
				}
			}

		case MotionEvent.ACTION_CANCEL:

		case MotionEvent.ACTION_UP:
			isDragging = false;
			recycleVelocityTracker();
			break;
		default:
			break;
		}
		return super.onInterceptTouchEvent(ev);
	}

	@SuppressLint("ClickableViewAccessibility")
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		initVelocityTracker();
		mVelocityTracker.addMovement(event);
		int action = event.getAction();
		float y = event.getY();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			mLastY = y;
			return true;
		case MotionEvent.ACTION_MOVE:
			float moveY = y - mLastY;
			if (!isDragging && Math.abs(moveY) > mTouchSlop) {
				isDragging = true;
			}
			if (isDragging) {
				scrollBy(0, (int) -moveY);
			}
			mLastY = y;
			break;
		case MotionEvent.ACTION_CANCEL:
			isDragging = false;
			recycleVelocityTracker();
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			break;
		case MotionEvent.ACTION_UP:
			isDragging = false;
			mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
			int velocity = (int) mVelocityTracker.getYVelocity();
			if (Math.abs(velocity) > mMinimumVelocity) {
				fling(-velocity);
			}
			recycleVelocityTracker();
			break;

		default:
			break;
		}
		return super.onTouchEvent(event);
	}

	/**
	 * 重写scrollTo,用来控制在滚动的过程中不至于 超出范围.
	 * 
	 * y<0,当Header完全显示在父容器时就不再允许Header能继续滑动.
	 * 
	 * y>mHeaderHeight,当Header部分完全画出父控件时,y能到达的最大值就是就是Header的高度.
	 * 
	 * y!=getScrollY(),调用父类的scrollTo,当y发生变化时,调用父类scrollTo滚动.
	 */
	@Override
	public void scrollTo(int x, int y) {
		y = (y < 0) ? 0 : y;
		y = (y > mHeaderHeight) ? mHeaderHeight : y;
		if (y != getScrollY()) {
			super.scrollTo(x, y);
		}
		isHeaderHidden = getScrollY() == mHeaderHeight;
	}

	@Override
	public void computeScroll() {
		if (mScroller.computeScrollOffset()) {
			scrollTo(0, mScroller.getCurrY());
			invalidate();
		}
	}

	/**
	 * 容器滚动时松开手指后根据velocity自动滚到到指定位置
	 * 
	 * @param velocityY
	 *            松开时的速度,OverScroll类帮助我们计算要滑多远
	 */
	public void fling(int velocityY) {
		mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mHeaderHeight);
		invalidate();
	}

	/**
	 * 根据当前的View来处理事件分发,例如容器当中是ScrollView,或者ListView时
	 */
	private void getCurrentScrollView() {
		int cuttentItem = mContent.getCurrentItem();
		PagerAdapter pagerAdapter = mContent.getAdapter();
		if (pagerAdapter instanceof FragmentPagerAdapter) {
			FragmentPagerAdapter adapter = (FragmentPagerAdapter) pagerAdapter;
			Fragment fragment = adapter.getItem(cuttentItem);
			mInnerScrollview = (ViewGroup) fragment.getView().findViewById(R.id.float_layout_inner_view);
		} else if (pagerAdapter instanceof FragmentStatePagerAdapter) {
			FragmentStatePagerAdapter adapter = (FragmentStatePagerAdapter) pagerAdapter;
			Fragment fragment = adapter.getItem(cuttentItem);
			mInnerScrollview = (ViewGroup) fragment.getView().findViewById(R.id.float_layout_inner_view);
		}
	}

	/**
	 * 初始化VelocityTracker
	 */
	private void initVelocityTracker() {
		if (mVelocityTracker == null) {
			mVelocityTracker = VelocityTracker.obtain();
		}
	}

	/**
	 * 回收VelocityTracker
	 */
	private void recycleVelocityTracker() {
		if (mVelocityTracker != null) {
			mVelocityTracker.recycle();
			mVelocityTracker = null;
		}
	}

}

内容部分分别使用了Fragment去装一个ListView的和一个ScrollView去处理事件分发

ListViewFragment.java

public class ListViewFragment extends Fragment {

	private View mContentView;
	private ListView mListView;
	private List<String> mList = new ArrayList<String>();

	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mContentView = LayoutInflater.from(getActivity()).inflate(R.layout.float_layout_inner_listview, null);
		mListView = (ListView) mContentView.findViewById(R.id.float_layout_inner_view);
		initData();
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		return mContentView;
	}

	private void initData() {
		for (int i = 0; i < 100; i++) {
			mList.add("ListView_Item" + i);
		}
		mListView.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, mList));
	}

	static Fragment getInstain() {
		Fragment fragment = new ListViewFragment();
		return fragment;
	}
}
xml

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@id/float_layout_inner_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

</ListView>

ScrollViewFragment.java

public class ScrollViewFragment extends Fragment {
	private View mContentView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mContentView = LayoutInflater.from(getActivity()).inflate(R.layout.float_layout_inner_scrollview, null);
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		return mContentView;
	}

	static Fragment getInstain() {
		Fragment fragment = new ScrollViewFragment();
		return fragment;
	}
}
xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@id/float_layout_inner_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        放20个TextView看效果

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dip"
            android:gravity="center"
            android:text="@string/float_layout_test_1" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dip"
            android:gravity="center"
            android:text="@string/float_layout_test_1" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dip"
            android:gravity="center"
            android:text="@string/float_layout_test_1" />
    </LinearLayout>

</ScrollView>

在Activity中如何使用

MainActivity.java

public class MainActivity extends FragmentActivity {

	private ViewPager mFloatContent;
	private List<Fragment> mFragments = new ArrayList<Fragment>();

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

	private void initView() {
		mFloatContent = (ViewPager) findViewById(R.id.float_layout_content);
		mFragments.add(ListViewFragment.getInstain());
		mFragments.add(ScrollViewFragment.getInstain());
		mFloatContent.setAdapter(new MyAdapter(getSupportFragmentManager(), mFragments));
	}

	class MyAdapter extends FragmentPagerAdapter {

		private List<Fragment> mList;

		public MyAdapter(FragmentManager fm, List<Fragment> list) {
			super(fm);
			mList = list;
		}

		@Override
		public Fragment getItem(int arg0) {
			return mList.get(arg0);
		}

		@Override
		public int getCount() {
			return mList.size();
		}
	}

}
xml

<com.mingwei.floatlayout.FloatLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <RelativeLayout
        android:id="@+id/float_layout_top"
        android:layout_width="match_parent"
        android:layout_height="200dip"
        android:background="@android:color/holo_blue_bright" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@string/float_top_layout_text" />
    </RelativeLayout>

    <LinearLayout
        android:id="@+id/float_layout_float"
        android:layout_width="match_parent"
        android:layout_height="50dip"
        android:background="@android:color/holo_green_light"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@string/float_layout_test_listview" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@string/float_layout_test_scrollview" />
    </LinearLayout>

    <android.support.v4.view.ViewPager
        android:id="@id/float_layout_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_orange_light" >
    </android.support.v4.view.ViewPager>

</com.mingwei.floatlayout.FloatLayout>


为了保证事件拦截的时候找到View,并且这个View又不确定是ListView还是ScrollView,所以在ids中定义了一个固定的id用来标识View,从而能顺利的找到该View,此后的分发和拦截的时候都去用instanceof来区别view的类型而做不同的判断。


Github:https://github.com/Mingwei360/FloatLayout


你可能感兴趣的:(仿美团浮动容器,FloatLayout,浮动的Layout)