English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
В проекте нужна функция обновления при прокрутке вниз, но этот View не ListView и подобный виджет, нужно реализовать эту функцию через ViewGroup, вначале я немного поискал в Интернете, но не нашел ничего особенно подходящего, и код был не очень понятен, поэтому решил написать это сам
Затем вытащил исходный код XlistView и начал его постепенно изучать, а затем大致 понял исходный код XLisview, и наконец решил взяться за это самому
Для удобства headView все равно использовал HeadView XListView, это значительно упростило работу:)
Обновление при прокрутке вниз, обновление при прокрутке вниз,肯定是先实现下拉功能, вначале я планировал реализовать это через extends ScrollView, потому что у него есть готовый эффект прокрутки, но на практике отказался по двум причинам:
1、Под ScrollView может быть только один подвид View, хотя можно добавить ViewGroup под Scroll, и динамически добавить headView в передний ViewGroup, но я все равно предпочитаю визуальный предпросмотр Studio, потому что это не так trựcуточно!
2、 При嵌入 ListView в ScrollView может произойти冲突, поэтому нужно перезаписать ListView. Поэтому отказался и перешел к другому подходу!
О причинах выше 1: динамически добавлять headView в GroupView ScrollView, можно перезаписать метод onViewAdded() ScrollView, чтобы добавить headView в подгруппу View
@Override public void onViewAdded(View child) { super.onViewAdded(child); //Потому что headView должен быть сверху, поэтому сначала подумал о Vertical LinearLayout LinearLayout linearLayout = (LinearLayout) getChildAt(0); linearLayout.addView(view, 0); }
Измените мышление, через extends LinearLayout!
Сначала подготовим, нам нужен headerView, который нужно получить высоту, а также исходная высота layout
private void initView(Context context) { mHeaderView = new SRefreshHeader(context); mHeaderViewContent = (RelativeLayout) mHeaderView.findViewById(R.id.slistview_header_content); setOrientation(VERTICAL); addView(mHeaderView, 0); getHeaderViewHeight(); getViewHeight(); }
mHeaderView = new SRefreshHeader(context);
Инстанцирование headerView через конструктор
mHeaderViewContent = (RelativeLayout)
mHeaderView.findViewById(R.id.slistview_header_content);
Это разбор содержимого headerView, через которую мы будем определять высоту, вы, наверное, спросите, почему не использовать mHeaderView для определения высоты, зайдите в метод конструктора и посмотрите следующий код
// Инициализация, установить высоту виджета для обновления下拉 на 0 LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); mContainer = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.listview_head_view_layout, null); w(mContainer, lp);
Если напрямую получить высоту mHeaderView, то это肯定是 0
getHeaderViewHeight();
getViewHeight();
Это получение высоты headerView и исходной высоты layout
/** * Получение высоты headView */ private void getHeaderViewHeight() { ViewTreeObserver vto2 = mHeaderViewContent.getViewTreeObserver(); vto2.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mHeaderViewHeight = mHeaderViewContent.getHeight(); mHeaderViewContent.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); } /** * Получение текущего экземпляра высоты SRefreshLayout */ private void getViewHeight() { ViewTreeObserver thisView = getViewTreeObserver(); thisView.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { SRefreshLayout.this.mHeight = SRefreshLayout.this.getHeight(); SRefreshLayout.this.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); }
Подготовка завершена,接下来 нужно выполнить действие прокрутки вниз
До этого момента, наверняка сразу приходит на ум метод onTouchEvent(), да! Начнем работу именно здесь
Реализация прокрутки вниз проходит через три этапа
ACTION_UP → ACTION_MOVE → ACTION_UP
В событии ACTION_UP, то есть когда палец нажимается, нам нужно просто запомнить координаты нажатия
switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // Запомнить начальную высоту mLastY = ev.getRawY(); // Запомнить координату Y при нажатии break;
Затем событие ACTION_MOVE, это наиболее важное, так как изменения высоты HeadView и layout происходят именно здесь
case MotionEvent.ACTION_MOVE: if (!isRefreashing) isRefreashing = true; final float deltaY = ev.getRawY() - mLastY; mLastY = ev.getRawY(); updateHeaderViewHeight(deltaY / 1.8f); // По определенному коэффициенту уменьшить расстояние перемещения updateHeight(); break;
Внутри updateHeaderViewHeight и updateHeight изменяются высота headerView и высота layout
private void updateHeight() { ViewGroup.LayoutParams lp = getLayoutParams(); //更新当前layout实例高度为headerView高度加上最初的layout高度 //如果不更新layout 会造成内容高度压缩 无法保持比例 lp.height = (mHeight + mHeaderView.getVisiableHeight()); setLayoutParams(lp); } private void updateHeaderViewHeight(float space) { // if (space < 0) // space = 0; // int factHeight = (int) (space - mHeaderViewHeight); if (mHeaderView.getStatus() != SRefreshHeader.STATE_REFRESHING) { //如果不处于刷新中同时如果高度 if (mHeaderView.getVisiableHeight() < mHeaderViewHeight * 2 && mHeaderView.getStatus() != SRefreshHeader.STATE_NORMAL) { mHeaderView.setState(SRefreshHeader.STATE_NORMAL); } if (mHeaderView.getVisiableHeight() > mHeaderViewHeight * 2 && mHeaderView.getStatus() != SRefreshHeader.STATE_READY) { mHeaderView.setState(SRefreshHeader.STATE_READY); } } mHeaderView.setVisiableHeight((int) space + mHeaderView.getVisiableHeight()); }
При обновлении высоты Header, мы определяем, достигли ли мы расстояния обновления, используя расстояние прокрутки, в коде я установил, что если расстояние достигает двойной высоты mHeaderView, то мы переходим в состояние «освободить обновление», если нет, то мы保持在 состоянии «прокрутка вниз для обновления»
В headerView установлено три состояния,分别是
public final static int STATE_NORMAL = 0;// pull to refresh public final static int STATE_READY = 1;// ready to refresh public final static int STATE_REFRESHING = 2;// refreshing
Метод обновления высоты headerView и layout одинаковый, это просто добавление расстояния перемещения к исходной высоте и повторное присвоение heights headerView или layout
mHeaderView.setVisiableHeight((int) space
+ mHeaderView.getVisiableHeight());
И, наконец, событие ACTION_UP, которое возникает, когда палец отрывается от экрана, здесь нам нужно определить конечное состояние headerView на основе текущего состояния headerView!
case MotionEvent.ACTION_UP: // При отпускании // Избегать триггера события нажатия if (!isRefreashing) break; // Если состояние headView находится в состоянии READY, то при отпускании он должен перейти в состояние REFRESHING if (mHeaderView.getStatus() == SRefreshHeader.STATE_READY) { mHeaderView.setState(SRefreshHeader.STATE_REFRESHING); } // В зависимости от состояния восстановить текущий экземпляр SrefreshLayout и высоту headView resetHeadView(mHeaderView.getStatus()); reset(mHeaderView.getStatus()); mLastY = -1; // Восстановить координаты break;
resetHeadView和reset分别是重置headerView高度和layout高度的方法
private void reset(int status) { ViewGroup.LayoutParams lp = getLayoutParams(); switch (status) { case SRefreshHeader.STATE_REFRESHING: lp.height = mHeight + mHeaderViewHeight; break; case SRefreshHeader.STATE_NORMAL: lp.height = mHeight; break; } setLayoutParams(lp); } private void resetHeadView(int status) { switch (status) { case SRefreshHeader.STATE_REFRESHING: mHeaderView.setVisiableHeight(mHeaderViewHeight);} break; case SRefreshHeader.STATE_NORMAL: mHeaderView.setVisiableHeight(0); break; } }
实现方式也是一样的。根据状态来判断,如果是处于刷新中,那headerView应该正常显示,并且高度是初始的高度,如果处于NORMAL,也就是"下拉刷新"状态,那么说未触发刷新,重置时,headerView应该被隐藏,也就是高度重置为0
到这里下拉刷新操作也基本完成了,还需要加一个回调接口进行通知
interface OnRefreshListener { void onRefresh(); }
case MotionEvent.ACTION_UP: // При отпускании // Избегать триггера события нажатия if (!isRefreashing) break; // Если состояние headView находится в состоянии READY, то при отпускании он должен перейти в состояние REFRESHING if (mHeaderView.getStatus() == SRefreshHeader.STATE_READY) { mHeaderView.setState(SRefreshHeader.STATE_REFRESHING); if (mOnRefreshListener != null) mOnRefreshListener.onRefresh(); } // В зависимости от состояния восстановить текущий экземпляр SrefreshLayout и высоту headView resetHeadView(mHeaderView.getStatus()); reset(mHeaderView.getStatus()); mLastY = -1; // Восстановить координаты break;
好,到这里就基本完成了,试试效果吧。咦,发现一个问题,嵌套ListView的时候为什么这个Layout不能执行下拉刷新!仔细想想应该是事件分发的问题,还需要处理一下事件的拦截!
关于事件拦截的处理,阅读了鸿洋大神写的viewgroup事件分发的博客和Android-Ultra-Pull-To-Refresh的部分源码,从中找到了解决办法:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { AbsListView absListView = null; for (int n = 0; n < getChildCount(); n++) { if (getChildAt(n) instanceof AbsListView) { absListView = (ListView) getChildAt(n); Logs.v("найден listView"); } } if (absListView == null) return super.onInterceptTouchEvent(ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mStartY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: float space = ev.getRawY() - mStartY; Logs.v("space:" + space); if (space > 0 && !absListView.canScrollVertically(-1) && absListView.getFirstVisiblePosition() == 0) { Logs.v("блокировка успешна"); return true; } else { Logs.v("не блокировать"); return false; } } return super.onInterceptTouchEvent(ev); }
其中
if (space > 0 && !absListView.canScrollVertically(-1) && absListView.getFirstVisiblePosition() == 0)
space即移动的距离 canScrollVertically()是判断ListView能否在垂直方向上滚动,参数为负数时代表向上,为正数时代码向下滚动,最后一个就是ListView第一个可见的item的postion
加上上面的事件拦截处理,一个可以满足开头提到的需求的Viewgroup也就完成了!
下面贴上Layout的源码和HeaderView(直接使用的XlistView的HeaderView)的源码
public class SRefreshLayout extends LinearLayout { private SRefreshHeader mHeaderView; private RelativeLayout mHeaderViewContent; private boolean isRefreashing; private float mLastY = -1; // 按下的起始高度 private int mHeaderViewHeight; // headerView内容高度 private int mHeight;// размер макета private float mStartY; interface OnRefreshListener { void onRefresh(); } public OnRefreshListener mOnRefreshListener; public SRefreshLayout(Context context) { super(context); initView(context); } public SRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public SRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } private void initView(Context context) { mHeaderView = new SRefreshHeader(context); mHeaderViewContent = (RelativeLayout) mHeaderView.findViewById(R.id.slistview_header_content); setOrientation(VERTICAL); addView(mHeaderView, 0); getHeaderViewHeight(); getViewHeight(); } /** * Получение высоты headView */ private void getHeaderViewHeight() { ViewTreeObserver vto2 = mHeaderViewContent.getViewTreeObserver(); vto2.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mHeaderViewHeight = mHeaderViewContent.getHeight(); mHeaderViewContent.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); } /** * Получение текущего экземпляра высоты SRefreshLayout */ private void getViewHeight() { ViewTreeObserver thisView = getViewTreeObserver(); thisView.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { SRefreshLayout.this.mHeight = SRefreshLayout.this.getHeight(); SRefreshLayout.this.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { AbsListView absListView = null; for (int n = 0; n < getChildCount(); n++) { if (getChildAt(n) instanceof AbsListView) { absListView = (ListView) getChildAt(n); Logs.v("найден listView"); } } if (absListView == null) return super.onInterceptTouchEvent(ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mStartY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: float space = ev.getRawY() - mStartY; Logs.v("space:" + space); if (space > 0 && !absListView.canScrollVertically(-1) && absListView.getFirstVisiblePosition() == 0) { Logs.v("блокировка успешна"); return true; } else { Logs.v("не блокировать"); return false; } } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (mLastY == -1) mLastY = ev.getRawY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // Запомнить начальную высоту mLastY = ev.getRawY(); // Запомнить координату Y при нажатии break; // При отрыве пальца от экрана case MotionEvent.ACTION_UP: // При отпускании // Избегать триггера события нажатия if (!isRefreashing) break; // Если состояние headView находится в состоянии READY, то при отпускании он должен перейти в состояние REFRESHING if (mHeaderView.getStatus() == SRefreshHeader.STATE_READY) { mHeaderView.setState(SRefreshHeader.STATE_REFRESHING); if (mOnRefreshListener != null) mOnRefreshListener.onRefresh(); } // В зависимости от состояния восстановить текущий экземпляр SrefreshLayout и высоту headView resetHeadView(mHeaderView.getStatus()); reset(mHeaderView.getStatus()); mLastY = -1; // Восстановить координаты break; case MotionEvent.ACTION_MOVE: if (!isRefreashing) isRefreashing = true; final float deltaY = ev.getRawY() - mLastY; mLastY = ev.getRawY(); updateHeaderViewHeight(deltaY / 1.8f); // По определенному коэффициенту уменьшить расстояние перемещения updateHeight(); break; } return super.onTouchEvent(ev); } private void reset(int status) { ViewGroup.LayoutParams lp = getLayoutParams(); switch (status) { case SRefreshHeader.STATE_REFRESHING: lp.height = mHeight + mHeaderViewHeight; break; case SRefreshHeader.STATE_NORMAL: lp.height = mHeight; break; } setLayoutParams(lp); } private void resetHeadView(int status) { switch (status) { case SRefreshHeader.STATE_REFRESHING: mHeaderView.setVisiableHeight(mHeaderViewHeight);} break; case SRefreshHeader.STATE_NORMAL: mHeaderView.setVisiableHeight(0); break; } } private void updateHeight() { ViewGroup.LayoutParams lp = getLayoutParams(); //更新当前layout实例高度为headerView高度加上最初的layout高度 //如果不更新layout 会造成内容高度压缩 无法保持比例 lp.height = (mHeight + mHeaderView.getVisiableHeight()); setLayoutParams(lp); } private void updateHeaderViewHeight(float space) { // if (space < 0) // space = 0; // int factHeight = (int) (space - mHeaderViewHeight); if (mHeaderView.getStatus() != SRefreshHeader.STATE_REFRESHING) { //如果不处于刷新中同时如果高度 if (mHeaderView.getVisiableHeight() < mHeaderViewHeight * 2 && mHeaderView.getStatus() != SRefreshHeader.STATE_NORMAL) { mHeaderView.setState(SRefreshHeader.STATE_NORMAL); } if (mHeaderView.getVisiableHeight() > mHeaderViewHeight * 2 && mHeaderView.getStatus() != SRefreshHeader.STATE_READY) { mHeaderView.setState(SRefreshHeader.STATE_READY); } } mHeaderView.setVisiableHeight((int) space + mHeaderView.getVisiableHeight()); } public void stopRefresh() { if (mHeaderView.getStatus() == SRefreshHeader.STATE_REFRESHING) { mHeaderView.setState(SRefreshHeader.STATE_NORMAL); resetHeadView(SRefreshHeader.STATE_NORMAL); reset(SRefreshHeader.STATE_NORMAL); } } public void setOnRefreshListener(OnRefreshListener onRefreshListener) { this.mOnRefreshListener = onRefreshListener; } }
public class SRefreshHeader extends LinearLayout { private LinearLayout mContainer; private int mState = STATE_NORMAL; private Animation mRotateUpAnim; private Animation mRotateDownAnim; private final int ROTATE_ANIM_DURATION = 500; public final static int STATE_NORMAL = 0;// pull to refresh public final static int STATE_READY = 1;// ready to refresh public final static int STATE_REFRESHING = 2;// refreshing private ImageView mHeadArrowImage; private TextView mHeadLastRefreashTimeTxt; private TextView mHeadHintTxt; private TextView mHeadLastRefreashTxt; private ProgressBar mRefreshingProgress; public SRefreshHeader(Context context) { super(context); initView(context); } /** * @param context * @param attrs */ public SRefreshHeader(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } private void initView(Context context) { // Инициализация, установить высоту виджета для обновления下拉 на 0 LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); mContainer = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.listview_head_view_layout, null); addView(mContainer, lp); setGravity(Gravity.BOTTOM); mHeadArrowImage = (ImageView) findViewById(R.id.slistview_header_arrow); mHeadLastRefreashTimeTxt = (TextView) findViewById(R.id.slistview_header_time); mHeadHintTxt = (TextView) findViewById(R.id.slistview_header_hint_text); mHeadLastRefreashTxt = (TextView) findViewById(R.id.slistview_header_last_refreash_txt); mRefreshingProgress = (ProgressBar) findViewById(R.id.slistview_header_progressbar); mRotateUpAnim = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION); mRotateUpAnim.setFillAfter(true); mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION); mRotateDownAnim.setFillAfter(true); } public void setState(int state) { if (state == mState) return; if (state == STATE_REFRESHING) { // Показать прогресс mHeadArrowImage.clearAnimation(); mHeadArrowImage.setVisibility(View.INVISIBLE); mRefreshingProgress.setVisibility(View.VISIBLE); } else { // Показать изображение стрелки mHeadArrowImage.setVisibility(View.VISIBLE); mRefreshingProgress.setVisibility(View.INVISIBLE); } switch (state) { case STATE_NORMAL: if (mState == STATE_READY) { mHeadArrowImage.startAnimation(mRotateDownAnim); } if (mState == STATE_REFRESHING) { mHeadArrowImage.clearAnimation(); } mHeadHintTxt.setText("Прогните вниз для обновления "); break; case STATE_READY: if (mState != STATE_READY) { mHeadArrowImage.clearAnimation(); mHeadArrowImage.startAnimation(mRotateUpAnim); mHeadHintTxt.setText("Отпустите, чтобы обновить "); } break; case STATE_REFRESHING: mHeadHintTxt.setText("Загрузка... "); break; default: } mState = state; } public void setVisiableHeight(int height) { if (height < 0) height = 0; LayoutParams lp = (LayoutParams) mContainer .getLayoutParams(); lp.height = height; mContainer.setLayoutParams(lp); } public int getStatus() { return mState; } public int getVisiableHeight() { return mContainer.getHeight(); } }
В конце файл разметки
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="bottom"> <RelativeLayout android:id="@+id/slistview_header_content" android:layout_width="match_parent" android:layout_height="60dp"> <LinearLayout android:id="@+id/slistview_header_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/slistview_header_hint_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下拉刷新" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3dp"> <TextView android:id="@+id/slistview_header_last_refreash_txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="上次刷新时间" android:textSize="12sp" /> <TextView android:id="@+id/slistview_header_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" /> </LinearLayout> </LinearLayout> <ProgressBar android:id="@+id/slistview_header_progressbar" android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_toLeftOf="@id/slistview_header_text" android:visibility="invisible" /> <ImageView android:id="@+id/slistview_header_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/slistview_header_progressbar" android:layout_centerVertical="true" android:layout_toLeftOf="@id/slistview_header_text" android:src="@drawable/mmtlistview_arrow" /> </RelativeLayout> </LinearLayout>
Вот и все, что было в этой статье, надеюсь, это поможет вам в изучении.我们也希望大家多多支持呐喊教程。
Объявление: содержимое этой статьи взято из Интернета, авторские права принадлежат соответствующему автору. Контент предоставлен пользователями Интернета, сайт не обладает правами собственности, не был отредактирован вручную и не несет ответственности за него. Если вы обнаружите подозрительное нарушение авторских прав, пожалуйста, отправьте письмо по адресу: notice#oldtoolbag.com (при отправке письма замените # на @) для отчета,并提供 соответствующие доказательства. В случае подтверждения мы немедленно удалим涉嫌侵权的内容.