上图是在购物软件上常见的上拉查看图文详情,关于这中动画效果的实现,其实实现整体的效果,办法是有很多的,网上有很多相关的例子,但是对某些细节的处理不是很清晰,比如,下拉之后显示的部分(例如底部的图文详情)又是一个类ScrollView的控件(比如WebView)的话,又会产生新的问题。这里就以下拉查看图文详情为背景做一下同方向滑动冲突的分析。
整体思路
这里看下图
多个ScrollView嵌套示意图
首先,关于这张图做一些设定:
1.黑色的框代表手机屏幕
2.绿色的框代表一个外层的ScrollView
3.两个红色的框代表嵌套在里面的两个类ScrollView控件,这里我们就暂时简称为 SUp,SDown
好了,接下来就分析一下实现整个流程的过程。
这里必须明确的一点,无论何时,SUp和SDown可见的部分始终是手机屏幕的高度。知道了这一点,我们就可以按以下步骤展开
首先,我们确保外部的ScrollView不拦截滑动事件,这样SUp必然获得此次事件,然后根据其Action_Move事件,当其为向下滑动且自身滑动距离+屏幕高度=其自身高度 时,即可认为SUp滑动到了底部,此时外部ScrollView可拦截滑动事件,从而保证整个视图能够继续向下滑动,这个时候底部SDown就显示出来了。
同理,这时候不允许外部ScrollView拦截滑动事件,由SDown处理,根据其Action_move事件,当其为向上滑动,且自身可滑动距离为0时,就说明SDown已经滑动到了顶部,这时外部ScrollView又可以获得拦截滑动事件的权利,从而保证整个视图能够向上继续滑动,此时SUp再次显示,又开始新一轮循环拦截。
这样整体的一个流程就可以实现动图中的效果。好了,说完原理,还是看代码。
代码实现
SUp实现
public class UpScrollView extends ScrollView { /** * 屏幕高度 */ private int mScreenHeight; /** * 上一次的坐标 */ private float mLastY; /** * 当前View滑动距离 */ private int mScrollY; /** * 当前View内子控件高度 */ private int mChildH; public UpScrollView(Context context) { super(context); init(context); } public UpScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public UpScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); mScreenHeight = dm.heightPixels; } @Override public boolean onTouchEvent(MotionEvent ev) { //默认设定顶层View不拦截 getParent().getParent().requestDisallowInterceptTouchEvent(true); switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mLastY = (int) ev.getY();break;case MotionEvent.ACTION_MOVE:float y = ev.getY();float deltaY = y - mLastY;mChildH = this.getChildAt(0).getMeasuredHeight();int translateY = mChildH - mScrollY;//向上滑动时,如果translateY等于屏幕高度时,即表明滑动到底部,可又顶层View控制滑动if (deltaY < 0 && translateY == mScreenHeight) { getParent().getParent().requestDisallowInterceptTouchEvent(false);}break;default:break; } return super.onTouchEvent(ev); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); mScrollY = t; }}这里在ACTION_MOVE里做了减法,其实道理是一样的。
@param t Current vertical scroll origin.即为当前View此次滑动的距离
public class MyWebView extends WebView { public float oldY; private int t; public MyWebView(Context context) { super(context); init(); } public MyWebView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { WebSettings settings = getSettings(); settings.setJavaScriptEnabled(true); setWebViewClient(new WebViewClient() {@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {view.loadUrl(url);return true;} }); } @Override public boolean onTouchEvent(MotionEvent ev) { getParent().getParent().requestDisallowInterceptTouchEvent(true); switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:oldY = ev.getY();break;case MotionEvent.ACTION_MOVE:float Y = ev.getY();float Ys = Y - oldY;if (Ys > 0 && t == 0) { getParent().getParent().requestDisallowInterceptTouchEvent(false);}break; } return super.onTouchEvent(ev); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { this.t = t; super.onScrollChanged(l, t, oldl, oldt); }}以上看到,这里底部的View并没有继承ScrollView,而是选择继承了WebView,这里只是为了方便,当然继承ScrollView也是没有问题。这里只是需要按实际情况考虑,因为底部图文详情的内容就是一个WebView加载数据。
public class CustomerScrollViews extends ScrollView { /** * 屏幕高度 */ private int mScreenHeight; private UpScrollView upScrollView; private MyWebView myWebView; private boolean init = false; private float fator = 0.2f; private int factorHeight; private boolean upShow = true; public CustomerScrollViews(Context context) { super(context); init(context); } public CustomerScrollViews(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CustomerScrollViews(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); mScreenHeight = dm.heightPixels; factorHeight = (int) (mScreenHeight * fator); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!init) {LinearLayout parentView = (LinearLayout) getChildAt(0);//获得内部的两个子viewupScrollView = (UpScrollView) parentView.getChildAt(0);myWebView = (MyWebView) parentView.getChildAt(2);////并设定其高度为屏幕高度upScrollView.getLayoutParams().height = mScreenHeight;myWebView.getLayoutParams().height = mScreenHeight;init = true; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed) {scrollTo(0, 0); } } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) {case MotionEvent.ACTION_UP:int scrollY = getScrollY();if (upShow) { if (scrollY <= factorHeight) { smoothScrollTo(0, 0); } else { smoothScrollTo(0, mScreenHeight); upShow = false; }} else { int scrollpadding = mScreenHeight - scrollY; if (scrollpadding >= factorHeight) { this.smoothScrollTo(0, 0); upShow = true; } else { this.smoothScrollTo(0, mScreenHeight); }}return true; } return super.onTouchEvent(ev); }}这个类的实现,就很灵活了,在onMeasure方法中初始化完内部的View之后,在OnTouch方法中就可以根据实际需求完成不同的逻辑实现,这里只是为了仿照查看图文详情的效果,对整个视图通过ScrollView的smoothScrollTo方法进行位移变化,这个逻辑很简单。
upScrollView = (UpScrollView) parentView.getChildAt(0);myWebView = (MyWebView) parentView.getChildAt(2);你可能会奇怪中间的child(1)去了哪里?这里还要从MainActivity的布局文件说起
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.example.dreamwork.activity.superscrollview.CustomerScrollViews android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><com.example.dreamwork.activity.superscrollview.UpScrollViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:scrollbars="vertical"><LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="fitXY" android:src="@drawable/taobao" /> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="fitXY" android:src="@drawable/taobao" /> <TextView android:textSize="20sp" android:padding="10dp" android:gravity="center" android:layout_marginTop="20dp" android:layout_marginBottom="60dp" android:text="查看图文详情" android:layout_width="match_parent" android:layout_height="wrap_content" /></LinearLayout></com.example.dreamwork.activity.superscrollview.UpScrollView><include layout="@layout/selector_tab_items" /><com.example.dreamwork.activity.superscrollview.MyWebViewandroid:id="@+id/web"android:layout_width="match_parent"android:layout_height="match_parent"/> </LinearLayout> </com.example.dreamwork.activity.superscrollview.CustomerScrollViews></LinearLayout>整个布局文件可以看出,我们在CustomerScrollViews这个最外层的自定义ScrollView内部又放置了两个自定义的ScrollView(就如我们看到的原理图那样),只不过在这两个ScrollView类控件的中间通过layout又放置一个LinearLayout,里面的内容就是在动图中看到的那个中间的写着qq,baidu字样的用于切换WebView内容的一个View。这里就不贴代码了。
public class DualScrollViewActivity1 extends Activity implements View.OnClickListener { private MyWebView webView; private TextView sinaTv, qqTv, baiduTv; private View line1, line2, line3; private final String BAIDU = "http://www.baidu.com"; private final String QQ = "http://www.qq.com"; private final String SINA = "http://sina.cn"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); InitView(); sinaTv.performClick(); } private void InitView() { setContentView(R.layout.dual_scrollview_activity_layout1); webView = V.f(this, R.id.web); sinaTv = V.f(this, R.id.one); sinaTv.setOnClickListener(this); qqTv = V.f(this, R.id.two); qqTv.setOnClickListener(this); baiduTv = V.f(this, R.id.three); baiduTv.setOnClickListener(this); line1 = V.f(this, R.id.line1); line2 = V.f(this, R.id.line2); line3 = V.f(this, R.id.line3); } @Override public void onClick(View v) { reset(); String url = ""; switch (v.getId()) {case R.id.one:line1.setVisibility(View.VISIBLE);url = SINA;break;case R.id.two:line2.setVisibility(View.VISIBLE);url = QQ;break;case R.id.three:line3.setVisibility(View.VISIBLE);url = BAIDU;break;default:break; } webView.loadUrl(url); } private void reset(){ line1.setVisibility(View.GONE); line2.setVisibility(View.GONE); line3.setVisibility(View.GONE); }}关于底部View内容更新,WebView 通过加载不同URL实现不同视图效果,只是作为Demo测试,实际中应考虑通过fragment切换实现。
@Override public boolean onInterceptTouchEvent(MotionEvent e) { int y = (int) e.getRawY(); int x = (int) e.getRawX(); boolean resume = false; switch (e.getAction()) {case MotionEvent.ACTION_DOWN:// 发生down事件时,记录y坐标mLastMotionY = y;mLastMotionX = x;resume = false;break;case MotionEvent.ACTION_MOVE:// deltaY > 0 是向下运动,< 0是向上运动int deltaY = y - mLastMotionY;int deleaX = x - mLastMotionX;if (Math.abs(deleaX) > Math.abs(deltaY)) { resume = false;} else {//当前正处于滑动 if (isRefreshViewScroll(deltaY)) { resume = true; }}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:break; } return resume; }这里最关键的代码就是这行
if (Math.abs(deleaX) > Math.abs(deltaY)) { resume = false;}横向滑动距离大于纵向时,无须拦截这次滑动事件。其实,就是这么简单,但前提是你必须明确了解Android Touch事件的传递机制,期间各个方法执行的顺序及意义。
private BannerView carouselView; private Context mContext; private PullToRefreshView refreshView; ......... refreshView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {carouselView.getParent().requestDisallowInterceptTouchEvent(false);return false;} }); carouselView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {carouselView.getParent().requestDisallowInterceptTouchEvent(true);int x = (int) event.getRawX();int y = (int) event.getRawY();switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int deltaY = y - lastY; int deltaX = x - lastX; if (Math.abs(deltaX) < Math.abs(deltaY)) {carouselView.getParent().requestDisallowInterceptTouchEvent(false); } else {carouselView.getParent().requestDisallowInterceptTouchEvent(true); } default: break;}return false;} });首先说一下这个方法
public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)API里的意思很明确,子View如果不希望其父View拦截Touch事件时,可调用此方法。当disallowIntercept这个参数为true时,父View将不拦截。