Android可滚动视图隐藏Toolbar实现

概述


有好多app在滑动视图列表的时候会动态的将ActionBar(Toolbar)隐藏(向下滑动显示,向上滑动隐藏),以留下更多的空间来显示内容;接下来我们自己来实现可滚动视图对Toolbar显示隐藏效果,看看实现的效果图:


其实看了上图的运行大家应该很清楚实现的步骤,不过在此我还是要唠叨两句了;
1.获取到手势滑动的方向和距离,以此来判断是上滑还是下滑(ps:距离可以用正负来表示,以此来判断方向)
2.上滑:将Toolbar隐藏。下滑:将Toolbar显示
当然上面只是简单介绍了流程,具体细节还得再继续讨论

视图布局

以上视图我采用自定义Toolbar和ListView来实现(ps:其他可滚动的视图也同样适用)

Toolbar的style:因为这里采用自定义Toolbar所以得更改默认布局,使用不带ActionBar的主题
<resources xmlns:android="http://schemas.android.com/apk/res/android">

    <style name="AppBaseTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">

        <!-- toolbar(actionbar)颜色 -->
        <item name="colorPrimary">#222222</item>
        <!-- 状态栏颜色 -->
        <item name="colorPrimaryDark">#3A5FCD</item>
        <item name="android:textColorPrimary">@android:color/white</item>
        <!-- 窗口的背景颜色 -->
        <item name="android:windowBackground">@android:color/white</item>
    </style>

    <style name="AppTheme" parent="@style/AppBaseTheme.Base"></style>

</resources>

Toolbar的布局:
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.xiaozhi.hideonscrolldemo"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary"
    android:minHeight="?attr/actionBarSize"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
    app:theme="@style/ThemeOverlay.AppCompat.ActionBar" >

</android.support.v7.widget.Toolbar>

activity布局:
<FrameLayout 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"
    tools:context="com.xiaozhi.hideonscrolldemo.MainActivity" >

    <com.xiaozhi.hideonscrolldemo.view.HidingListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:dividerHeight="16dp"
        android:paddingRight="16dp"
        android:paddingLeft="16dp"
        android:divider="@null" />
    
    <include layout="@layout/toolbar" />

</FrameLayout>
这里使用FrameLayout目的就是为了让Toolbar盖住ListView,当Toolbar隐藏不会留下空白区域

在MainActivity中实例化Toolbar:
		mToolbar = (Toolbar) findViewById(R.id.toolbar);
		setTitle("Xiao");
		setSupportActionBar(mToolbar);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		mListView = (HidingListView) findViewById(R.id.listView);

ListView的布局:
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="8dp"
    card_view:cardCornerRadius="2dp" >

    <TextView
        android:id="@+id/itemTextView"
        android:layout_width="match_parent"
        android:layout_height="?attr/listPreferredItemHeight"
        android:gravity="center_vertical"
        android:textColor="#000000"
        android:padding="8dp" />

</android.support.v7.widget.CardView>
注意:这里我们使用了CardView,记得把 android-support-v7-cardview.jar引用到自己的项目下
接下来我们为ListView设置Adapter,完成后的界面如下:



发现布局和我们预料有些诧异,ListView的第一个Item被遮挡住了,当然这个难不倒我们。我们可以给ListView加一个头布局,并让头布局和Toolbar的高度相同就OK了。

头布局view_header声明如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:gravity="center"
    android:text="hello world"/>

要如何在适配器中加载他呢?只需要在getVIew中判断是否是第一项就OK了,这没什么好说的还是看看Adapter如何实现的吧:
	private class MyAdapter extends BaseAdapter{

		private static final int TYPE_HEAD = 0;
		private static final int TYPE_ITEM = 1;

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

		@Override
		public String getItem(int position) {
			return mList.get(position - 1);
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public int getItemViewType(int position) {
			if (position == 0) {
				return TYPE_HEAD;
			}
			return TYPE_ITEM;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			if (getItemViewType(position) == TYPE_HEAD) {
				convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.view_header, parent,false);
			}else {

				ViewHolder holder = new ViewHolder();
				convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.listview_item, parent,false);
				holder.textView = (TextView) convertView.findViewById(R.id.itemTextView);
				convertView.setTag(holder);

				holder.textView.setText(getItem(position));
			}

			return convertView;
		}
	}

	private class ViewHolder {
		TextView textView;
	}
