重寫ViewGroup並藉助ViewDragHelper實現各種拖拽互動效果(二)
本文是《重寫ViewGroup並藉助ViewDragHelper實現各種拖拽互動效果(一)》http://blog.csdn.net/lin_dianwei/article/details/79166466 的延續,針對(一)中存在的問題繼續優化。
一、前面SlideUpLayout控制元件在使用時,如果包含有ListView或RecyclerView等列表的時候,可能存在這樣的問題,直接看圖:
其中xml檔案如下:
<com.dway.testwork.viewdrag.SlideUpLayout
android:id="@+id/slide_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<FrameLayout
android:id="@+id/slide_layout_up"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1f00ffff">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="up"/>
</FrameLayout>
<FrameLayout
android:id="@+id/slide_layout_down"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1fffff00">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="down"/>
<ListView
android:id="@+id/slide_layout_down_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<FrameLayout
android:id="@+id/slide_layout_slide"
android:layout_width="match_parent"
android:layout_height="100dp"
android:clickable="true"
android:background="#1fff00ff">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="slide"/>
</FrameLayout>
</com.dway.testwork.viewdrag.SlideUpLayout>
二、原因經分析是事件分派攔截存在著問題,還記得上一篇文章中,事件的攔截是完全交給ViewDragHelper處理的,如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 把事件處理交給ViewDragHelper
return mHelper.shouldInterceptTouchEvent(ev);
}
因為把攔截交給了ViewDragHelper,那麼包含的ListView等就無法消費事件,從而無法正常滾動。所以解決方法就是在這個方法中先對ListView進行判斷看是否需要消費,需要消費事件則把事件交給ListView處理。
三、此處穿插科普下事件分派攔截的原理:(此處是Copy過來的,放這裡幫助理解)
dispatchTouchEvent
用於touch事件的分發;通俗點說,就是決定當前這個touch事件應該交給誰來處理(是當前View還是父View)
當觸控事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會從根元素依次往下傳遞直到最內層子元素或在中間某一元素中事件被攔截或者消費.
如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;這樣該View的onTouchEvent事件也不會得到響應. 如果return false,會將事件返回給父 View 的 onTouchEvent 進行消費。 如果 return super.dispatchTouchEvent(ev),事件會分發給當前 View 的 onInterceptTouchEvent 方法去進行處理。
onInterceptTouchEvent
用於touch事件的攔截;通俗點說,就是決定剛剛 dispatchTouchEvent 拋給我的touch事件應該交給誰來處理(是當前View還是子View)注意跟dispatchTouchEvent的區別
如果 return true,則將事件進行攔截,並將攔截到的事件交由該 View 的 onTouchEvent 進行處理; 如果 return false,則將事件向子View傳遞,再由子View的 dispatchTouchEvent來對這個事件處理; 如果 return super.onInterceptTouchEvent(ev),事件會被攔截,並將事件交由該 View 的 onTouchEvent 進行處理。
onTouchEvent
用於touch事件的處理;通俗點說,就是決定剛剛 onInterceptTouchEvent 拋給我的touch事件進行處理。
如果return false,那麼這個事件會從該 View 向父View傳遞,父 View 的 onTouchEvent 來接收,而且如果父View也是return false,那事件也會向上傳遞由onTouchEvent接收處理. 如果retrun true, 則會接收並消費該事件。 如果retrun super.onTouchEvent(ev) 和返回 false 時相同。
四、有了思路,那麼解決方法就是在攔截事件之前,先判斷下ListView是否需要消費事件,即是否還能繼續往下滾動,即是否已經到頂部了。所以優化後的SlideUpLayout如下:
package com.dway.testwork.viewdrag;
import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* 可上下切換,類似縱向的ViewPager,並且上劃時向下彈出選單效果
* Created by dway on 2018/1/23.
*/
public class SlideUpLayout extends ViewGroup {
private View mUpView;
private View mDownView;
private View mSlideView;
//private RecyclerView mListView;
private ViewDragHelper mHelper;
//上下滑的程度,0表示在upView,1表示在downView
private float mSlidePercent = 0;
private boolean mInLayout = false;
private boolean mFirstLayout = true;
public SlideUpLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if(child == mUpView){
return Math.max(- mUpView.getMeasuredHeight() + mSlideView.getMeasuredHeight(), Math.min(top, 0));
}else if(child == mDownView){
return Math.max(mSlideView.getMeasuredHeight(), Math.min(top, mUpView.getMeasuredHeight()));
}else if(child == mSlideView){
return Math.max(- mSlideView.getMeasuredHeight(), Math.min(top, 0));
}
return 0;
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mUpView || child == mDownView;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(releasedChild == mUpView){
int upViewHeight = mUpView.getMeasuredHeight();
int slideViewHeight = mSlideView.getMeasuredHeight();
float offset = (upViewHeight + releasedChild.getTop() - slideViewHeight) * 1.0f / (upViewHeight - slideViewHeight);
mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? 0 : -upViewHeight + slideViewHeight);
invalidate();
}else if(releasedChild == mDownView){
int downViewHeight = mDownView.getMeasuredHeight();
int slideViewHeight = mSlideView.getMeasuredHeight();
float offset = (releasedChild.getTop() - slideViewHeight) * 1.0f / downViewHeight;
mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? mUpView.getMeasuredHeight() : slideViewHeight);
invalidate();
}
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView == mUpView){
mDownView.setTop(top + mUpView.getMeasuredHeight());
mSlidePercent = (float) (-top) / mDownView.getMeasuredHeight();
if(mSlidePercent > 0.9f){
mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
}else{
mSlideView.setTop(-mSlideView.getMeasuredHeight());
}
requestLayout();
if(mOnSlideListener != null){
mOnSlideListener.onSlide(mSlidePercent);
}
}else if(changedView == mDownView){
mUpView.setTop(top - mUpView.getMeasuredHeight());
mSlidePercent = (float) (mUpView.getMeasuredHeight() - top) / mDownView.getMeasuredHeight();
if(mSlidePercent > 0.9f){
mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
}else{
mSlideView.setTop(-mSlideView.getMeasuredHeight());
}
requestLayout();
if(mOnSlideListener != null){
mOnSlideListener.onSlide(mSlidePercent);
}
}
}
@Override
public int getViewVerticalDragRange(View child) {
return child == mUpView ? mUpView.getMeasuredHeight() - mSlideView.getMeasuredHeight() :
child == mDownView ? mDownView.getMeasuredHeight() :
child == mSlideView ? mSlideView.getMeasuredHeight() : 0;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
mUpView = getChildAt(0);
mDownView = getChildAt(1);
mSlideView = getChildAt(2);
//mListView = mDownView.findViewById(R.id.rv_select_course);
//up
MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
int widthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
mUpView.measure(widthSpec, heightSpec);
//slide
lp = (MarginLayoutParams) mSlideView.getLayoutParams();
widthSpec = getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin, lp.width);
heightSpec = getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin, lp.height);
mSlideView.measure(widthSpec, heightSpec);
//down
lp = (MarginLayoutParams) mDownView.getLayoutParams();
widthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
heightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin - mSlideView.getMeasuredHeight(), MeasureSpec.EXACTLY);
mDownView.measure(widthSpec, heightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
int upTop;
if(mFirstLayout){
upTop = lp.topMargin;
}else{
upTop = mUpView.getTop();
}
mUpView.layout(lp.leftMargin, upTop,
lp.leftMargin + mUpView.getMeasuredWidth(),
upTop + mUpView.getMeasuredHeight());
lp = (MarginLayoutParams) mDownView.getLayoutParams();
int downTop;
if(mFirstLayout){
downTop = mUpView.getMeasuredHeight() + lp.topMargin;
}else{
downTop = mDownView.getTop();
}
mDownView.layout(lp.leftMargin, downTop,
lp.leftMargin + mDownView.getMeasuredWidth(),
downTop + mDownView.getMeasuredHeight());
lp = (MarginLayoutParams) mSlideView.getLayoutParams();
int slideTop;
if(mFirstLayout){
slideTop = - mSlideView.getMeasuredHeight() + lp.topMargin;
}else{
slideTop = mSlideView.getTop();
}
mSlideView.layout(lp.leftMargin, slideTop,
lp.leftMargin + mSlideView.getMeasuredWidth(),
slideTop + mSlideView.getMeasuredHeight());
mInLayout = false;
mFirstLayout = false;
}
@Override
public void requestLayout() {
if(!mInLayout) {
super.requestLayout();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//return mHelper.shouldInterceptTouchEvent(ev);
//方法一:還需要根據座標是否是mListView的範圍,進行控制。該方式更直接,理論上效率會更高
/*if(isInViewArea(mListView, ev.getRawX(), ev.getRawY()) && mListView.canScrollVertically(-1)){
//listview可以滾動則交給子view決定
return false;
}else{
//已經滾動到頂部,則交給ViewDragHelper處理
return mHelper.shouldInterceptTouchEvent(ev);
}*/
//方法二:把以上的處理改為更一般的情況,遞迴獲取每個子view進行判斷處理
if(hasViewCanScrollUp(mDownView, ev.getRawX(), ev.getRawY())){
return false;
}else{
return mHelper.shouldInterceptTouchEvent(ev);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
if (mHelper.continueSettling(true)) {
invalidate();
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
/**
* 判斷指定點所在的view是否未到頂可以繼續上劃,如果view為ViewGroup的話還得遞迴往子view判斷進去
*/
private boolean hasViewCanScrollUp(View view, float x, float y){
if(view instanceof ViewGroup){
ViewGroup viewGroup = (ViewGroup) view;
for(int i=0; i<viewGroup.getChildCount(); i++){
View child = viewGroup.getChildAt(i);
if(hasViewCanScrollUp(child, x, y)){
return true;
}
}
return isInViewArea(view, x, y) && view.canScrollVertically(-1);
}else{
return isInViewArea(view, x, y) && view.canScrollVertically(-1);
}
}
/**
* 判斷座標是否在view區域內
*/
private boolean isInViewArea(View view, float x, float y){
int[] local = new int[2];
view.getLocationOnScreen(local);
return x > local[0] && x < local[0]+view.getMeasuredWidth() && y > local[1] && y < local[1]+view.getMeasuredHeight();
}
public float getSlidePercent(){
return mSlidePercent;
}
public boolean isSlideUp(){
return floatCompare(mSlidePercent, 0);
}
public boolean isSlideDown(){
return floatCompare(mSlidePercent, 1);
}
public void slideToDown(){
mHelper.smoothSlideViewTo(mDownView, mDownView.getLeft(), mSlideView.getMeasuredHeight());
invalidate();
}
public void slideToUp(){
mHelper.smoothSlideViewTo(mUpView, mUpView.getLeft(), 0);
invalidate();
}
private OnSlideListener mOnSlideListener = null;
/**
* 外部可設定監聽slide的位置回撥
*/
public void setOnSlideListener(OnSlideListener listener) {
mOnSlideListener = listener;
}
public interface OnSlideListener{
/**
* slide回撥
* @param percent 取值區間[0, 1],0代表滑到最頂部,1代表滑到最底部
*/
void onSlide(float percent);
}
private boolean floatCompare(float f1, float f2){
return Math.abs(f1 - f2) < Float.MIN_VALUE;
}
}
註釋都在程式碼中,有兩種方法,一種比較直接,直接判斷ListView。第二種是把方法一般化,遞迴判斷每一個子view。注意其中view位置的判斷和view是否能滾動的判斷方法。另外程式碼也新增了滾動的監聽。
五、最終效果圖:完美
相關文章
- 藉助HTML5details,summary無JS實現各種互動效果HTMLAIJS
- 利用HTML5,無JS實現各種互動效果HTMLJS
- Flutter互動實戰-即刻App探索頁下拉&拖拽效果FlutterAPP
- Android ViewDragHelper 自定義 ViewGroup 神器AndroidView
- Android ViewDragHelper完全解析 自定義ViewGroup神器AndroidView
- 使用BottomSheetBehavior實現美團拖拽效果
- JavaScript使網頁顯示動態效果並實現與使用者互動功能。JavaScript網頁
- div實現拖拽效果,同時包含iframe
- iOS後臺模式藉助位置更新實現iOS模式
- 【新手指南】App原型設計:如何快速實現這6種互動效果?APP原型
- 互動投影的幾種實現方式
- 藉助 Webpack 靜態分析能力實現程式碼動態載入Web
- 「譯」如何實現互動式 WebGL 懸停效果Web
- Android一種翻板式互動效果Android
- 藉助XPopup,用50行程式碼實現更好的抖音評論彈窗效果!行程
- 藉助 :has 實現3d輪播圖3D
- 《NieR: Automata》的空間聲學設計以及藉助Wwise實現對多種遊戲玩法的支援(二)遊戲
- 短影片app原始碼,藉助輪詢最佳化互動體驗APP原始碼
- 藉助 CSS Colorguard 避免使用重複顏色CSS
- UIButton實現各種圖文結合的效果以及原理UI
- python藉助web3py與以太坊區塊鏈節點互動的幾種方式PythonWeb區塊鏈
- 企業展廳互動能實現什麼效果
- Qt實現遮罩效果並可以拖動伸縮QT遮罩
- 資料驅動模式藉助react的實踐探索模式React
- echart 各種圖實現
- Java 藉助ImageMagic實現圖片編輯服務Java
- python與mysql互動中的各種坑PythonMySql
- Python(一)Android藉助Python實現打包自動上傳firPythonAndroid
- Android 的滑動分析以及各種實現Android
- 各種並查集並查集
- 互動滑軌屏的幾種實現形式
- 大屏報表元件間的聯動互動效果實現方法元件
- javascript實現拖拽並替換網頁塊元素JavaScript網頁
- 實現QQ的TabBar拖拽動效tabBar
- 短視訊軟體開發,RecyclerView實現拖拽效果View
- 動畫-CAShapeLayer實現QQ訊息紅點拖拽效果動畫
- Android中ScrollView實現拖拽反彈效果動畫AndroidView動畫
- 食品企業如何藉助SAP實現高效智慧化管理