仿淘寶商品詳情頁面Android
1、需求:
要實現一個類似淘寶、京東的商品詳情頁面。首先是在看一些前輩的思路,檢視之後,發現博主qifengdeqingchen的文章不錯,然後去下載下來檢視demo。
2、查閱資料
來看看前輩的思路圖。使用兩個scrollView,兩個scrollView 豎直排列,通過自定義viewGroup來控制兩個scrollView的豎直排列,以及滑動事件的處理。
3、發現問題:
我在scrollView2中新增一個TabLayout+ViewPager,然後在新增兩個Fragment,在fragment中寫一個scrollview.這時候發現在Fragment中只可以向下滑動,當想要去向上滑動的時候,就滑動到了scrollView1中。
4、解決問題:
首先來看一下前輩的程式碼,如下:
public class PullUpToLoadMore extends ViewGroup {
public static String TAG = PullUpToLoadMore.class.getName();
MyScrollView topScrollView, bottomScrollView;
VelocityTracker velocityTracker = VelocityTracker.obtain();
Scroller scroller = new Scroller(getContext());
int currPosition = 0;
int position1Y;
int lastY;
public int scaledTouchSlop;//最小滑動距離
int speed = 200;
boolean isIntercept;
public boolean bottomScrollVIewIsInTop = false;
public boolean topScrollViewIsBottom = false;
public PullUpToLoadMore(Context context) {
super (context);
init();
}
public PullUpToLoadMore(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PullUpToLoadMore(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
post(new Runnable() {
@Override
public void run() {
topScrollView = (MyScrollView) getChildAt(0);
bottomScrollView = (MyScrollView) getChildAt(1);
topScrollView.setScrollListener(new MyScrollView.ScrollListener() {
@Override
public void onScrollToBottom() {
topScrollViewIsBottom = true;
}
@Override
public void onScrollToTop() {
}
@Override
public void onScroll(int scrollY) {
}
@Override
public void notBottom() {
topScrollViewIsBottom = false;
}
});
bottomScrollView.setScrollListener(new MyScrollView.ScrollListener() {
@Override
public void onScrollToBottom() {
}
@Override
public void onScrollToTop() {
}
@Override
public void onScroll(int scrollY) {
if (scrollY == 0) {
bottomScrollVIewIsInTop = true;
} else {
bottomScrollVIewIsInTop = false;
}
}
@Override
public void notBottom() {
}
});
position1Y = topScrollView.getBottom();
scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//防止子View禁止父view攔截事件
this.requestDisallowInterceptTouchEvent(false);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//判斷是否已經滾動到了底部
if (topScrollViewIsBottom) {
int dy = lastY - y;
//判斷是否是向上滑動和是否在第一屏
if (dy > 0 && currPosition == 0) {
if (dy >= scaledTouchSlop) {
isIntercept = true;//攔截事件
lastY=y;
}
}
}
if (bottomScrollVIewIsInTop) {
int dy = lastY - y;
//判斷是否是向下滑動和是否在第二屏
if (dy < 0 && currPosition == 1) {
if (Math.abs(dy) >= scaledTouchSlop) {
isIntercept = true;
}
}
}
break;
}
return isIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int dy = lastY - y;
if (getScrollY() + dy < 0) {
dy = getScrollY() + dy + Math.abs(getScrollY() + dy);
}
if (getScrollY() + dy + getHeight() > bottomScrollView.getBottom()) {
dy = dy - (getScrollY() + dy - (bottomScrollView.getBottom() - getHeight()));
}
scrollBy(0, dy);
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
velocityTracker.computeCurrentVelocity(1000);
float yVelocity = velocityTracker.getYVelocity();
if (currPosition == 0) {
if (yVelocity < 0 && yVelocity < -speed) {
smoothScroll(position1Y);
currPosition = 1;
} else {
smoothScroll(0);
}
} else {
if (yVelocity > 0 && yVelocity > speed) {
smoothScroll(0);
currPosition = 0;
} else {
smoothScroll(position1Y);
}
}
break;
}
lastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int childTop = t;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(l, childTop, r, childTop + child.getMeasuredHeight());
childTop += child.getMeasuredHeight();
}
}
//通過Scroller實現彈性滑動
private void smoothScroll(int tartY) {
int dy = tartY - getScrollY();
scroller.startScroll(getScrollX(), getScrollY(), 0, dy);
invalidate();
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
}
通過分析我們可以大概瞭解到,出現在個問題應該是在滑動事件的解決上。在自定義控制元件中的onInterceptTouchEvent方法中,可能攔截了滑動事件,才導致了問題的出現。於是我在onInterceptTouchEvent方法中做出了改動。當向下滑動時,判斷是否在ScrollView2中,分別處理滑動事件。
當頁面沒有在ScrollView2中,就要讓自定義viewGroup中的Scrollview頁面切換(這時候父控制元件攔截OnTouchEvent,不向子控制元件傳遞,讓父控制元件滑動)。當頁面在ScrollView2中,還要去判斷viewpager裡邊的scrollview是否滑動到了頂部,如果在頂部,要去切換viewgroup中的頁面(這時候父控制元件攔截OnTouchEvent,不向子控制元件傳遞,讓父控制元件滑動),如果沒有滑動到頂部,就要讓viewpager中的scrollview滑動(這時候父控制元件不攔截OnTouchEvent,向子控制元件傳遞,讓子控制元件滑動)。
5、詳細程式碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//判斷是否已經滾動到了底部
if (topScrollViewIsBottom) {
int dy = lastY - y;
//判斷是否是向上滑動和是否在第一屏
if (dy > 0 && currPosition == 0) {
if (dy >= scaledTouchSlop) {
isIntercept = true;//攔截事件
lastY=y;
}
}
}
if (bottomScrollVIewIsInTop) {
int dy = lastY - y;
//判斷是否是向下滑動和是否在第二屏
if (dy < 0 && currPosition == 1) {
if (Math.abs(dy) >= scaledTouchSlop) {
if(PublicStaticClass.IsTop){//如果viewpager裡邊的scrollview在最頂部,,就讓外邊的scrollview獲取焦點,否則,讓最裡邊的scrollview獲取焦點
isIntercept = true;
}
}
}
}else{
int dy = lastY - y;
//判斷是否是向上滑動和是否在第二屏 如果是在剛到第二屏的時候,向上滑動,也讓父控制元件獲取焦點
// 在onInterceptTouchEvent()方法中,如果返回true,父控制元件攔截事件,如果返回false,則向下傳遞
if (dy < 0 && currPosition == 1) {
if (Math.abs(dy) >= scaledTouchSlop) {
if(PublicStaticClass.IsTop){
//PublicStaticClass.IsTop 判斷fragment中的scrollview時候滑動到了頂部。
//如果viewpager裡邊的scrollview在最頂部,,就讓外邊的scrollview獲取焦點,否則,讓最裡邊的scrollview獲取焦點
isIntercept = true;
}
}
}
}
break;
}
return isIntercept;
}
來看一下ViewPager中的實現:
自定義控制元件:MyScrollView.java:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;
/**
* Created by baoyunlong on 16/6/8.
*/
public class MyScrollView extends ScrollView {
private static String TAG=MyScrollView.class.getName();
public void setScrollListener(ScrollListener scrollListener) {
this.mScrollListener = scrollListener;
}
private ScrollListener mScrollListener;
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
if(mScrollListener!=null){
int contentHeight=getChildAt(0).getHeight();
int scrollHeight=getHeight();
int scrollY=getScrollY();
mScrollListener.onScroll(scrollY);
if(scrollY+scrollHeight>=contentHeight||contentHeight<=scrollHeight){
mScrollListener.onScrollToBottom();
}else {
mScrollListener.notBottom();
}
if(scrollY==0){
mScrollListener.onScrollToTop();
}
}
break;
}
boolean result=super.onTouchEvent(ev);
requestDisallowInterceptTouchEvent(false);
return result;
}
public interface ScrollListener{
void onScrollToBottom();
void onScrollToTop();
void onScroll(int scrollY);
void notBottom();
}
}
xml:
<com.qfdqc.views.pulltoloadmoreview.utils.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/oneScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/px_300"
android:text="我是第一個頁面,向上滑動一下試試1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/px_300"
android:text="我是第一個頁面,向上滑動一下試試2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/px_300"
android:text="我是第一個頁面,向上滑動一下試試3"/>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/px_300"
android:text="我是第一個頁面,向上滑動一下試試4"/>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/px_300"
android:text="我是第一個頁面,向上滑動一下試試5"/>
</LinearLayout>
</com.qfdqc.views.pulltoloadmoreview.utils.MyScrollView>
Fragment.java:
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.qfdqc.views.pulltoloadmoreview.R;
import com.qfdqc.views.pulltoloadmoreview.utils.MyScrollView;
import com.qfdqc.views.pulltoloadmoreview.utils.PublicStaticClass;
public class OneFragment extends Fragment {
View mView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.fragment_one, container, false);
initView();
return mView;
}
private void initView() {
MyScrollView oneScrollView= (MyScrollView) mView.findViewById(R.id.oneScrollview);
oneScrollView.setScrollListener(new MyScrollView.ScrollListener() {
@Override
public void onScrollToBottom() {
}
@Override
public void onScrollToTop() {
}
@Override
public void onScroll(int scrollY) {
//判斷時候滑動到了頂部
if (scrollY == 0) {
PublicStaticClass.IsTop = true;
} else {
PublicStaticClass.IsTop = false;
}
}
@Override
public void notBottom() {
}
});
}
}
6、最後來上效果圖:
7、又發現問題:
在第二頁上,左右滑動時,在水平方向上如果出現一點變化,就會滑動到第一頁上。這裡在onInterceptTouchEvent()方法中做了修改,判斷滑動的距離。如果左右滑動距離大於上下滑動距離,我們就認為使用者在左右滑動,這時候我們要讓子控制元件獲取到事件,去切換fragment。相反,如果上下滑動距離大於左右滑動距離,我們就認為使用者在上下滑動,這時候讓父控制元件攔截事件。具體程式碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
int x = (int) ev.getX();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = y;
lastX = x;
break;
case MotionEvent.ACTION_MOVE:
//判斷是否已經滾動到了底部
if (topScrollViewIsBottom) {
int dy = lastY - y;
//判斷是否是向上滑動和是否在第一屏
if (dy > 0 && currPosition == 0) {
if (dy >= scaledTouchSlop) {
isIntercept = true;//攔截事件
lastY=y;
lastX=x;
}
}
}
if (bottomScrollVIewIsInTop) {
int dy = lastY - y;
//判斷是否是向下滑動和是否在第二屏
if (dy < 0 && currPosition == 1) {
if (Math.abs(dy) >= scaledTouchSlop) {
if(PublicStaticClass.IsTop){//如果viewpager裡邊的scrollview在最頂部,,就讓外邊的scrollview獲取焦點,否則,讓最裡邊的scrollview獲取焦點
isIntercept = true;
}
}
}
}else{
int dy = lastY - y;//上下滑動的距離
int dx = lastX - x;//左右滑動的距離
//判斷是否是向上滑動和是否在第二屏 如果是在剛到第二屏的時候,向上滑動,也讓父控制元件獲取焦點
// 在onInterceptTouchEvent()方法中,如果返回true,父控制元件攔截事件,如果返回false,則向下傳遞
if (dy < 0 && currPosition == 1) {
if (Math.abs(dy) >= scaledTouchSlop) {
if(PublicStaticClass.IsTop){//如果viewpager裡邊的scrollview在最頂部,,就讓外邊的scrollview獲取焦點,否則,讓最裡邊的scrollview獲取焦點
//這裡加一個判斷,如果左右滑動的距離小於上下滑動的距離,我們認為使用者在上下滑動
//如果左右滑動的距離大於上下滑動的距離,我們認為使用者在左右滑動
//上下滑動時,讓父控制元件攔截事件
//左右滑動時,讓子控制元件攔截事件
if(Math.abs(dy)>Math.abs(dx)){//上下滑動
isIntercept = true;
}else{//左右滑動
isIntercept = false;
}
}
}
}
}
break;
}
return isIntercept;
}