热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

关于CoordinatorLayoutAppBarLayout原理的一些分析

这几天学了一些CoordinatorLayout、AppBarLayout配合使用的一些方法,之前还写了一篇CoordinatorLayoutBehavior一些笔记,通过这几天对

这几天学了一些CoordinatorLayout、AppBarLayout配合使用的一些方法,之前还写了一篇CoordinatorLayout Behavior一些笔记,通过这几天对源码的阅读,现在对CoordinatorLayout、AppBarLayout这部分的内容有了更深一层的理解,接下来我就把我所理解的源码简单的分析一下。

一、 NestedScrolling机制

CoordinatorLayout、AppBarLayout分别实现了NestedScrolling机制中需要的接口和接口中的一些方法,如果大家对NestedScrolling不是很了解,可以先去网上了解一下,这里我简单说明一下这个机制的原理:Nested这个单词的意思是“嵌套”,这个机制其实就是嵌套滑动的一种处理机制,它和之前只能单一View消耗滑动事件的处理机制不同,它会在子View处理滑动事件时,先将滑动事件传递到父View中,询问父View是否需要消耗滑动事件,如果父View需要消耗滑动事件,子View会将此次x,y滑动的距离先传递到父View中,父View会先消耗滑动事件,如果父View没消耗全部的滑动距离,子View会消耗剩余的滑动距离,如果剩余的滑动距离大于子View剩余需要的滑动距离(例如RecyclerView距离自身Content滑动到顶部的距离只有10,但是此次滑动距离dy有50,父View消耗了30,剩余20大于RecyclerView剩余需要滑动的距离),子View会把剩下的滑动距离再次传递给父View,由父View去消耗。
我推荐两篇我觉得还挺不错的文章可以帮助理解这个机制:Android NestedScrolling机制完全解析 带你玩转嵌套滑动和android NestedScroll嵌套滑动机制完全解析-原来如此简单

二、可以实现的效果

说了这么多,这个机制到底可以实现什么样的效果呢,其实就是滑动起来非常的顺滑,例如,我在界面中放了一个RecyclerView,RecyclerView上面放了一个AppBarLayout包裹的ImageView,当我滑动这个界面时,不会像原来那种机制需要在RecyclerView滑动到顶部时,需要抬起手指进行下次滑动才能把RecyclerView上面的View滑出屏幕以外,效果图如下:

《关于CoordinatorLayout AppBarLayout原理的一些分析》 效果图

三、原理分析

下面开始进行我对源码阅读的分析理解,这里主要分成两个部分,主要是RecyclerView、CoordinatorLayout 、AppBarLayout如何实现了NestedScrolling机制。
先简要概括一下总体的中心思想,根据上文对NestedScrolling的介绍,这里的RecyclerView就是子View,CoordinatorLayout就是父View,AppBarLayout是父View在判断是否消耗事件,在判断方法中主要依据的View。主要的过程都是在RecyclerView的onTouchEvent中,分别在Down和Move事件中完成了整个机制的流程。这里说一句题外话,为什么ListView、GirdView不能实现这种效果?因为这两个View并没有实现NestedScrolling机制中相关的方法,可以看一下RecyclerView源码,我们会发现RecycerView定义如下:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild

1.RecyclerView中MotionEvent.ACTION_DOWN做了哪些事儿?

这里我先来一张流程图:

《关于CoordinatorLayout AppBarLayout原理的一些分析》 ActionDown.png

这里所做的一件事儿,就是子View在滑动事件开始时,传递给父View,父View会去判断是否需要消耗此次事件,下面就是源码的分析

//RecyclerView
@Override
public boolean onTouchEvent(MotionEvent e){
//.................
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//调用NestedScrollingChildHelper
startNestedScroll(nestedScrollAxis);
} break;
//..........
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}

上面的startNestedScroll方法就会调用到NestedScrollingChildHelper中的startNestedScroll方法。Helper中该方法的实现如下:

//NestedScrollingChildHelper
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
//通过while循环,不断的去判断是否有View的ParentView需要消耗这次滑动事件
while (p != null) {
//判断parent是否需要消耗
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
//父View消耗滑动事件
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
//循环结束,没有发现需要消耗的View
return false;
}

在这个方法中,所做的事儿只有一件,去循环遍历并且询问这个View的ParentView和ParentView的ParentView是否需要消耗这次事件,如果有消耗的返回true否则返回false,这里判断的方法使用了ViewParentCompat.onStartNestedScroll(p, child, mView, axes),这个方法实现很简单,里面仅仅是调用了我们传入的参数p的onStartNestedScroll方法,在我的事例中,p就是CoordinatorLayout,所以我们可以直接查看CoordinatorLayout中onStartNestedScroll方法的实现

