編輯:關於Android編程
最近公司沒事,研究了下多嵌套滾動組件的事件分發,雖然以前也接觸過,但都是拿網上的用,也是特別簡單的,正好朋友也需要,就研究了下。這個Demo也不是很完善,放上來也是讓各位大牛給指點一下,優化優化
使用情景:
小米商城商品詳情界面,界面看似ScrollView,但當正常滾動到底部時,提示繼續上拉顯示更多詳情,上拉後直接滾動到第二屏,第二屏是個ViewPager,ViewPager裡面的各個pager有的是WebView有的是ListView,有的是ScrollView,一開始想想就特別頭暈,後來理清思路後,實現起來卻處處碰壁,不是ViewPager不能左右滑動就是ListView不能上拉,網上也搜索了很多相關Demo,但都沒有完善一點的,也許根本沒幾個人使用這樣的無腦嵌套吧,好吧,既然這樣,就只有自己動手了。
花了1周時間,總算出來點效果了,重寫了幾個組件:InnerScrollView、InnerWebView、InnerListView
如果內部ScrollView是固定高度,那麼需要滾動,外部的當然也需要滾動,所以要判斷當內部滾動到頂部並且手指繼續下滑時,把事件交父類處理,同樣當滾動到底部並繼續上滑時也要交出去,如果InnerScrollView的ChildView高度小於等於InnerScrollView高度(就是不出現滾動條)時,把事件交給父類處理。
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;
/**
* 內部ScrollView,解決滑動內部ScrollView時,觸發外部滾動問題
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerScrollView extends ScrollView {
private final String TAG = "XinInnerScrollView";
private float childHeight = 0;
private float downX, downY; // 按下時
private float currX, currY; // 移動時
private float moveY; // 從按下到移動的Y距離
private float scrollViewHeight;
private boolean isOnTop; // ScrollView是否處於屏幕頂端
private boolean isOnBottom; // ScrollView是否處於屏幕底端
private boolean debug = true;
private Position position = Position.NONE;
public XinInnerScrollView(Context context) {
this(context, null);
}
public XinInnerScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XinInnerScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
downX = ev.getX();
downY = ev.getY();
childHeight = getChildAt(0).getMeasuredHeight();
scrollViewHeight = getHeight();
break;
case MotionEvent.ACTION_MOVE:
currX = ev.getX();
currY = ev.getY();
moveY = Math.abs(currY - downY);
isOnTop = getScrollY() == 0;
isOnBottom = (getScrollY() + scrollViewHeight) == childHeight;
// 垂直滑動
if (moveY > Math.abs(currX - downX)) {
if (childHeight <= scrollViewHeight) {
printLog("onTouchEvent ACTION_MOVE 不能滾動 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else if (isOnTop) { // 當前處於ScrollView頂部
if (currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 已到頂部 下滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到頂部 上滑 子處理");
}
} else if (isOnBottom) {
// 當前處於ScrollView底部
if (currY - downY < 0) {
printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子處理");
}
} else {
// 當前處於ScrollView中間
printLog("onTouchEvent ACTION_MOVE 在中間 子處理");
}
}
// 水平滾動
else {
if(position.equals(Position.TOP)){
printLog("onTouchEvent ACTION_MOVE 水平滾動 position=TOP 子處理");
} else {
if(Math.abs(currX - downX) > 30){
printLog("onTouchEvent ACTION_MOVE 水平滾動 position!=TOP 橫向滑動距離>30 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 水平滾動 position!=TOP 橫向滑動距離<=30 子處理");
}
}
}
break;
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP ========================");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
/**
* 為了更好的處理手勢滑動事件,設置該組件所處的位置;
* 比如只有上下兩屏時,如果該View是在第一屏,那麼設置為Position.TOP,如果在第二屏,則設置為Position.BOTTOM
*
* @param position
*/
public void setPosition(Position position) {
this.position = position;
}
public static enum Position {
/**
* 頂部View,橫向滑動時將不考慮將事件交給父View。(該設計只為第一屏為純ScrollView考慮)
*/
TOP,
/**
* 底部View, 橫向滑動時,將把事件交給父View處理
*/
BOTTOM,
/**
* 不設置,將自動判斷(自動判斷並不是很精准)
*/
NONE
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
說一下Position,因為第一屏或者第二屏中的ViewPager裡面也可能用到InnerScrollView,ViewPager裡面的需要考慮左右滑動的事件,但第一屏是不需要的,為了在第一屏做橫向滑動時(一般第一屏應該只有一個ScrollView),不把事件交給父類,所以需要知道該InnerScrollView是在哪裡使用的,設置該標記,做更好的判斷。日志中“子處理”處只打日志,不設置getParent().getParent().requestDisallowInterceptTouchEvent(true);是因為在ACTION_DOWN時已經告訴父類不要攔截,只需要在移動時在適合的條件下通知父類自己不再處理。這就是重寫的內部ScrollView。
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.webkit.WebView;
/**
* 內部WebView, 該View只適合放在最後一屏
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerWebView extends WebView {
private final String TAG = "XinInnerScrollView";
private boolean debug = true;
private float downX, downY; // 按下時
private float currX, currY; // 移動時
private float moveX; // 移動長度-橫向
public XinInnerWebView(Context context) {
super(context);
}
public XinInnerWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public XinInnerWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
printLog("onTouchEvent ACTION_DOWN");
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
currX = ev.getX();
currY = ev.getY();
moveX = Math.abs(currX - downX);
printLog("onTouchEvent ACTION_MOVE getScrollX()="+getScrollX() + " getScrollY()="+getScrollY());
// 垂直滑動
if (Math.abs(currY - downY) > moveX) {
// 處於頂部或者無法滾動,並且繼續下滑,交出事件(currY-downY >0是下滑, <0則是上滑)
if (getScrollY() == 0 && currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 在頂部 下滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
// 已到底部且繼續上滑時,把事件交出去
else if(getContentHeight()*getScale() - (getHeight() + getScrollY()) <= 1 && currY - downY < 0){
printLog("onTouchEvent ACTION_MOVE 在底部 上滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
// 水平滾動,橫向滑動長度大於20像素時再交出去,不然都當做是垂直滑動。
else if(moveX > 20){
// 橫向滑動事不直接交出去,是因為可能頁面出現水平滾動條,就是網頁寬度比屏幕還寬的情況下就需要判斷滑到左邊和滑到右邊的情況。
// printLog("onTouchEvent ACTION_MOVE 橫向滑動 父處理");
// getParent().getParent().requestDisallowInterceptTouchEvent(false);
// 已在左邊且繼續右滑時,把事件交出去(currX - downX >0是右滑, <0則是左滑)
if (getScrollX() == 0 && currX - downX > 0) {
printLog("onTouchEvent ACTION_MOVE 在左邊 右滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
// 已在右邊且繼續左滑時,把事件交出去
else if(getRight()*getScale() - (getWidth() + getScrollX()) <= 1 && currX - downX < 0){
printLog("onTouchEvent ACTION_MOVE 在右邊 左滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.ListView;
/**
* 內部ListView, 該組件不支持下拉刷新,上拉加載更多,是為了嵌套在ScrollView或者ViewPager中的,適合數據少的環境使用,最好是一次性顯示所有數據
* 提供一個接口OnLastItemVisibleListener,當最後一個item完全顯示時,回調onLastItemVisible(),可以去加載更多數據。
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerListView extends ListView implements AbsListView.OnScrollListener {
private final String TAG = "XinInnerScrollView";
private boolean debug = true;
private boolean isFirstItemVisible; // 第一個item是否可見
private boolean isLastItemVisible; // 最後一個item是否可見
private int downX, downY; // 按下時
private int currX, currY; // 移動時
private int moveY; // 從按下到移動的Y距離
public XinInnerListView(Context context) {
super(context);
setOnScrollListener(this);
}
public XinInnerListView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnScrollListener(this);
}
public XinInnerListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnScrollListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
downX = (int)ev.getX();
downY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
currX = (int)ev.getX();
currY = (int)ev.getY();
moveY = Math.abs(currY - downY);
if(currY == downY){
break;
}
// 垂直滑動
if (moveY > Math.abs(currX - downX)) {
if (isFirstItemVisible) { // 當前處於頂部
if (currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 已到頂部 下滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到頂部 上滑 子處理");
}
} else if (isLastItemVisible) {
// 當前處於底部
if (currY - downY < 0) {
printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子處理");
}
} else {
// 當前處於中間
printLog("onTouchEvent ACTION_MOVE 在中間 子處理");
}
} else {
// 水平滾動
printLog("onTouchEvent ACTION_MOVE 水平滾動 父處理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP ========================");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
/**
* 判斷最後listView中最後一個item是否完全顯示出來
* @return
*/
protected boolean isLastItemVisible() {
Adapter adapter = getAdapter();
if (null == adapter || adapter.isEmpty()) {
return true;
}
int lastVisiblePosition = getLastVisiblePosition();
if (lastVisiblePosition >= (adapter.getCount() - 1) - 1) {
View lastVisibleChild = getChildAt(Math.min(lastVisiblePosition - getFirstVisiblePosition(), getChildCount() - 1));
if (lastVisibleChild != null) {
return lastVisibleChild.getBottom() <= getBottom();
}
}
return false;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
isFirstItemVisible = firstVisibleItem == 0 && getScrollY() == 0;
isLastItemVisible = isLastItemVisible();
if (isLastItemVisible) {
if (onLastItemVisibleListener != null) {
onLastItemVisibleListener.onLastItemVisible(view, firstVisibleItem + visibleItemCount - 1, getAdapter());
}
}
}
public void setOnLastItemVisibleListener(OnLastItemVisibleListener onLastItemVisibleListener) {
this.onLastItemVisibleListener = onLastItemVisibleListener;
}
private OnLastItemVisibleListener onLastItemVisibleListener;
/**
* 最後一個Item顯示的監聽器
*/
public interface OnLastItemVisibleListener {
void onLastItemVisible(AbsListView view, int position, Adapter adapter);
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
在代碼中出現很多沒有代碼的else,只是為了看日志方便而已。
白色區域是可以單獨滾動的

ScrollViewInScrollViewActivity.java
public class ScrollViewInScrollViewActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrollview);
setTitle("ScrollView In ScrollView");
}
}
activity_scrollview.xml
黑色背景是原生ScrollView,中間是InnerWebView,該WebView設置了固定高度,所以可以上下滑動,如果高度設置為wrap_content,則會把整個網頁撐開,與外部ScrollView融合在一起。

