您的位置:首页 > 移动开发 > Android开发

Android从零开搞系列:自定义View(14)仿天天美剧拖动卡片的效果(上)

2017-06-18 15:50 561 查看
转载请注意:http://blog.csdn.net/wjzj000/article/details/73432852

本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…

https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)

https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)

写在前面

一晃大三即将结束,这学期真是出奇的快。快到还没感受春天的清香,就进入了热出翔的夏天…mmp,真的热,热到翻个身一身汗….不过,好在学习使我快乐,学习使我快乐,学习使我快乐!

今天记录一篇开源项目的分析。原作者项目GitHub:https://github.com/Diolor/Swipecards

开始

开始之前让我们先瞅一下效果:



看过效果,我们来看一下项目整体,了解一下大体的结构:



我们来通过源码结构来分析一波:

红线所划掉的类是Activity,也就是这个控件的使用。因此它不属于咱们分析结构的部分。

SwipeFlingAdapterView这个类必然是我们所要使用的这个自定义View,既然结尾是AdapterView那么,这个自定义View很可能设计模式参考了同样继承了AdapterView的ListView等同类控件。

FlingCardListener必然是负责回调,这个毋庸置疑…

只是这个LinearRegression比较不好理解,那么就让咱们在分析代码的时候去好好理解一番!

使用方式

swipeCard.setAdapter(arrayAdapter);
swipeCard.setFlingListener(new SwipeFlingAdapterView.onFlingListener() {
//重写相应的回调方法
}


通过它的用法,我们可以看到和ListView的用法基本类似,和我们开头分析的一样。

那么就让我们看一看这个自定义View的setAdapter是如何处理的。

SwipeFlingAdapterView

setAdapter方法( ):

如果我们看了ListView的setAdapter方法,我们会发现二者思路基本相同,无非了考虑的情况精细与否。

@Override
public void setAdapter(Adapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
mDataSetObserver = null;
}

mAdapter = adapter;

if (mAdapter != null  && mDataSetObserver == null) {
//简单的继承DataSetObserver
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
}
}


AdapterDataSetObserver:

private class AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
requestLayout();
}

@Override
public void onInvalidated() {
requestLayout();
}

}


接下来让我们看一看比较核心的方法,重点的地方已经直接写在了注释之中。

onLayout( )方法:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mAdapter == null) {
return;
}

mInLayout = true;
final int adapterCount = mAdapter.getCount();
//如果adapter中没有对象,清空View
if(adapterCount == 0) {
removeAllViewsInLayout();
}else {
//获取最上层的Card,LAST_OBJECT_IN_STACK初始化为0
View topCard = getChildAt(LAST_OBJECT_IN_STACK);
if(mActiveCard!=null && topCard!=null && topCard==mActiveCard) {
if (this.flingCardListener.isTouching()) {
//此处通过Listener的getLastPoint()拿到Card的坐标信息
PointF lastPoint = this.flingCardListener.getLastPoint();
//第一次时mLastTouchPoint必然为null,此时进行拖动赋值
if (this.mLastTouchPoint == null || !this.mLastTouchPoint.equals(lastPoint)) {
this.mLastTouchPoint = lastPoint;
//移除第一个Card,加载下面的Card
removeViewsInLayout(0, LAST_OBJECT_IN_STACK);
//加载下面的Card ( 此方法具体内容在下面展开 )
layoutChildren(1, adapterCount);
}
}
}else{
//重置UI并设置顶视图监听器
removeAllViewsInLayout();
layoutChildren(0, adapterCount);
// ( 此方法具体内容在下面展开 )
setTopView();
}
}


layoutChildren( )方法:

private void layoutChildren(int startingIndex, int adapterCount){
while (startingIndex < Math.min(adapterCount, MAX_VISIBLE) ) {
//获取移除后的下一个Card
View newUnderChild = mAdapter.getView(startingIndex, null, this);
if (newUnderChild.getVisibility() != GONE) {
//显示移除后的下一个Card ( 此方法具体内容在下面展开 )
makeAndAddView(newUnderChild);
//LAST_OBJECT_IN_STACK赋值成当前的Card的索引
LAST_OBJECT_IN_STACK = startingIndex;
}
//自增
startingIndex++;
}
}