//CoordinatorLayout
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
//仍然是遍历子View,判断是否有View需要消耗
for (int i = 0; i final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
//判断behavior是否为空
if (viewBehavior != null) {
//获取View是否消耗滑动事件
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}

这里我们可以看到,当父View也就是CoordinatorLayout判断是否消耗滑动事件的方式也很简单,就是遍历自己的子View,如果子View有消耗就返回true,这里使用的是 “|=” 只要有子View需要接收便是true,接着在当前例子中,ImageView包裹在AppBarLayout,那么在这个函数遍历中,就会获取到AppBarLayout的Behavior,并且调用AppBarLayout的中Behavior的onStartNestedScroll方法,就是上面时序图的最后一个LifeLine,AppBarLayout的中Behavior的onStartNestedScroll实现如下:

//AppBarLayout$Behavior
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
if (started && mOffsetAnimator != null) {
// Cancel any offset animation
mOffsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
mLastNestedScrollingChildRef = null;
return started;
}

这里可以看到,在AppBarLayout$Behavior这个类的onStartNestedScroll,会根据当前的nestedScrollAxes和自身的一些条件判断是否需要消耗这次滑动事件,这里也插一句题外话,我之前疑惑了半天,我发现CoordinatorLayout并没有给AppBarLayout在哪里设置了AppBarLayout$Behavior,我在CoordinatorLayout代码中并没有找到,后来忽然发现,原来是用了注解的形式在AppBarLayout开头声明了

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout

这样到此为止,我们第一个阶段的分析就完成了,当RecyclerView发生了MotionEvent.ACTION_DOWN事件时,经历了NestedScrollingChildHelper->ViewParentCompat->CoordinatorLayout->AppBarLayout来完成NestedScrolling机制中的第一步,子View在滑动事件发生时,告知父View是否需要消耗事件

2.RecyclerView中MotionEvent.ACTION_MOVE做了哪些事儿?

同样这里也首先来一张流程图:

《关于CoordinatorLayout AppBarLayout原理的一些分析》 ActionMove.png

这里做的事儿,就是在父View需要处理滑动事件时,先将滑动事件传递到父View,然后拿到剩下未消耗的距离自己消耗,如果在自己消耗后还有剩余,那么在传递给父View,下面开始一步一步的分析源码

//RecyclerView
@Override
public boolean onTouchEvent(MotionEvent e) {
//...............
switch (action) {
//..................
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index <0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//调用dispatchNestedPreScroll方法并且mScrollConsumed数组记录消耗
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
//......................
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//scrollByInternal传递剩余消耗
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
//.........................
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}

这里首先是在子View消耗事件之前,通过调用dispatchNestedPreScroll方法,如果父View消耗事件,则子View的dx,dy会减去已经消耗掉的,dispatchNestedPreScroll主要调用了NestedScrollingChildHelper的dispatchNestedPreScroll方法,我们看一下实现

//NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (cOnsumed== null) {
if (mTempNestedScrollCOnsumed== null) {
mTempNestedScrollCOnsumed= new int[2];
}
cOnsumed= mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//这里调用了onNestedPreScroll方法询问父View是否消耗
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}

这里依然使用了ViewParentCompat的方法,ViewParentCompat.onNestedPreScroll方法依然是调用我们传入的父View的onNestedPreScroll方法,这里我的父View依然还是CoordinatorLayout,这里没用循环遍历,是因为之前我们已经在ViewParentCompat.startNestedScroll遍历中保存了mNestedScrollingParent为CoordinatorLayout,所以我们下一步可以直接查看CoordinatorLayout的onNestedPreScroll

//CoordinatorLayout
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xCOnsumed= 0;
int yCOnsumed= 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
//传递给子View进行消耗
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xCOnsumed= dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yCOnsumed= dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}

可以看到,CoordinatorLayout对于是否消耗事件,依然是传递给子View去消耗,在我们例子中的这个布局下能够消耗掉这个事件的View就是AppBarLayout,这样事件就又传递给了CoordinatorLayout的子View去消耗,消耗完了以后,可以看到下面还调用了onChildViewsChanged这方法,这个方法的作用是做一些和Behavior相关的操作,有关这部分内容可以看我的上篇文章CoordinatorLayout Behavior一些笔记,这样对于viewBehavior.onNestedPreScroll这里,我们需要查看AppBarLayout$Behavior中的实现:

