android仿探探卡片右滑缩小,仿探探卡片滑动选择

探探的滑动选择妹子的功能,算是一个很经典的交互方式。自从出来以后可以说是备受关注,渐渐地很多类似功能的app也都有尝试。实现也是具有综合性的挑战,所以说网上也是有不少例子的,在这里我通过自定义ViewGroup的方式来实现。

需要达到的效果

实现的过程中,当然我们需要参考探探。这里实现最核心的功能,如下:

卡片的层叠显示

拖动选择卡片

加载数据

怎么实现呢?

当第一眼看到,察觉到的难点当然是拖动的实现。拖动的过程中会旋转,同时层叠中的view 会改变位置。如果松手还会返回原位置或者移除卡片。在自定义viewGroup中拖动事件算是很麻烦的实现。但是呢官方给我们提供一一大神器ViewDragHelper。有了它我们实现起来就事半功倍了,在这里之前也有文章介绍。如果不太明白使用,参考资料会列出来。既然拖动现在好说了。那么层叠的效果呢?这里不得不说算是核心了。在这里我也走过弯路,因为之前的实现我是想的让onlayout的时候,让子view在不同位置,并且缩放的宽高也用onLayout变更left,top,right,bottom实现。但是实践过程中会变得很复杂,不好实现。后面果断改变思路。在onLayout中对每一个view都根据它自身的已测量宽高居中显示,然后通过设置setScale,setTranslationY改变y轴防线的偏移量实现。可以看到我们是居中layout,我们事先的效果是y轴方向的偏移,所以主要看y轴的layout.这里需要琢磨一下滑动的过程中的显示,卡片的总量是固定值,我们默认设置为4,当然是可以改变的。我们可以看到探探滑动的时候,最底层的view,跟倒数第二层初始状态是叠在一起的。我们定义从最顶层为第一层,一次递增。并且每一层都有一个固定的offset,每一层都有固定的缩放scale。因为缩放也会造成y轴方向的偏移变化,这里记缩放引起的偏移scaleYOffset.所以总的totalOffset = offset + scaleYOffset.可以看到offset,scaleYOffset都跟子view所在的层次有关。接下来结合代码分析

先定义一些常量

private static final float DEFAULT_SCALE = 0.05f;//默认缩放的级别

private static final int DEFAULT_OFFSET = 10;//dp

private static final int DEFAULT_MARGIN = 10;//dp

private static final int DEFAULT_DEGRESS = 20;//旋转的度数

private static final int DEFAULT_SHOW_COUNT = 4;//默认显示数量

layout 实现

protected void onLayout(boolean changed, int l, int t, int r, int b) {

float scale = 1f;

int level = 0;

for (int i = getChildCount() - 1; i >= 0; i--) {

View child = getChildAt(i);

float scaleValue = scale - DEFAULT_SCALE * (level);

int offset = ViewExKt.dp2px(this, DEFAULT_OFFSET);

int offsetValue = offset * (level);

child.layout(mCenterX - child.getMeasuredWidth() / 2

, mCenterY - child.getMeasuredHeight() / 2

, mCenterX + child.getMeasuredWidth() / 2

, mCenterY + child.getMeasuredHeight() / 2);

float yOffset = child.getMeasuredHeight() * DEFAULT_SCALE * (level) / 2;

child.setTranslationY(yOffset + offsetValue);

child.setScaleX(scaleValue);

child.setScaleY(scaleValue);

// i > 1 是因为确保最后两个view是重叠在一起

if (i > 1 || getChildCount() < showCount) {

level++;

}

}

}

可以看到以上代码对没个子view进行遍历,同时根据每个子view的level,最顶部为0.根据level 算出拨通的offsetValue,yOffset,最终相加计算出总偏移量,scaleValue 也根据level 计算。最终判断i>1 是为了,不计算最底部level增加,让最底部view跟倒数第二个子view缩放级别一致。在layout之前肯定要先measure,这里实现比较简单,仅仅是对自view进行测量,WRAP_CONTENT状态下没有根据子view宽高,定义自身宽高,还需要改进根据子view最大宽高。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