完成效果图:

以上就是基本成型的布局了,代码中没有什么注释,相信大家看代码的速度要远胜于看注释速度。由于代码都是常用的,写大量的注释反而感觉很多余。下面开始我们实现正真的逻辑代码了。

逻辑实现

1.获取滑动的距离以及滑动的方向
由于ListView的onScrollListener没法获取到这些参数,所以只好自己复写onTouchEvent方法
public boolean onTouchEvent(MotionEvent ev) {
		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
			startX = ev.getX();
			startY = ev.getY();
		}else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
			if (mListener == null) {
				return super.onTouchEvent(ev);
			}
			onScrolled(startX - ev.getX(), startY - ev.getY());
		}
		return super.onTouchEvent(ev);
	}
具体来解释一下这个方法:
当我们按下的时候记录下此时手指在x轴和y轴的位置作为起始位置。
当我们手指在滑动的时候我们用按下的位置减去滑动的位置就可以算出滑动的距离,由于Android手机的坐标系是以左上方为原点,向右为x正方向,向下为y正方向。所以当我们手指上滑的时候y值变小:startY-ev.getY()为+;手指下滑:startY-ev.getY为-;这里我们要记住这个规则下面的就好办了。

既然我们已经很轻松的获取到了上滑和下滑事件并且也可以知道滑动的距离,那我们就在onScrolled()方法中处理这个逻辑;
	private void onScrolled(float dx,float dy){
		if (dy > HIDE_DISTANCE && isToolbarVisible) {
			mListener.onHide();
			isToolbarVisible = false;
		}else if(dy < 0 && !isToolbarVisible){
			mListener.onShow();
			isToolbarVisible = true;
		}
	}
	
	public interface ScrollListener{
		public void onHide();
		public void onShow();
	}
因为move事件是很灵敏的所以我们最好让他滑动一段距离才隐藏或者显示,所以加入 HIDE_DISTANCE。因为触发事件要传到Activity中所以自定义 ScrollListener让MainActivity去实现它,用来控制Toolbar的显示或是隐藏。

自定义HidingListView如下:
public class HidingListView extends ListView {
	
	private static final String TAG = "HidingListView";
	
	private float startX;
	private float startY;
	private ScrollListener mListener;
	private static final int HIDE_DISTANCE = 30;
	private boolean isToolbarVisible = true;
	
	public void setScrollListener(ScrollListener mListener) {
		this.mListener = mListener;
	}

	public HidingListView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}

	public HidingListView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public HidingListView(Context context) {
		super(context);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
			startX = ev.getX();
			startY = ev.getY();
		}else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
			if (mListener == null) {
				return super.onTouchEvent(ev);
			}
			onScrolled(startX - ev.getX(), startY - ev.getY());
		}
		return super.onTouchEvent(ev);
	}
	
	private void onScrolled(float dx,float dy){
		if (dy > HIDE_DISTANCE && isToolbarVisible) {
			mListener.onHide();
			isToolbarVisible = false;
		}else if(dy < 0 && !isToolbarVisible){
			mListener.onShow();
			isToolbarVisible = true;
		}
	}
	
	public interface ScrollListener{
		public void onHide();
		public void onShow();
	}
}

在MainActivity中实现接口如下:
@SuppressLint("NewApi")
public class MainActivity extends ActionBarActivity {

	private static final String TAG = "MainActivity";

	private Toolbar mToolbar;
	private HidingListView mListView;
	private MyAdapter mMyAdapter;
	private List<String> mList = new ArrayList<String>();

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