//AppBarLayout$Behavior
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
if (dy != 0 && !mSkipNestedPreScroll) {
int min, max;
if (dy <0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
//AppbarLayout只消耗dy的事件,将消耗的事件赋值给consumed[1]并且scroll自身内容
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}

到这里,如果AppBarLayout需要消耗滑动事件的话,就会消耗并且滚动自己的内容。大家有没有好奇一点,整个过程并没有返回值,那么RecyclerView是如何通过一大堆调用拿到AppBarLayout的消耗呢?其实很简单,就是Java中传递数组时,和C++中按值传递不一样,Java中非基本类型的传递类似于C++中按引用传递,还记得我们RecyclerView中调用dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)这个方法么?这里mScrollConsumed数组就是传递到onNestedPreScroll中的 int[] consumed,所以只要给 int[] consumed赋值,就可以在RecyclerView拿到消耗的dx,dy,分别对应mScrollConsumed[0]和mScrollConsumed[1],接着在AppBarLayout处理完以后,我们还是看上面的时序图,会发现,NestedScrollingChildHelper中是需要有返回值的,需要RecyclerView判断父View是否消耗了滑动事件,我们可以看上面NestedScrollingChildHelper的dispatchNestedPreScroll方法中在子View处理消耗事件后,会return consumed[0] != 0 || consumed[1] != 0;

// NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//........................
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}

这样这个父View的消耗就在子View滑动之前完成了,接着就是子View的滑动,并且如果还有没消耗完的滑动距离会传递给父View让父View去处理,这里的过程主要就是在RecyclerView的scrollByInternal方法中了:

//RecyclerView
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int uncOnsumedX= 0, uncOnsumedY= 0;
int cOnsumedX= 0, cOnsumedY= 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
cOnsumedX= mLayout.scrollHorizontallyBy(x, mRecycler, mState);
uncOnsumedX= x - consumedX;
}
if (y != 0) {
cOnsumedY= mLayout.scrollVerticallyBy(y, mRecycler, mState);
uncOnsumedY= y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
//dispatchNestedScroll方法传递consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset给父View
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}

scrollByInternal在onTouchEvent中会被调用,scrollByInternal通过调用dispatchNestedScroll把事件传递给父View,其实仍然是调用了NestedScrollingChildHelper的dispatchNestedScroll,该方法的实现:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}

ViewParentCompat.onNestedScroll方法依然是调用了CoordinatorLayout的onNestedScroll方法,实现如下:

//CoordinatorLayout
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}

这也依然一样,CoordinatorLayout会循环遍历,交给子View去处理,这里仍然还是AppBarLayout$Behavior的onNestedScroll:

//AppBarLayout$Behavior
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed <0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
scroll(coordinatorLayout, child, dyUnconsumed,
-child.getDownNestedScrollRange(), 0);
// Set the expanding flag so that onNestedPreScroll doesn't handle any events
mSkipNestedPreScroll = true;
} else {
// As we're no longer handling nested scrolls, reset the skip flag
mSkipNestedPreScroll = false;
}
}

到这未消耗的事件就又传递到AppBarLayout了,这里的注释很清晰:If the scrolling view is scrolling down but not consuming, it&#8217;s probably be at the top of it&#8217;s content,翻译一下,就是如果scrolling view 向下滚动,但是没有消耗滚动事件,可能是已经滑倒了顶部,例如RecyclerView已经滑倒了第一个Item,然后AppBarLayout就会消耗剩余的事件在scroll方法中
到此,整个流程就已经清晰明了了,整个Scrolling机制在CoordinatorLayout AppBarLayout中就是这么实现的,整体的流程的概览就可以参照上面的两个时序图,对整个源码的分析和理解后,以后在使用起来我们就会更加的得心应手。


推荐阅读
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • 今天就跟大家聊聊有关怎么在Android应用中实现一个换肤功能,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 本文介绍了iOS开发中检测和解决内存泄漏的方法,包括静态分析、使用instruments检查内存泄漏以及代码测试等。同时还介绍了最能挣钱的行业,包括互联网行业、娱乐行业、教育行业、智能行业和老年服务行业,并提供了选行业的技巧。 ... [详细]
  • SmartRefreshLayout自定义头部刷新和底部加载
    1.添加依赖implementation‘com.scwang.smartrefresh:SmartRefreshLayout:1.0.3’implementation‘com.s ... [详细]
  • macOS命令行创建Android模拟器
    macOS下不安装AndroidStudio使用VSCode来开发Flutter应用使用命令行创建和管理Android模拟器设备avdmanageravdmanager 是一种命令 ... [详细]
  • 开发笔记:图像识别基于主成分分析算法实现人脸二维码识别
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了图像识别基于主成分分析算法实现人脸二维码识别相关的知识,希望对你有一定的参考价值。 ... [详细]
author-avatar
phpyi
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有