measureChildren(widthMeasureSpec, heightMeasureSpec);

}

当我们测量,和布局之后。显示出来就已经是层叠的效果了,接下来则需要通过ViewDragHelper 对子view进行拖动及触摸反馈了。还有对数据加载的处理。

拖动的处理

可以看到使用ViewDraghelpr处理是非常方便的,每个回调方法都很清晰,方法也很实用。接下来是ViewDragHelper标准操作如下:

//接管onTneterceptTouchEvent

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

return mDragHelper.shouldInterceptTouchEvent(ev);

}

//处理onTouchEvent,核心方法,处理事件的封装都在这里了

@Override

public boolean onTouchEvent(MotionEvent event) {

mDragHelper.processTouchEvent(event);

return true;

}

//vdh的滑动采用的OverScroll 当然需要实现computeScroll

@Override

public void computeScroll() {

super.computeScroll();

if (mDragHelper.continueSettling(true)) {

postInvalidate();

}

}

回调方法,这里所有重要的操作都在这些方法里面了,特别是

tryCcaptureView,onViewReleased,onViewPositionChanged.

在拖动的过程中,始终拖动的是最顶部的view,这里怎么实现呢?,很简单,tryCaptureView指定某个view可以被拖动

public boolean tryCaptureView(View child, int pointerId) {

// 最top 的view 可滑动

return indexOfChild(child) == getChildCount() - 1;

}

现在已经可以拖动最顶部的view了,如果我们松手会停留在拖动到的位置,这里只需要调用settleCaptureViewAt,结合computeScroll 可以滑动到指定位置

if (isDraging) {

mDragHelper.settleCapturedViewAt(mCenterX - releasedChild.getMeasuredWidth() / 2

, mCenterY - releasedChild.getMeasuredHeight() / 2);

invalidate();

}

好了,现在我们具有层叠效果,并且可以拖动顶部view,并且松手会返回原位了。接下来就该拖动的时候剩下子view的变化。在拖动的过程中onViewPositionChanged会始终被调用,这里根据拖动的位置left,top,dx,dy的变化,判断出子view的变化。那么子view需要什么变化呢。通过之前onLayout的分析,可知道子view是分level的,比如倒数的二层在onlayout level是1,设定的缩放是0.9f,在这里我们需要根据顶部view的拖动使其它子view,变大或变小,也就是缩放和translationY的变化,都要结合起onLayout的时候来做。这都需要有一个变化率在[0,1]之前,这里我们通过

float rate = left * 1.0f / (getMeasuredWidth() / 3);

float a = Math.min(1, Math.max(0, Math.abs(rate)));

以上代码可以算出我们想要的比例,为什么是宽除以3,这里是我选择的当然也可以选择其他值。因为我觉得3正好。当然越大rate越大。

int offset = ViewExKt.dp2px(TinderStackLayout.this, DEFAULT_OFFSET);

// 这里为什么会有判断 i = 0,i= 1,是因为如果释放了会把view remove

// 所以这里会做判断保证布局底部的显示,从1开始最底部view 不会有变化

for (int i = getChildCount() < showCount ? 0 : 1; i < getChildCount() - 1; i++) {

View child = getChildAt(i);

// ds 代表缩放,分为两部分计算 + 号前面是布局的时候应该缩放多少,后段是跟随滑动

// 缩放的变化量

float ds = 1 - DEFAULT_SCALE * (getChildCount() - 1 - i) + DEFAULT_SCALE * a;

// 同根据布局时固定的的偏移量 - 变化量

float doffset = (getChildCount() - 1 - i) * offset - offset * a;

// 同布局时缩放的偏移量 - 变化量

float yOffset = child.getMeasuredHeight() * DEFAULT_SCALE * (getChildCount() - 1 - i - a) / 2;

child.setScaleY(ds);

child.setScaleX(ds);

child.setTranslationY(doffset + yOffset);

L.d(TAG, "ds : " + ds + " doffset : " + doffset + " a : " + a);

}