makeAndAddView( )方法:

private void makeAndAddView(View child) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
/**
* 将child加入到ViewGroup中
* true:则调用此方法将不会触发子对象的布局请求
*
* 想要Child被绘制出来,需要调用Child的layout()
*/
addViewInLayout(child, 0, lp, true);

//指示在下一层次布局传递期间是否请求此视图的布局
final boolean needToMeasure = child.isLayoutRequested();

//对View进行测量
if (needToMeasure) {
int childWidthSpec = getChildMeasureSpec(getWidthMeasureSpec(),
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
lp.width);
int childHeightSpec = getChildMeasureSpec(getHeightMeasureSpec(),
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin,
lp.height);
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}

int w = child.getMeasuredWidth();
int h = child.getMeasuredHeight();

int gravity = lp.gravity;

//对Gravity的情况进行处理
if (gravity == -1) {
gravity = Gravity.TOP | Gravity.START;
}

int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

int childLeft;
int childTop;

//通过不同的Gravity模式,计算的Card位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = (getWidth() + getPaddingLeft() - getPaddingRight()  - w) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.END:
childLeft = getWidth() + getPaddingRight() - w - lp.rightMargin;
break;
case Gravity.START:
default:
childLeft = getPaddingLeft() + lp.leftMargin;
break;
}

switch (verticalGravity) {
case Gravity.CENTER_VERTICAL:
childTop = (getHeight() + getPaddingTop() - getPaddingBottom()  - h) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = getHeight() - getPaddingBottom() - h - lp.bottomMargin;
break;
case Gravity.TOP:
default:
childTop = getPaddingTop() + lp.topMargin;
break;
}

//设置Card的位置,不调用此方法addViewInLayout()无效果
child.layout(childLeft, childTop, childLeft + w, childTop + h);
}


走到这,我们的这个自定义View通过setAdapter就可以让Adapter中的数据进行显示了。当然这里正常显示还是需要自定义View中的其他代码进行辅助。接下来就让我们进一步展开这个自定义View的其他内容。

setTopView()方法:

设置第一个Card的显示,并且设置监听

private void setTopView() {
if(getChildCount()>0){
//拿到第一个Card
mActiveCard = getChildAt(LAST_OBJECT_IN_STACK);
if(mActiveCard!=null) {
//移动Card的监听,具体展开在下边
flingCardListener = new FlingCardListener(mActiveCard, mAdapter.getItem(0),
ROTATION_DEGREES, new FlingCardListener.FlingListener() {

@Override
public void onCardExited() {
mActiveCard = null;
mFlingListener.removeFirstObjectInAdapter();
}
//从左边离开回调
@Override
public void leftExit(Object dataObject) {
mFlingListener.onLeftCardExit(dataObject);
}
//从右边离开回调
@Override
public void rightExit(Object dataObject) {
mFlingListener.onRightCardExit(dataObject);
}
//点击回调
@Override
public void onClick(Object dataObject) {
if(mOnItemClickListener!=null)
mOnItemClickListener.onItemClicked(0, dataObject);
}
//手指移动不松开的回调
@Override
public void onScroll(float scrollProgressPercent) {
mFlingListener.onScroll(scrollProgressPercent);
}
});
mActiveCard.setOnTouchListener(flingCardListener);
}
}
}


OK,到这里setAdapter这条线我们就已经分析出来了。但是这里仅仅是显示多个Card而已,并没有像效果当中的拖动,因此想要拖动的话,势必要存在onTouch事件。而这里我们则需要追溯到FlingCardListener这个类当中,由于篇幅比较长,关于
FlingCardListener
这个类的展开放到了下一篇博客当中。

本篇下:http://blog.csdn.net/wjzj000/article/details/73441173

尾声

希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:

https://github.com/zhiaixinyang/PersonalCollect

https://github.com/zhiaixinyang/MyFirstApp
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android github 开源