android 自訂ScrollView實現反彈效果(以及解決和ListView之間的衝突)

來源:互聯網
上載者:User

首先還是一貫作風,先看一些案例:(介面)

          

玩過的朋友想必很熟悉,其實就是介面可以拖拽,會有反彈效果,看起來很炫酷.(總之比拖拽沒反應的死板要好.)

下面我來一一講解如何?這樣效果:

原理:自訂ScrollView對其Touch監聽,對布局時時更改.

一:不包含孩子,就是只針對普通的布局:

MyScrollView.java

package com.rebound.myscroll;import android.content.Context;import android.graphics.Rect;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.animation.TranslateAnimation;import android.widget.ScrollView;/** * 自訂ScrollView */public class MyScrollView extends ScrollView {private View inner;// 孩子private float y;// 座標private Rect normal = new Rect();// 矩形空白public MyScrollView(Context context, AttributeSet attrs) {super(context, attrs);}/*** * 根據 XML 產生視圖工作完成.該函數在產生視圖的最後調用,在所有子視圖添加完之後. 即使子類覆蓋了 onFinishInflate * 方法,也應該調用父類的方法,使該方法得以執行. */@Overrideprotected void onFinishInflate() {if (getChildCount() > 0) {inner = getChildAt(0);// 擷取其孩子}}@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (inner != null) {commOnTouchEvent(ev);}return super.onTouchEvent(ev);}/*** * 觸摸事件 *  * @param ev */public void commOnTouchEvent(MotionEvent ev) {int action = ev.getAction();switch (action) {case MotionEvent.ACTION_DOWN:y = ev.getY();// 擷取點擊y座標break;case MotionEvent.ACTION_UP:if (isNeedAnimation()) {animation();}break;case MotionEvent.ACTION_MOVE:final float preY = y;float nowY = ev.getY();int deltaY = (int) (preY - nowY);// 擷取滑動距離y = nowY;// 當滾動到最上或者最下時就不會再滾動,這時移動布局if (isNeedMove()) {if (normal.isEmpty()) {// 填充矩形,目的:就是告訴this:我現在已經有了,你鬆開的時候記得要執行迴歸動畫.normal.set(inner.getLeft(), inner.getTop(),inner.getRight(), inner.getBottom());}// 移動布局inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,inner.getRight(), inner.getBottom() - deltaY / 2);}break;default:break;}}/*** * 開啟動畫移動 */public void animation() {// 開啟移動動畫TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),normal.top);ta.setDuration(300);inner.startAnimation(ta);// 設定回到正常的布局位置inner.layout(normal.left, normal.top, normal.right, normal.bottom);normal.setEmpty();// 清空矩形}/*** * 是否需要開啟動畫 *  * 如果矩形不為空白,返回true,否則返回false. *  *  * @return */public boolean isNeedAnimation() {return !normal.isEmpty();}/*** * 是否需要移動布局 inner.getMeasuredHeight():擷取的是控制項的高度 * getHeight():擷取的是當前控制項在螢幕中顯示的高度 *  * @return */public boolean isNeedMove() {int offset = inner.getMeasuredHeight() - getHeight();int scrollY = getScrollY();// 0是頂部,後面那個是底部if (scrollY == 0 || scrollY == offset) {return true;}return false;}}


注釋已經很明確,我也不過多解釋,如有不明請留言。

效果:


下拉效果,鬆開會自動回縮.

是我隨便弄的,這個比較簡單,下面我們看下如果自訂的ScrollView裡麵包含ViewGroup類的控制項如何辦?

二:自訂ScrollView裡麵包含ListView.

想必這種效果比較常見,用處也比較廣.但是單存的用上面自訂的ScrollView是行不通的,滑動時候相當不靈敏,發生錯亂.為何:因為Touch受到影響,因為要繼續向下傳遞嘛.

我們要對上面自訂稍做修改:

MyScrollView.java

package com.jj.drag;import android.content.Context;import android.graphics.Rect;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.animation.TranslateAnimation;import android.widget.ScrollView;/** * ScrollView反彈效果的實現 */public class MyScrollView extends ScrollView {private View inner;// 孩子Viewprivate float y;// 點擊時y座標private Rect normal = new Rect();// 矩形(這裡只是個形式,只是用於判斷是否需要動畫.)private boolean isCount = false;// 是否開始計算public MyScrollView(Context context, AttributeSet attrs) {super(context, attrs);}/*** * 根據 XML 產生視圖工作完成.該函數在產生視圖的最後調用,在所有子視圖添加完之後. 即使子類覆蓋了 onFinishInflate * 方法,也應該調用父類的方法,使該方法得以執行. */@Overrideprotected void onFinishInflate() {if (getChildCount() > 0) {inner = getChildAt(0);}}/*** * 監聽touch */@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (inner != null) {commOnTouchEvent(ev);}return super.onTouchEvent(ev);}/*** * 觸摸事件 *  * @param ev */public void commOnTouchEvent(MotionEvent ev) {int action = ev.getAction();switch (action) {case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_UP:// 手指鬆開.if (isNeedAnimation()) {animation();isCount = false;}break;/*** * 排除出第一次移動計算,因為第一次無法得知y座標, 在MotionEvent.ACTION_DOWN中擷取不到, * 因為此時是MyScrollView的touch事件傳遞到到了LIstView的孩子item上面.所以從第二次計算開始. * 然而我們也要進行初始化,就是第一次移動的時候讓滑動距離歸0. 之後記錄準確了就正常執行. */case MotionEvent.ACTION_MOVE:final float preY = y;// 按下時的y座標float nowY = ev.getY();// 時時y座標int deltaY = (int) (preY - nowY);// 滑動距離if (!isCount) {deltaY = 0; // 在這裡要歸0.}y = nowY;// 當滾動到最上或者最下時就不會再滾動,這時移動布局if (isNeedMove()) {// 初始化頭部矩形if (normal.isEmpty()) {// 儲存正常的布局位置normal.set(inner.getLeft(), inner.getTop(),inner.getRight(), inner.getBottom());}Log.e("jj", "矩形:" + inner.getLeft() + "," + inner.getTop()+ "," + inner.getRight() + "," + inner.getBottom());// 移動布局inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,inner.getRight(), inner.getBottom() - deltaY / 2);}isCount = true;break;default:break;}}/*** * 回縮動畫 */public void animation() {// 開啟移動動畫TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),normal.top);ta.setDuration(200);inner.startAnimation(ta);// 設定回到正常的布局位置inner.layout(normal.left, normal.top, normal.right, normal.bottom);Log.e("jj", "迴歸:" + normal.left + "," + normal.top + "," + normal.right+ "," + normal.bottom);normal.setEmpty();}// 是否需要開啟動畫public boolean isNeedAnimation() {return !normal.isEmpty();}/*** * 是否需要移動布局 inner.getMeasuredHeight():擷取的是控制項的總高度 *  * getHeight():擷取的是螢幕的高度 *  * @return */public boolean isNeedMove() {int offset = inner.getMeasuredHeight() - getHeight();int scrollY = getScrollY();Log.e("jj", "scrolly=" + scrollY);// 0是頂部,後面那個是底部if (scrollY == 0 || scrollY == offset) {return true;}return false;}}

這個與上面那個稍有不同,我簡單講解下不同之處:

我們要排除出第一次ACTION_MOVE移動計算,因為第一次無法得知y座標, 在MotionEvent.ACTION_DOWN中是擷取不到, 因為此時是MyScrollView的touch事件傳遞到到了LIstView的孩子item上面.所以從第二次計算開始.(這裡其實不應該這麼搞,但是想不到更好的方法就只有這麼搞了.如有好方法,記得share). 然而我們也要進行對第一次移動的距離deltaY進行初始化,就是第一次移動的時候讓滑動距離歸0.
之後的在進行記錄執行相應的計算.

哈哈,明白了吧.其實也不難,可是也花了我好些時間.看下吧.

                
   

              原樣                                      下拉會自動縮回                       上拉會自動縮回


看著勉強湊合吧,重要的是實現.




/********************************仔細品讀想必對你有協助*****************************************/

在這裡我額外補充一點,不管你是否遇到過,總之掌握了對你沒有害處,為之,我可是花費了2個小時才發現錯誤在哪裡,原因:自己很土鱉.呵呵,不瞎扯了.

adapter.notifyDataSetChanged();這句話想必大家都不陌生,
作用:動態更新UI資料用的.

用法簡單說明:非同步或開線程對資料來源BaseAdapter裡面的資料進行更新.然後在主線程中執行adapter.notifyDataSetChanged();(其實就是從新執行getView方法,自己可以調試try 一下.)

但是我們這裡用的不是單純的ListView,而是自訂的ScrollView包裹著ListView,而ListView和ScrollView本來是冤家,想必現在大家都明白怎麼解決,就是我setAdapter();後再次對ListView布局從新布局,這樣有效解決了二者的衝突.

方法如下:(大家都看的明白,這裡就不過多解釋)

/*** * 動態設定listview的高度 *  * @param listView */public void setListViewHeightBasedOnChildren(ListView listView) {ListAdapter listAdapter = listView.getAdapter();if (listAdapter == null) {return;}int totalHeight = 0;for (int i = 0; i < listAdapter.getCount(); i++) {View listItem = listAdapter.getView(i, null, listView);listItem.measure(0, 0);totalHeight += listItem.getMeasuredHeight();}ViewGroup.LayoutParams params = listView.getLayoutParams();params.height = totalHeight+ (listView.getDividerHeight() * (listAdapter.getCount() - 1))+ 15;listView.setLayoutParams(params);}

重點:我們在其中執行了adapter.notifyDataSetChanged();UI沒有做出任何反映,這個為什麼呢?

牛逼人物我想一目瞭然就知道哪裡,但是我卻花了2個小時,原因:還是原理沒搞懂.

解釋:我們首先解釋下顯示的效果原理:首先我們先把ListView show出來了,因為和ScrollView衝突然後我們又調用setListViewHeightBasedOnChildren();進行重新排版布局,才使得顯示我們要的那種效果,之後我們對資料來源做的修改,調用了adapter.notifyDataSetChanged();原理上應該顯示出來了(此時資料來源已經更新),因為此時我們setListViewHeightBasedOnChildren();只是顯示我們修改資料之前,所以UI是不會更新的,所以要想更新,那麼我們得從新調用一下setListViewHeightBasedOnChildren();根據最新的資料來源繪圖,這樣就不會出錯了。大家明白了吧,只要明白道理了,其實很簡單.

就不展示了,想必大家都知道是what.


上訴有一個小BUG,就是如果當資料超出一屏的話,你下拉的時候不鬆開,然後慢慢向上移動,你會發現跑得那是相當的快,如果想做成那種效果就是下拉的時候重新整理,那麼相當不好控制,原因其實很簡單:當我們下拉的時候用的根本不是ScrollView的滑動.我們是通過對layout布局進行時時更新,此時的getScrollY()始終為0,但是你停止下拉的時候反而向上滑動的時候這個時候getScrollY()就不為0,原因為什麼呢,因為我們下拉其實就是讓布局向下移動了,拋開這裡其實就相當於我滑動了,所以你上滑動的時候會ScrollView滑動會影響你,(自己可以測試看下log.)這就是bug原因,解決辦法:我們只需要在滑動中添加: scrollBy(0,
-deltaY);當超過一屏的時候ScrollView就會有自己的滑動,但是必須取消該滑動,因為我們有自己的滑動,作用:抵消ScrollView內建滑動.從而至運用對布局時時更新.

問題解決了,不過又有一個扯淡的問題,我到底在瞎忙什麼,直接用簡單的布局檔案不就行了,為何要用ScrollView呢,還有ScrollView這個東西和ListView衝突在加上對Touch事件的分發處理不是那麼協調,NND,搞了一下午,現在頭都大了.


就說到這裡,如有疑問請留言。

另外,如果對您有協助的話,記得贊一個哦.

在此:Thanks for you !


 


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.