Android自定義ViewGroup實(shí)現(xiàn)淘寶商品詳情頁(yè)
最近公司在新版本上有一個(gè)需要,要在首頁(yè)添加一個(gè)滑動(dòng)效果,具體就是仿照X寶的商品詳情頁(yè),拉到頁(yè)面底部時(shí)有一個(gè)粘滯效果,如下圖X東的商品詳情頁(yè),如果用戶繼續(xù)向上拉的話就進(jìn)入商品圖文描述界面:

剛開(kāi)始是想拿來(lái)主義,直接從網(wǎng)上找個(gè)現(xiàn)成的demo來(lái)用, 但是網(wǎng)上無(wú)一例外的答案都特別統(tǒng)一: 幾乎全部是ScrollView中再套兩個(gè)ScrollView,或者是一個(gè)LinearLayout中套兩個(gè)ScrollView。 通過(guò)指定父view和子view的focus來(lái)切換滑動(dòng)的處理界面---即通過(guò)view的requestDisallowInterceptTouchEvent方法來(lái)決定是哪一個(gè)ScrollView來(lái)處理滑動(dòng)事件。
使用以上方法雖然可以解一時(shí)之渴, 但是存在幾點(diǎn)缺陷:
1 擴(kuò)展性不強(qiáng) : 如果后續(xù)產(chǎn)品要求不止是兩頁(yè)滑動(dòng)呢,是三頁(yè)滑動(dòng)呢, 難道要嵌3個(gè)ScrollView并通過(guò)N個(gè)判斷來(lái)實(shí)現(xiàn)嗎
2 兼容性不強(qiáng) : 如果需要在某一個(gè)子頁(yè)中需要處理左右滑動(dòng)事件或者雙指操作事件呢, 此方法就無(wú)法實(shí)現(xiàn)了
3 個(gè)人原因 : 個(gè)人喜歡自己掌握主動(dòng)性,事件的處理自己來(lái)控制更靠譜一些(PS:就如同一份感情一樣,需要細(xì)心去經(jīng)營(yíng))
總和以上原因, 自己實(shí)現(xiàn)了一個(gè)ViewGroup,實(shí)現(xiàn)文章開(kāi)頭提到的效果, 廢話不多說(shuō) 直接上源碼,以下只是部分主要源碼,并對(duì)每一個(gè)方法都做了注釋,可以參照注釋理解。 文章最后對(duì)這個(gè)ViewGroup加了一點(diǎn)實(shí)現(xiàn)的細(xì)節(jié)以及如何使用此VIewGroup, 以及demo地址
package com.mcoy.snapscrollview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* @author jiangxinxing---mcoy in English
*
* 了解此ViewGroup之前, 有兩點(diǎn)一定要做到心中有數(shù)
* 一個(gè)是對(duì)Scroller的使用, 另一個(gè)是對(duì)onInterceptTouchEvent和onTouchEvent要做到很熟悉
* 以下幾個(gè)網(wǎng)站可以做參考用
* http://blog.csdn.net/bigconvience/article/details/26697645
* http://blog.csdn.net/androiddevelop/article/details/8373782
* http://blog.csdn.net/xujainxing/article/details/8985063
*/
public class McoySnapPageLayout extends ViewGroup {
。。。。
public interface McoySnapPage {
/**
* 返回page根節(jié)點(diǎn)
*
* @return
*/
View getRootView();
/**
* 是否滑動(dòng)到最頂端
* 第二頁(yè)必須自己實(shí)現(xiàn)此方法,來(lái)判斷是否已經(jīng)滑動(dòng)到第二頁(yè)的頂部
* 并決定是否要繼續(xù)滑動(dòng)到第一頁(yè)
*/
boolean isAtTop();
/**
* 是否滑動(dòng)到最底部
* 第一頁(yè)必須自己實(shí)現(xiàn)此方法,來(lái)判斷是否已經(jīng)滑動(dòng)到第二頁(yè)的底部
* 并決定是否要繼續(xù)滑動(dòng)到第二頁(yè)
*/
boolean isAtBottom();
}
public interface PageSnapedListener {
/**
* @mcoy
* 當(dāng)從某一頁(yè)滑動(dòng)到另一頁(yè)完成時(shí)的回調(diào)函數(shù)
*/
void onSnapedCompleted(int derection);
}
。。。。。。
/**
* 設(shè)置上下頁(yè)面
* @param pageTop
* @param pageBottom
*/
public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) {
mPageTop = pageTop;
mPageBottom = pageBottom;
addPagesAndRefresh();
}
private void addPagesAndRefresh() {
// 設(shè)置頁(yè)面id
mPageTop.getRootView().setId(0);
mPageBottom.getRootView().setId(1);
addView(mPageTop.getRootView());
addView(mPageBottom.getRootView());
postInvalidate();
}
/**
* @mcoy add
* computeScroll方法會(huì)調(diào)用postInvalidate()方法, 而postInvalidate()方法中系統(tǒng)
* 又會(huì)調(diào)用computeScroll方法, 因此會(huì)一直在循環(huán)互相調(diào)用, 循環(huán)的終結(jié)點(diǎn)是在computeScrollOffset()
* 當(dāng)computeScrollOffset這個(gè)方法返回false時(shí),說(shuō)明已經(jīng)結(jié)束滾動(dòng)。
*
* 重要:真正的實(shí)現(xiàn)此view的滾動(dòng)是調(diào)用scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
*/
@Override
public void computeScroll() {
//先判斷mScroller滾動(dòng)是否完成
if (mScroller.computeScrollOffset()) {
if (mScroller.getCurrY() == (mScroller.getFinalY())) {
if (mNextDataIndex > mDataIndex) {
mFlipDrection = FLIP_DIRECTION_DOWN;
makePageToNext(mNextDataIndex);
} else if (mNextDataIndex < mDataIndex) {
mFlipDrection = FLIP_DIRECTION_UP;
makePageToPrev(mNextDataIndex);
}else{
mFlipDrection = FLIP_DIRECTION_CUR;
}
if(mPageSnapedListener != null){
mPageSnapedListener.onSnapedCompleted(mFlipDrection);
}
}
//這里調(diào)用View的scrollTo()完成實(shí)際的滾動(dòng)
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//必須調(diào)用該方法,否則不一定能看到滾動(dòng)效果
postInvalidate();
}
}
private void makePageToNext(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
private void makePageToPrev(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
public int getCurrentScreen() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return i;
}
}
return mCurrentScreen;
}
public View getCurrentView() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return getChildAt(i);
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
* 重寫了父類的onInterceptTouchEvent(),主要功能是在onTouchEvent()方法之前處理
* touch事件。包括:down、up、move事件。
* 當(dāng)onInterceptTouchEvent()返回true時(shí)進(jìn)入onTouchEvent()。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
// 記錄y與mLastMotionY差值的絕對(duì)值。
// yDiff大于gapBetweenTopAndBottom時(shí)就認(rèn)為界面拖動(dòng)了足夠大的距離,屏幕就可以移動(dòng)了。
final int yDiff = (int)(y - mLastMotionY);
boolean yMoved = Math.abs(yDiff) > gapBetweenTopAndBottom;
if (yMoved) {
if(MCOY_DEBUG) {
Log.e(TAG, "yDiff is " + yDiff);
Log.e(TAG, "mPageTop.isFlipToBottom() is " + mPageTop.isAtBottom());
Log.e(TAG, "mCurrentScreen is " + mCurrentScreen);
Log.e(TAG, "mPageBottom.isFlipToTop() is " + mPageBottom.isAtTop());
}
if(yDiff < 0 && mPageTop.isAtBottom() && mCurrentScreen == 0
|| yDiff > 0 && mPageBottom.isAtTop() && mCurrentScreen == 1){
Log.e("mcoy", "121212121212121212121212");
mTouchState = TOUCH_STATE_SCROLLING;
}
}
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
mLastMotionY = y;
Log.e("mcoy", "mScroller.isFinished() is " + mScroller.isFinished());
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Release the drag
mTouchState = TOUCH_STATE_REST;
break;
}
boolean intercept = mTouchState != TOUCH_STATE_REST;
Log.e("mcoy", "McoySnapPageLayout---onInterceptTouchEvent return " + intercept);
return intercept;
}
/*
* (non-Javadoc)
*
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
* 主要功能是處理onInterceptTouchEvent()返回值為true時(shí)傳遞過(guò)來(lái)的touch事件
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("mcoy", "onTouchEvent--" + System.currentTimeMillis());
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if(mTouchState != TOUCH_STATE_SCROLLING){
// 記錄y與mLastMotionY差值的絕對(duì)值。
// yDiff大于gapBetweenTopAndBottom時(shí)就認(rèn)為界面拖動(dòng)了足夠大的距離,屏幕就可以移動(dòng)了。
final int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > gapBetweenTopAndBottom;
if (yMoved) {
mTouchState = TOUCH_STATE_SCROLLING;
}
}
// 手指拖動(dòng)屏幕的處理
if ((mTouchState == TOUCH_STATE_SCROLLING)) {
// Scroll to follow the motion event
final int deltaY = (int) (mLastMotionY - y);
mLastMotionY = y;
final int scrollY = getScrollY();
if(mCurrentScreen == 0){//顯示第一頁(yè),只能上拉時(shí)使用
if(mPageTop != null && mPageTop.isAtBottom()){
scrollBy(0, Math.max(-1 * scrollY, deltaY));
}
}else{
if(mPageBottom != null && mPageBottom.isAtTop()){
scrollBy(0, deltaY);
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 彈起手指后,切換屏幕的處理
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) velocityTracker.getYVelocity();
if (Math.abs(velocityY) > SNAP_VELOCITY) {
if( velocityY > 0 && mCurrentScreen == 1 && mPageBottom.isAtTop()){
snapToScreen(mDataIndex-1);
}else if(velocityY < 0 && mCurrentScreen == 0){
snapToScreen(mDataIndex+1);
}else{
snapToScreen(mDataIndex);
}
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}else{
}
mTouchState = TOUCH_STATE_REST;
break;
default:
break;
}
return true;
}
private void clearOnTouchEvents(){
mTouchState = TOUCH_STATE_REST;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void snapToDestination() {
// 計(jì)算應(yīng)該去哪個(gè)屏
final int flipHeight = getHeight() / 8;
int whichScreen = -1;
final int topEdge = getCurrentView().getTop();
if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){
//向下滑動(dòng)
whichScreen = mDataIndex + 1;
}else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){
//向上滑動(dòng)
whichScreen = mDataIndex - 1;
}else{
whichScreen = mDataIndex;
}
Log.e(TAG, "snapToDestination mDataIndex = " + mDataIndex);
Log.e(TAG, "snapToDestination whichScreen = " + whichScreen);
snapToScreen(whichScreen);
}
private void snapToScreen(int dataIndex) {
if (!mScroller.isFinished())
return;
final int direction = dataIndex - mDataIndex;
mNextDataIndex = dataIndex;
boolean changingScreens = dataIndex != mDataIndex;
View focusedChild = getFocusedChild();
if (focusedChild != null && changingScreens) {
focusedChild.clearFocus();
}
//在這里判斷是否已到目標(biāo)位置~
int newY = 0;
switch (direction) {
case 1: //需要滑動(dòng)到第二頁(yè)
Log.e(TAG, "the direction is 1");
newY = getCurrentView().getBottom(); // 最終停留的位置
break;
case -1: //需要滑動(dòng)到第一頁(yè)
Log.e(TAG, "the direction is -1");
Log.e(TAG, "getCurrentView().getTop() is "
+ getCurrentView().getTop() + " getHeight() is "
+ getHeight());
newY = getCurrentView().getTop() - getHeight(); // 最終停留的位置
break;
case 0: //滑動(dòng)距離不夠, 因此不造成換頁(yè),回到滑動(dòng)之前的位置
Log.e(TAG, "the direction is 0");
newY = getCurrentView().getTop(); //第一頁(yè)的top是0, 第二頁(yè)的top應(yīng)該是第一頁(yè)的高度
break;
default:
break;
}
final int cy = getScrollY(); // 啟動(dòng)的位置
Log.e(TAG, "the newY is " + newY + " cy is " + cy);
final int delta = newY - cy; // 滑動(dòng)的距離,正值是往左滑<—,負(fù)值是往右滑—>
mScroller.startScroll(0, cy, 0, delta, Math.abs(delta));
invalidate();
}
}
McoySnapPage是定義在VIewGroup的一個(gè)接口, 比如說(shuō)我們需要類似某東商品詳情那樣,有上下兩頁(yè)的效果。 那我就需要自己定義兩個(gè)類實(shí)現(xiàn)這個(gè)接口,并實(shí)現(xiàn)接口的方法。getRootView需要返回當(dāng)前頁(yè)需要顯示的布局內(nèi)容;isAtTop需要返回當(dāng)前頁(yè)是否已經(jīng)在頂端; isAtBottom需要返回當(dāng)前頁(yè)是否已經(jīng)在底部
onInterceptTouchEvent和onTouchEvent決定當(dāng)前的滑動(dòng)狀態(tài), 并決定是有當(dāng)前VIewGroup攔截touch事件還是由子view去消費(fèi)touch事件
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android 自動(dòng)化測(cè)試經(jīng)驗(yàn)分享 UiObejct.getFromParent()的使用方法
本篇文章對(duì)Android中UiObejct.getFromParent()的使用進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05
關(guān)于androidstuio導(dǎo)入系統(tǒng)源碼的問(wèn)題
小編最近在做系統(tǒng)源碼導(dǎo)出來(lái)的小項(xiàng)目,在導(dǎo)入androidstudio過(guò)程中遇到過(guò)一些問(wèn)題,本文以Schedule power on off為例給大家詳細(xì)介紹,需要的朋友參考下吧2021-06-06
簡(jiǎn)單實(shí)現(xiàn)Android讀取網(wǎng)絡(luò)圖片到本地
這篇文章主要為大家詳細(xì)介紹了如何簡(jiǎn)單實(shí)現(xiàn)Android讀取網(wǎng)絡(luò)圖片到本地的方法,感興趣的小伙伴們可以參考一下2016-08-08
Android倒計(jì)時(shí)控件 Splash界面5秒自動(dòng)跳轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Android倒計(jì)時(shí)控件,Splash界面5秒自動(dòng)跳轉(zhuǎn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
Android 接收推送消息跳轉(zhuǎn)到指定頁(yè)面的方法
這篇文章主要介紹了Android 接收推送消息跳轉(zhuǎn)到指定頁(yè)面的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
Kotlin 創(chuàng)建接口或者抽象類的匿名對(duì)象實(shí)例
這篇文章主要介紹了Kotlin 創(chuàng)建接口或者抽象類的匿名對(duì)象實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03
Android Listview 滑動(dòng)過(guò)程中提示圖片重復(fù)錯(cuò)亂的原因及解決方法
android中l(wèi)istview是比較常見(jiàn)的組件,通過(guò)本文主要給大家分析Android中Listview滾動(dòng)過(guò)程造成的圖片顯示重復(fù)、錯(cuò)亂、閃爍的原因及解決方法,順便跟進(jìn)Listview的緩存機(jī)制,感興趣的朋友一起看下吧2016-08-08
Android使用ViewPager實(shí)現(xiàn)導(dǎo)航
本文主要介紹了Android使用ViewPager實(shí)現(xiàn)導(dǎo)航的方法代碼。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-03-03
Android編程實(shí)現(xiàn)屏幕自適應(yīng)方向尺寸與分辨率的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)屏幕自適應(yīng)方向尺寸與分辨率的方法,涉及Android屏幕分辨率、布局、橫豎屏切換等相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-12-12