		initViews();
	}

	private void initViews(){
		mToolbar = (Toolbar) findViewById(R.id.toolbar);
		setTitle("Xiao");
		setSupportActionBar(mToolbar);
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		mListView = (HidingListView) findViewById(R.id.listView);
		getData();
		mMyAdapter = new MyAdapter();
		mListView.setAdapter(mMyAdapter);
		mListView.setScrollListener(new ScrollListener() {

			@Override
			public void onShow() {
				showToolbar();
			}

			@Override
			public void onHide() {
				hideToolbar();
			}
		});

	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	private void hideToolbar(){
		mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2));
	}

	private void showToolbar(){
		mToolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2));
	}

	private void getData(){
		for (int i = 0; i < 10; i++) {
			mList.add("Item " + i);
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle action bar item clicks here. The action bar will
		// automatically handle clicks on the Home/Up button, so long
		// as you specify a parent activity in AndroidManifest.xml.
		int id = item.getItemId();
		if (id == R.id.action_settings) {
			return true;
		}
		return super.onOptionsItemSelected(item);
	}

	private class MyAdapter extends BaseAdapter{

		private static final int TYPE_HEAD = 0;
		private static final int TYPE_ITEM = 1;

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

		@Override
		public String getItem(int position) {
			return mList.get(position - 1);
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public int getItemViewType(int position) {
			if (position == 0) {
				return TYPE_HEAD;
			}
			return TYPE_ITEM;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			if (getItemViewType(position) == TYPE_HEAD) {
				convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.view_header, parent,false);
			}else {

				ViewHolder holder = new ViewHolder();
				convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.listview_item, parent,false);
				holder.textView = (TextView) convertView.findViewById(R.id.itemTextView);
				convertView.setTag(holder);

				holder.textView.setText(getItem(position));
			}

			return convertView;
		}
	}

	private class ViewHolder {
		TextView textView;
	}
}

以上算是我们基本上完成了显示和隐藏的功能,我非常幸喜的运行自己的demo结果出现如下问题:

想必看了图之后我们应该都知道是什么问题了当滑动小距离的时候Toolbar是隐藏了,可是view_header还是可见的。这该如何是好呢?

后来我发现可以获取第一个可见的Item来判断,但是ListView如何获取呢,查了资料可以实现它自己的的OnScrollListener并在onScroll方法中可以获取到firstVisibleItem这个参数,有了这个那就好办了。那么我自定义实现了OnScrollListener,并且稍作改动如下:
public abstract class HidingScrollListener implements OnScrollListener {

	private static final String TAG = "HidingScrollListener";
	private static final int HIDE_DISTANCE = 30;
	private boolean isToolbarVisible = true;
	private int firstVisibleItem;

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		this.firstVisibleItem = firstVisibleItem;
		if (isFirstItemVisible()) {
			onScrolled(0, 0);
		}
	}

	public void onScrolled(float dx,float dy){
		if (isFirstItemVisible()&& !isToolbarVisible) {
			onShow();
			isToolbarVisible = true;
		}else {
			if (dy > HIDE_DISTANCE && isToolbarVisible) {
				onHide();
				isToolbarVisible = false;
			}else if(dy < 0 && !isToolbarVisible){
				onShow();
				isToolbarVisible = true;
			}
		}
	}

	private boolean isFirstItemVisible(){
		return firstVisibleItem == 0;
	}

	public abstract void onHide();
	public abstract void onShow();

}

值得注意的是我在onScroll方法中加入了如下代码:
if (isFirstItemVisible()) {
			onScrolled(0, 0);
		}

这个也是我在后来发现的问题,具体就是在手指不离开界面先上滑然后猛的下滑由于下滑距离太短但是ListView还在惯性下移,此时并没有触发move事件,view_header又在此出现了。而onScroll方法不同当手指离开界面也会触发该事件,所以加入此消除bug。

最后

我只是实现了ListView的滑动隐藏和显示,其实GridView和ScrollView原理也是一样,ScrollView可能还会稍复杂一些,不过也都是大同小异。以上就是我个人最近做的一些总结和学习成果,希望帮到可能会用到的人。

附上GitHub中项目源码地址: https://github.com/xiaozhi003/HideOnScrollDemo



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值