本文主要整理了一些scroll常用的知识点,并基于滑动知识,仿微博微信实现了滑动时标题栏渐变的运行效果。进一步增长了对滑动api的理解与运用的能力。
讲到滑动,又不得不和滑动去打交道,滑动这块的 scrollTo() , scrollBy() , getScrollX() getScrollY()啊这些真实比较难懂,小白我就不班门弄斧了,可以参考讲的比较好的博客 关于View的ScrollTo, getScrollX 和 getScrollY 。主要是需要掌握一个宏观的窗口与画布的关系,那个图是真的好懂。
重点:scrollTo(),scrollBy()
这2个API还是比较重要的,现在着重拎出来领悟一下。
不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,是瞬间完成的(无法看到平移的效果)
说明: 如果滑动的是LinearLayout 那么实际上滑动的是其内部的内容,而如果滑动一个Button那么其实很可能结果会出乎我们的意料的。
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
// 最终会调用的
@Deprecated
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
如果我们传递的scrollerX值是正数的话,(l - scrollX) 计算后则左边距会变小,所以内容会往左移动(也就是x轴的负方向)。如果我们传递的scrollerX值是负数的话,(l - scrollX) 计算后则左边距会变大,因此内容会往右移动(也就是x轴的正方向),同理,y轴也一样。
案例与运行结果:
线性布局内含有一个Button按钮,执行了如下代码,线性布局的内容向右移动了100px。然后打印的log日志如下所示。
mLinearLayout = findViewById(R.id.llScrollLayout);
Log.e(TAG, "getScrollX() = mScrollX ="+mLinearLayout.getScrollX() );
mLinearLayout.scrollTo(-100,0);
Log.e(TAG, "getScrollX() = mScrollY ="+mLinearLayout.getScrollX() );
//E/MainActivity: getScrollX() = getScrollX() = mScrollX = 0
//E/MainActivity: getScrollX() = getScrollX() = mScrollX = -100
Scroller类,平滑过渡
原理:Scroller类本身并没有实现view的滑动,它实际上需要配合View的computeScroller方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动的起始时间会有一个时间间隔,通过这个时间间隔Scroller类就可以得出View当前滑动的位置,知道了滑动位置就可以通过scrollTo()方法来完成View的滑动(这里也说明view的滑动最终还是要靠scrollTo()方法来实现,Scroller类本身本身也只是个辅助类而已),就这样,View每一次重绘都会导致View进行小幅度的滑动,而多次小幅度滑动的就组成了弹性滑动。
概述: 每隔一段小时时间重绘一次view(去调用scrollerTo()滑动),从而导致View进行了小幅度的滑动,次数多了,也就形成了平滑过渡
Scroller.computeScrollOffset()结束返回false,未结束返回true
核心方法:
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
* 推断滑动
*/
public void computeScroll() {
}
解释:当invalidate()或postInvalidate()都会导致这个方法的执行,也就是说view重绘时都会导致这个方法执行
/**
* 覆写computeScroll
* 在computeScroll()方法中
* 在View的源码中可以看到public void computeScroll(){}是一个空方法.
* 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy()
* 来实现移动(动画).该方法才是实现移动的核心.
* 4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成
* 注意:该方法是Scroller中的方法而不是View中的!!!!!!
* public boolean computeScrollOffset(){ }
* Call this when you want to know the new location.
* If it returns true,the animation is not yet finished.
* loc will be altered to provide the new location.
* 返回true时表示还移动还没有完成.
* 4.2 若动画没有结束,则调用:scrollTo(By)();
* 使其滑动scrolling
*
* 5 再次调用invalidate()或者postInvalidate();.
* 调用invalidate()方法那么又会重绘View树.
* 从而跳转到第3步,如此循环,便形成了动画移动的效果,直到computeScrollOffset返回false
*
*
* invalidate()与postInvalidate() 区别:
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
*
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
* 这段话的意思是:
* invalidate()只能在UI线程调用而不能在非UI线程调用
* postInvalidate() 可以在非UI线程调用也可以在UI线程调用
*/
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
//重绘
postInvalidate();
}
}
简单的案例:参考资郭霖大神
/**
* @author crazyZhangxl on 2018/11/5.
* Describe: 一个简易的viewPager当然这是很不完善的
* 主要用于了解 scroller的运用,包括onMeasure() onLayout() 对于子布局的测量和摆放
*/
public class SampleViewpager extends ViewGroup{
private Scroller mScroller;
private int mTouchSlop; // 最小滑动距离
private float mMLastX;
public SampleViewpager(Context context) {
this(context,null);
}
public SampleViewpager(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public SampleViewpager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i=0;i mTouchSlop){
isIntercept = true;
}
mMLastX = moveRawX;
break;
default:
break;
}
return isIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float moveRawX = event.getRawX();
int scrollX = (int) (mMLastX - moveRawX);
Log.e("结果", "onTouchEvent: "+scrollX );
// 判断边界----
scrollBy(scrollX,0);
mMLastX = moveRawX;
break;
case MotionEvent.ACTION_UP:
// 抬起的时候进行校验 进行回滚
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
// 起始的位置(x,y)滑动的偏移量
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
首先了解下ScrollView滑动的api
/**
* 滑动监听
* This is called in response to an internal scroll in this view (i.e., the
* view scrolled its own contents). This is typically as a result of
* {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
* called.
*
* @param l Current horizontal scroll origin. 当前滑动的x轴距离
* @param t Current vertical scroll origin. 当前滑动的y轴距离
* @param oldl Previous horizontal scroll origin. 上一次滑动的x轴距离
* @param oldt Previous vertical scroll origin. 上一次滑动的y轴距离
*/
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
Log.e("onScrollChanged","l = "+l+" ,t = "+t+" ,oldl = "+oldl+" ,oldt = "+oldt);
}
以下图例为测量的控件的实际高度以及上滑和下滑的log日志打印
向上滑动,当前的滑动的y轴距离一直增加,也和我们的scrollTo相吻合。
仿微博滑动标题栏渐变效果
重写NestedScrollView,设置垂直滑动的监听。
public class ObservableAlphaScrollView extends NestedScrollView {
private OnAlphaScrollChangeListener mOnAlphaScrollChangeListener;
public ObservableAlphaScrollView(@NonNull Context context) {
super(context);
}
public ObservableAlphaScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ObservableAlphaScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnAlphaScrollChangeListener(OnAlphaScrollChangeListener onAlphaScrollChangeListener){
this.mOnAlphaScrollChangeListener = onAlphaScrollChangeListener;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mOnAlphaScrollChangeListener != null){
mOnAlphaScrollChangeListener.onVerticalScrollChanged(t);
}
}
public interface OnAlphaScrollChangeListener{
void onVerticalScrollChanged(int t);
}
}
主体活动,测量渐变的距离,沉浸式状态栏设置状态栏偏移,设置滑动时状态改变。
public class ScrollAlphaActivity extends AppCompatActivity implements ObservableAlphaScrollView.OnAlphaScrollChangeListener {
private ObservableAlphaScrollView mObservableAlphaScrollView;
private View mTitleView;
private TextView tvHeadView;
private int statusBarHeight;
private RelativeLayout mRlHead;
private int moveDiatance;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroll_alpha);
// 设置的沉浸式状态栏
mObservableAlphaScrollView = findViewById(R.id.llTest);
mTitleView = findViewById(R.id.ll_header_content);
tvHeadView = findViewById(R.id.tv_main_topContent);
mRlHead = findViewById(R.id.rlHead);
// 设置空余的Padiing
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mRlHead.getLayoutParams();
layoutParams.topMargin = getStatusBarHeight();
mRlHead.setLayoutParams(layoutParams);
// 测量高度差-----
ViewTreeObserver treeObserver = tvHeadView.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听
tvHeadView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int titleHeight = mTitleView.getMeasuredHeight();
int headHeight = tvHeadView.getMeasuredHeight();
moveDiatance = headHeight - titleHeight;
mObservableAlphaScrollView.setOnAlphaScrollChangeListener(ScrollAlphaActivity.this);
}
});
}
@Override
public void onVerticalScrollChanged(int t) {
if (t<=0){
mTitleView.setBackgroundColor(Color.argb(0, 48, 63, 159));
}else {
if (t 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
statusBarHeight = result;
}
return statusBarHeight;
}
}
仿微信朋友圈渐进效果
/**
* @author crazyZhangxl on 2018-11-5 16:49:54.
* Describe: 仿微信朋友圈界面
*/
public class LikeWChatActivity extends AppCompatActivity implements ObservableAlphaScrollView.OnAlphaScrollChangeListener {
private LinearLayout mLlTitle,mLlScHead;
private int mTitleHeight;
private int mHeadHeight;
private int mDistance;
private ObservableAlphaScrollView mScrollView;
private ImageView mIvBack;
private TextView mTvText;
private int mDistanceY = 30;// 设置一个临界值吧
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_like_wchat);
mLlTitle = findViewById(R.id.llTitle);
mLlScHead = findViewById(R.id.llScHead);
mScrollView = findViewById(R.id.scrollView);
mIvBack = findViewById(R.id.imageBack);
mTvText = findViewById(R.id.ivText);
ViewTreeObserver viewTreeObserver = mLlScHead.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mLlScHead.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mTitleHeight = mLlTitle.getMeasuredHeight();
mHeadHeight = mLlScHead.getMeasuredHeight();
mDistance = mHeadHeight - mTitleHeight;
Log.e("result mTitleHead = ",mTitleHeight+"");
Log.e("result mHeadHeight = ",mHeadHeight+"");
Log.e("result mDistance = ",mDistance+"");
mScrollView.setOnAlphaScrollChangeListener(LikeWChatActivity.this);
}
});
}
@Override
public void onVerticalScrollChanged(int t) {
if (t<= (mDistance - mDistanceY)){
mTvText.setAlpha(0f);
mIvBack.setSelected(false);
mLlTitle.setBackgroundColor(Color.argb(0, 242, 242, 242));
}else if (t<=mDistance) {
mTvText.setAlpha(0f);
mIvBack.setSelected(false);
}else if (t <= (mDistance + mDistanceY)){
mTvText.setAlpha(1f);
mIvBack.setSelected(true);
mLlTitle.setBackgroundColor(Color.argb(0, 242, 242, 242));
}else {
mTvText.setAlpha(1f);
mIvBack.setSelected(true);
mLlTitle.setBackgroundColor(Color.argb(255, 242, 242, 242));
}
}
}
参考博客:
Android Scroller完全解析,关于Scroller你所需知道的一切
scroller类的用法完全解析以及带源码分析
高仿美团APP页面滑动标题栏渐变效果
项目地址:https://github.com/crazyzhangxl/ScrollDemo