WebViewInScrollViewActivity.java
package com.wuguangxin.morescrolldemo.ui;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.wuguangxin.morescrolldemo.Configs;
import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerWebView;
/**
* WebView In ScrollView
*
* @author wuguangxin
* @date 16/7/5 上午11:35
*/
public class WebViewInScrollViewActivity extends FragmentActivity {
private XinInnerWebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
setTitle("WebView In ScrollView");
mWebView = (XinInnerWebView) findViewById(R.id.webview);
mWebView.loadUrl(Configs.URL_BAIDU);
initWebView();
}
private void initWebView(){
WebSettings webSet = mWebView.getSettings();
webSet.setSupportZoom(true); // 支持縮放
webSet.setAllowFileAccess(true); // 設置可以訪問文件
webSet.setJavaScriptEnabled(true); // 啟用JavaScript
webSet.setBlockNetworkImage(false); // 限制網絡圖片
webSet.setBuiltInZoomControls(true); // 控制頁面縮放
webSet.setLoadWithOverviewMode(true); // 設置webview加載的頁面的模式,
webSet.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM); // 設置默認的縮放級別
webSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
mWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url){
view.loadUrl(url);
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon){
}
@Override
public void onPageFinished(WebView view, String url){
}
});
}
}
activity_webview.xml