以上代码,根据onlayout的数据,和rate值的变化设置child的scale,和 translationy的变化。这里就不多解释了,代码注释相信可以理解。就是onLayout的值加上 rate的相关变化率。通过这里代码的实现我们已经可以拖动的时候实现其他子view的缩放平移变化了。会发现,可以一直拖动但是我们需要,超过一个限定值就会触发选择事件,移除view,并滑向远方。这里使用两个值判断,a.是否left超过width的三分之一,b.斜率是否超过0.15。

//斜率,有方向

float sloap = top * 1.0f / left;

斜率的计算。

判断是否是继续拖动还是触发事件

// top view 滑动的距离超过 宽度的三分之一,并且斜率 大于0.15 可以视为触发选择事件

if (Math.abs(left) > getMeasuredWidth() / 3 && Math.abs(sloap) > 0.15) {

mReleasedPoint.x = left;

mReleasedPoint.y = top;

isDraging = false;

}

在这里因为需要记录状态值,和拖动事件触发的位置,用于释放时的计算。通过isDraging,mReleasedPoint保存。接下来看onViewReleased的实现,这里是实现的事件触发的关键

if (isDraging) {

通过isDraging的判断是否停止拖动触发事件

if (mReleasedPoint.x != 0 && mReleasedPoint.y != 0) {

final float sloap = mReleasedPoint.y / (mReleasedPoint.x * 1.0f);

if (Math.abs(mReleasedPoint.x) > getMeasuredWidth() / 3 && Math.abs(sloap) > 0.15) {

mDragHelper.smoothSlideViewTo(releasedChild, getMeasuredWidth(), (int) (getMeasuredWidth() * sloap));

onChoosePick(sloap);

invalidate();

mReleasedPoint.x = 0;

mReleasedPoint.y = 0;

removeView(releasedChild);

onAddView();

}

}

通过代码判断是否触发移除和触发事件。mDraghelper.smoothSlideViewTo 把view 通过动画移到远处,并且removeView,触发onChoosePick(sloap)是左选还是右选,onAddView()添加新的view进来,如果有的话。

通过以上实现我们已经可以拖动到指定限制处释放view了。实现选择功能了。但是我们还需要旋转,这里很简单,在onViewPositionChanged里面的rate可以帮助实现,并且rate是又方向的,这可以实现左右拖动角度的变化

changedView.setRotation(rate * DEFAULT_DEGRESS);

限制基本上效果都有了,但是还有个问题,因为left不会为0,所以rate不会为0 会有偏差,所以需要监听IDLE状态,设置到0

public void onViewDragStateChanged(int state) {

super.onViewDragStateChanged(state);

// 停止滑动的时候,将最后一个view 角度设置为0,因为算斜率的

// 的方式最后滑动完成会有微小的偏差

if (state == ViewDragHelper.STATE_IDLE && isDraging) {

View childTop = getChildAt(getChildCount() - 1);

if (childTop != null) {

childTop.setRotation(0);

}

}

}

这样基本功能已经实现,但是我们需要数据还有选择的监听,这也很重要。这里采用适配器实现我们关心的只有是否添加view.还有个数。

public interface BaseCardAdapter {

int getItemCount();

View getView();

}

public interface OnChooseListener{

// 1 为右边滑动 0 为左边滑动

void onPicked(int directon);

}

这里是回调

private void onAddView() {

if (adapter != null) {

if (adapter.getView() == null) {

return;

}

addView(adapter.getView(),0);

}

}

private void onChoosePick(float sloap) {

if (chooseListener != null) {

chooseListener.onPicked(sloap > 0 ? 1 : 0);

}

}

设置adapter添加初始数据

public void setAdapter(BaseCardAdapter adapter) {

this.adapter = adapter;

if (adapter != null){

int count = Math.min(adapter.getItemCount(),showCount);

if (count <= 0) {

return ;

}

for (int i = 0 ;i < count ; i++) {

addView(adapter.getView());

}

}

}

到这里已经实现完毕,效果还不错,如果需要查看一下demo,请参考源码。

158fd88b769c

device-2019-01-05-181446.png

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值