ListViewInScrollViewActivity.java
package com.wuguangxin.morescrolldemo.ui;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerListView;
import java.util.ArrayList;
import java.util.List;
/**
* ListView In ScrollView
*
* @author wuguangxin
* @date 16/7/5 上午10:59
*/
public class ListViewInScrollViewActivity extends FragmentActivity {
private int maxListSize = 300;
private List list;
private MyAdapter mAdapter;
private int i;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview);
setTitle("ListView In ScrollView");
XinInnerListView mListView = (XinInnerListView) findViewById(R.id.listview);
list = getList();
mAdapter = new MyAdapter(this, list);
mListView.setAdapter(mAdapter);
mListView.setOnLastItemVisibleListener(new XinInnerListView.OnLastItemVisibleListener() {
@Override
public void onLastItemVisible(AbsListView view, int position, Adapter adapter) {
if(list.size() < maxListSize){
list.addAll(getList());
mAdapter.setData(list);
mAdapter.notifyDataSetChanged();
}
}
});
}
private List getList(){
if(list == null){
list = new ArrayList<>();
}
List tempList = new ArrayList<>();
for (i = 0; i < 50; i++) {
tempList.add("item " + (this.list.size() + i+1) + " / "+maxListSize);
}
return tempList;
}
public class MyAdapter extends BaseAdapter {
private List list;
private Context context;
public MyAdapter(Context context, List list) {
this.context = context;
this.list = list;
}
@Override
public int getCount() {
return list == null ? 0 : list.size();
}
@Override
public String getItem(int position) {
return list == null ? null : list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder = null;
if(convertView == null){
holder = new Holder();
convertView = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, null);
holder.mTextView = (TextView)convertView;
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
}
holder.mTextView.setText(getItem(position));
return convertView;
}
public void setData(List list) {
this.list = list;
}
class Holder {
private TextView mTextView;
}
}
}
activity_listview.java
該界面仿小米商城App商品詳情界面,也類似蘑菇街、京東、淘寶等界面。
如下面原型圖



本Demo效果圖:



手機qq能截圖嗎 安卓手機qq怎麼開啟截屏功能
很多朋友在用手機聊天的時候,常常會將自己的聊天記錄當做是一張圖片形式保存起來。或者是把一些奇葩的聊天過程曬出來。手機qq能截圖嗎?安卓手機qq怎麼開啟截屏功
基於Dragonboard 410c從零到使用Sensor Demo
前言:本文主要是針對沒有接觸過Dragonboard 410c開發板的朋友,教大家如何從裸板搭建平台以及通過這個平台如何去操作Light、Gesture、Color這三個
android listview級聯三菜單選擇地區,本地數據庫sqlite級聯地區,item選中不變色
前言:因為找了N多網上的資源都沒有好的解決方案,別人都是只給思路沒給具體源碼,真TMD糾結,干嘛求別人,自己動手才是真,最痛恨那些所謂大牛的作風,給了點點代碼就讓別人去想
pkbox安卓模擬器雙開多開辦法
在電腦安裝手機游戲軟件時候要用到安卓模擬器,為了工作需要,比如聊天類工具都需要多開,那麼今天講下載使用pkbox安卓模擬器多開的方法。請升級為最新版PXbo