Android ScrollView滚动机制及嵌套滑动机制NestedScrolling
Android ScrollView滚动机制
我们都知道通过View#scrollTo(x,y)既可以实现将View滚动的效果,如果再添加Scroller类,就可以实现滚到效果。但是,这背后是如何实现的呢?这个问题涉及到View的绘图机制。我们先看看View的绘图的基本流程
(图片来自于网上比较常见的view绘图流程图)
关于三个阶段的简单描述:
1. measure:预估计ViewTree的各个View的占用空间。
2. layout : 确定ViewTree中各个View所处的空间位置,包括width,height,left,top,right,bottom
3. draw: 使用RootViewImpl中的一个surface.lockCanvas(dirty)对象作为画布,然ViewTree上所有的View都在这个Canvas上进行画图,
值得注意的是,Canvas通过getHeight() 和 getWidth()就是整个屏幕的真实大小。包括了通知栏(虽然在打印出来的ViewTree看不到,但是通过top属性,留下了一点空间给通知栏),标题栏,Content,底部虚拟按键等。
我们先看看mScrollX/mScrollY在代码中的注释:
mScrollX/mScrollY相对这个View的内容(文字,图片,子View)垂直/水平的像素偏移。如下图:
在设置mScrollX / mScrollY后,就可以滚动到指定的“内容",而mScrollX/mScrollY 就是相对于“内容”的偏移量,内容原点为(0,0)。
而这种内容大小以及偏移是如何发生的?在ViewGroup中,存在一个API drawChild(),这个函数主要完成对子View的空间大小的限制以及偏移,见如下的描述
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = false; //获取子View的空间大小 final int cl = child.mLeft; final int ct = child.mTop; final int cr = child.mRight; final int cb = child.mBottom; //通知子View进行判断是否完成滚动,这里就是通过Scroller代码实现滚动的关键点 child.computeScroll(); //获取最新的偏移量 final int sx = child.mScrollX; final int sy = child.mScrollY; //创建一个还原点 final int restoreTo = canvas.save(); //偏移,通过这个API,实现了scroll对内容偏移, 先把内容的原点进行偏移到负数区域 canvas.translate(cl - sx, ct - sy); //剪切,因为之前有一个translate操作,所有剪切出来的空间就是父View给定的可见区域 //所以如果子View填充Canvas的内容超出给定的空间,也不会显示出来 canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct)); //让子View进行绘图,注意子View不用处理Scroll属性,既可以实现内容偏移 child.draw(canvas); //还原 canvas.restoreToCount(restoreTo); return more; }
值得注意的是,ListView不是采用这种机制实现的,而是采用替换ChildView来实现滑动效果的。
Android 嵌套滑动机制(NestedScrolling)
Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性
NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView。
效果如上图
在这之前,我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
这种分发机制有一个漏洞:
如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。
但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下
NestedScrollingChild NestedScrollingParent NestedScrollingChildHelper NestedScrollingParentHelper
以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout)
@Override public void setNestedScrollingEnabled(boolean enabled) { super.setNestedScrollingEnabled(enabled); mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); }
对,简单的话你就这么实现就好了。
这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法
/** * Start a new nested scroll for this view. * * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link NestedScrollingChild} interface method with the same signature to implement * the standard policy.</p> * * @param axes Supported nested scroll axes. * See {@link NestedScrollingChild#startNestedScroll(int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。
那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:
一、startNestedScroll
首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent
二、dispatchNestedPreScroll
在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。
三、dispatchNestedScroll
向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。
四、stopNestedScroll
结束整个流程。
整个对应流程是这样
子view | 父view |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
一般是子view发起调用,父view接受回调。
我们最需要关注的是dispatchNestedPreScroll中的consumed参数。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;
它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。
对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。
先看看效果图
Activity:
package com.example.editortoast; import android.app.Activity; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toastShow(); } }); } private void toastShow() { LayoutInflater inflater = LayoutInflater.from(getApplicationContext()); View view = inflater.inflate(R.layout.item_toast, null); TextView textView1 = (TextView) view.findViewById(R.id.TextView_1); textView1.setText("Toast1"); Toast toast = new Toast(getApplicationContext()); toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); toast.setDuration(0); toast.setView(view); toast.show(); } }
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.editortoast.MainActivity" > <Button android:id="@+id/bt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击" />
</RelativeLayout>
item_toast.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/image" android:layout_width="80dp" android:layout_height="80dp" android:layout_centerVertical="true" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/TextView_1" android:textSize="30sp" android:textColor="@android:color/holo_red_light" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@id/image" /> </RelativeLayout>
android 自定义Toast,可设定显示时间
开发android的同学可能会抱怨Toast设定显示的时长无效,只能是Toast.LENGTH_LONG 或者Toast.LENGTH_SHORT 之一,为了解决这些办法,有多种实现方式:
1.使用定时器,定时调用show()方法.
2.使用CountDownTimer类,也是调用show()方法.
3.使用WindownManager类实现.
本文使用方法三进行实现,难度不大,直接看代码吧.
package com.open.toast; import android.content.Context; import android.graphics.Color; import android.graphics.PixelFormat; import android.os.Handler; import android.view.Gravity; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; /** * 自定义时长的Toast * @author DexYang * */ public class CToast { public static CToast makeText(Context context, CharSequence text, int duration) { CToast result = new CToast(context); LinearLayout mLayout=new LinearLayout(context); TextView tv = new TextView(context); tv.setText(text); tv.setTextColor(Color.WHITE); tv.setGravity(Gravity.CENTER); mLayout.setBackgroundResource(R.drawable.widget_toast_bg); int w=context.getResources().getDisplayMetrics().widthPixels / 2; int h=context.getResources().getDisplayMetrics().widthPixels / 10; mLayout.addView(tv, w, h); result.mNextView = mLayout; result.mDuration = duration; return result; } public static final int LENGTH_SHORT = 2000; public static final int LENGTH_LONG = 3500; private final Handler mHandler = new Handler(); private int mDuration=LENGTH_SHORT; private int mGravity = Gravity.CENTER; private int mX, mY; private float mHorizontalMargin; private float mVerticalMargin; private View mView; private View mNextView; private WindowManager mWM; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); public CToast(Context context) { init(context); } /** * Set the view to show. * @see #getView */ public void setView(View view) { mNextView = view; } /** * Return the view. * @see #setView */ public View getView() { return mNextView; } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(int duration) { mDuration = duration; } /** * Return the duration. * @see #setDuration */ public int getDuration() { return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin) { mHorizontalMargin = horizontalMargin; mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin() { return mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin() { return mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset) { mGravity = gravity; mX = xOffset; mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public int getGravity() { return mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset() { return mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset() { return mY; } /** * schedule handleShow into the right thread */ public void show() { mHandler.post(mShow); if(mDuration>0) { mHandler.postDelayed(mHide, mDuration); } } /** * schedule handleHide into the right thread */ public void hide() { mHandler.post(mHide); } private final Runnable mShow = new Runnable() { public void run() { handleShow(); } }; private final Runnable mHide = new Runnable() { public void run() { handleHide(); } }; private void init(Context context) { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = android.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); mWM = (WindowManager) context.getApplicationContext() .getSystemService(Context.WINDOW_SERVICE); } private void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; // mWM = WindowManagerImpl.getDefault(); final int gravity = mGravity; mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams); } } private void handleHide() { if (mView != null) { if (mView.getParent() != null) { mWM.removeView(mView); } mView = null; } } }
测试类的代码如下:
package com.open.toast; import android.app.Activity; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.EditText; public class MainActivity extends Activity { private EditText mEditText; private CToast mCToast; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { mEditText=(EditText)findViewById(R.id.timeEditText); findViewById(R.id.showToastBtn).setOnClickListener(listener); findViewById(R.id.hideToastBtn).setOnClickListener(listener); } private View.OnClickListener listener=new View.OnClickListener() { @Override public void onClick(View v) { switch(v.getId()) { case R.id.showToastBtn: if(null!=mCToast) { mCToast.hide(); } int time=TextUtils.isEmpty(mEditText.getText().toString())?CToast.LENGTH_SHORT:Integer.valueOf(mEditText.getText().toString()); mCToast=CToast.makeText(getApplicationContext(), "我来自CToast!",time); mCToast.show(); break; case R.id.hideToastBtn: if(null!=mCToast) { mCToast.hide(); } break; } } }; }
效果如下:
Layout_weight是Android开发中一个比较常用的布局属性,在面试中也经常被问到.下面通过实例彻底搞懂Layout_weight的用法.
先看下面的布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="3"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
在水平方向的线性布局中,有三个TextView,layout_width都是0dp,layout_weight分别为1,2,3,所以这三个TextView应该按1:2:3的比例分布,
显示效果:
可以看到,虽然三个TextView是按1:2:3的比例进行分布的,但是第一个TextView却没有和另外两个对齐,这是为什么呢?
仔细看就能发现,虽然三个TextView不是对齐的,但是第一行的文本是对齐的.
我们只需将LinearLayout的baselineAligned属性设置为false即可,baselineAligned表示是否以文本基准线对齐.
这只是对layout_weight属性最基本的用法.假如把第一个TextView的layout_width该为wrap_content,会出现什么情况呢?
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<TextView
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="3"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
看下效果:
发现三个TextView并没有再按照1:2:3的比例进行分配.其实,layout_weight属性是先按控件声明的尺寸进行分配,然后将剩余的尺寸按layout_weight的比例进行分配.
设屏幕总宽度为sum,第一个TextView声明的尺寸为x,那么,
第一个TextView的宽度为: x + (sum - x) * 1/6
第二个TextView的宽度为: (sum - x) * 2/6
第三个TextView的宽度为: (sum - x) * 3/6
为了证实这个结论,接下来继续修改布局,将三个TextView的layout_width都设置为match_parent,把layout_weight分别设置为1,2,2
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
先看下效果:
看到这个结果可能会感到很诧异,第一个TextView的layout_weight明明比其它两个的小,为什么宽度却比它们大呢?
我们按刚才得出的结论算一下.
依然设屏幕总宽度为sum,由于这三个TextView声明的尺寸都是match_parent,也就是sum,那么,
第一个TextView的宽度为: sum + (sum - 3*sum) * 1/5 = sum*3/5
第二个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5
第三个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5
三个TextView的宽度比例为 3:1:1,所以我们的结论是正确的.
需要注意的是,我们结论所说的剩余的尺寸可能是负的,如 sum - 3*sum,
另外,通过最后这个例子可以看出,并不是layout_weight越大,宽度越大.
Android Layout_weight的深刻理解
首先看一下Layout_weight属性的作用:它是用来分配属于空间的一个属性,你可以设置他的权重。很多人不知道剩余空间是个什么概念,下面 我先来说说剩余空间。
看下面代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:text="one"/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1.0"
android:text="two"/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:text="three"/>
</LinearLayout>
运行结果是:
看上面代码:只有Button2使用了Layout_weight属性,并赋值为了1,而Button1和Button3没有设置 Layout_weight这个属性,根据API,可知,他们默认是0
下面我就来讲,Layout_weight这个属性的真正的意思:Android系统先按照你设置的3个Button高度 Layout_height值wrap_content,给你分配好他们3个的高度,
然后会把剩下来的屏幕空间全部赋给Button2,因为只有他的权重值是1,这也是为什么Button2占了那么大的一块空间。
有了以上的理解我们就可以对网上关于Layout_weight这个属性更让人费解的效果有一个清晰的认识了。
我们来看这段代码:
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:background="#ff0000"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="1"
android:textColor="@android:color/white"
android:layout_weight="1"/>
<TextView
android:background="#cccccc"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="2"
android:textColor="@android:color/black"
android:layout_weight="2" />
<TextView
android:background="#ddaacc"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="3"
android:textColor="@android:color/black"
android:layout_weight="3" />
</LinearLayout>
三个文本框的都是 layout_width=“wrap_content ”时,会得到以下效果
按照上面的理解,系统先给3个TextView分配他们的宽度值 wrap_content(宽度足以包含他们的内容1,2,3即可),然后会把剩下来的屏幕空间按照1:2:3的比列分配给3个textview,所以就 出现了上面的图像。
而当layout_width=“fill_parent”时, 如果分别给三个TextView设置他们的Layout_weight为1、2、2的话,就会出现下面的效果:
你会发现 1的权重小,反而分的多了,这是为什么呢???网上很多人说是当layout_width=“fill_parent”时,weighth 值越小权重越大,优先级越高,就好像在背口诀
一样,其 实他们并没有真正理解这个问题,真正的原因是Layout_width="fill_parent"的原因造成的。依照上面理解我们来分析:
系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度
那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )
那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/5 * 剩余空间大小(-2 parent_width)=3/5parent_width
同理第二个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;
第三个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;所以就是3:1:1的比列显示了。
这样你也就会明白为什么当你把三个Layout_weight设置为1、2、3的话,会出现下面的效果了:
第三个直接不显示了,为什么呢?一起来按上面方法算一下吧:
系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度
那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )
那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/6 * 剩余空间大小(-2 parent_width)=2/3parent_width
同理第二个TextView的实际所占宽度=parent_width + 2/6*(-2parent_width)=1/3parent_width;
第三个TextView的实际所占宽度=parent_width + 3/6*(-2parent_width)=0parent_width;所以就是2:1:0的比列显示了。第三个就直接没有空间了。
PullToRefrech下拉刷新..这个效果在绝大多数使用app貌似都使用的到..一个页面的、如果数据太多必然进行下拉..那么下拉之后就需要显示新的数据信息..这就是下拉刷新的完整体现.也可以说这种效果能够给用户一个更加良好的体验..那么我们看看下拉刷新到底是如何实现的..
1.普通View的PullReferch
无论是普通View的下拉刷新还是ListView,还是GridView..下拉刷新实现的原理基本都是相同的..只不过实现上有一些简单的细节不太一样而已...首先先看看普通View的下拉刷新..实现普通下拉刷新需要使用到AbPullView, AbPullView extends ScollView 可滑动视图...不难想象的一件事情就是无论是怎样进行滑动..其实都是对布局的覆盖..
手机的屏幕大小是有限的...因此无论是怎样进行滑动..都是一层布局去覆盖原来的那层布局..原来的那层布局会被直接销毁..如果按照正常思路去想,那么就算是View被销毁了...那么自然有记录了这次布局的相关数据...当这些被销毁的布局需要再次显示的时候,那么直接调用记录数据就可以了..没有必要再建立新的布局..由于是AndBase内部把这个视图已经封装好了因此我们需要进行相关的引用..说白了就是需要进行动态的布局...因此布局文件中,需要这样去进行书写..我们通过获取ID的方式进行动态的布局..
<com.ab.view.pullview.AbPullView
android:id="@+id/mPullView"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</com.ab.view.pullview.AbPullView>
获取到了ID之后..通过动态加载xml数据..就可以设置好下拉需要显示的相关View..设置的方式其实就是一行代码就能解决的事情..
mAbPullView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
这个方式就是设置顶部下拉刷新的一个背景显示...这里我设置了一个动态的效果...R.drawable.progress_circular...
<!-- 单帧动画旋转功能的实现 -->
<!-- 持续时间:两秒
起始角度:0度
定义动画的旋转样式..起始和结束都减速..中间过程加速..
偏移量:顶部和左部
重复的样式:重新开始
末端角度:360度
背景设置:drawable-->
<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromDegrees="0.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:repeatMode="restart"
android:toDegrees="360.0"
android:drawable="@drawable/progress_loading2">
</animated-rotate>
上面的这些过程都是设置下拉时需要显示的View数据..并没有真正涉及到下拉的过程..说白了这个就是下拉后需要发生的事情..而真正想要实现下拉..少不了监听事件的设置..有了对下拉的监听..系统才能够对下拉进行相关的响应..响应事件的绑定..
mAbPullView.setAbOnRefreshListener(new AbOnRefreshListener(){
@Override
public void onRefresh() {
mAbTaskQueue.execute(item); //下拉时需要执行的事件..
}
});
绑定上了监听之后..当我们在屏幕上进行下拉的时候..那么就会有事件进行响应了..这里我设置任务是一个异步线程任务...我们可以在这个任务中设置需要执行的事件..然后将事物放入到任务执行队列当中..那么任务就会在下拉的这个时间段内被执行..然后更新在UI界面上..
final AbTaskItem item = new AbTaskItem();
item.listener = new AbTaskListener() {
@Override
public void update() {
//当任务完成之后..那么停止刷新..也就表示下拉时出现的视图需要被隐藏了...
removeProgressDialog();
mAbPullView.stopRefresh(); //停止刷新..
}
@Override
public void get() {
try {
Thread.sleep(1000);
//这里是任务执行的过程...
} catch (Exception e) {
}
};
};
这样通过几个简单的过程就可以完成普通View的下拉刷新...这个的源码我并没有进行特别深入的研究...其实关键地方就是ScollView 和 FrameLayout..总体的思想我也说过了..视图无论是滑动还是下拉刷新原理就是这样...就是View的覆盖..如果有更有兴趣的..可以去研究研究这两个类的源码...
2.ListView下拉刷新的实现...
ListView下拉刷新,这个在绝大多数的app中都必然会使用到..就拿我们的博客园app来说吧..博客的放入在屏幕上的显示是有限的..那么如果我们想要看到更多的博客文章..那么无非需要下拉刷新加载更多数据,或者是使用分页的方式去显示更多的数据..博客园的app是下拉刷新来实现的..用过的人还是非常清楚的..因此ListView的下拉刷新还是非常的重要的..必须要进行掌握的...
AndBase仍然把一些类进行了封装..提供给我们进行使用..去实现ListView的下拉刷新..原理基本是相同的..需要使用AbPullListView extends ListView implements OnScollListener在继承了ListView的情况下同时也就实现了滚动监听..下面这个接口是在为ListView设置监听后需要实现的方法...方法的实现由我们去实现..这个类仅仅提供了在下拉或上啦后需要做的事情..并没有涉及到滑动的过程...
public interface AbOnListViewListener {
/**
* On refresh.
*/
public void onRefresh();
/**
* On load more.
*/
public void onLoadMore();
}
滑动的过程是由于AbPullListView实现了OnTouchEvent方法..OnTouchEvent表示触摸事件监听...当在屏幕上发生按下,抬起,移动等操作时需要执行..这也就是对滑动事件的一个监听设置..没了这个方法..我们怎么滑动屏幕都是徒劳..那么当发生了上拉或者是下拉就会去执行相应的监听事件..这个上拉或者是下拉的过程是通过滑动的坐标值来确定的..这里就涉及到了OnTouchEvent的源码..源码的实现也比较简单...大体的东西还是比较清晰的..
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) { //对事件的判断..
case MotionEvent.ACTION_DOWN: //按下操作..
mLastY = ev.getRawY(); //获取数值坐标..
break;
case MotionEvent.ACTION_MOVE: //移动操作..
final float deltaY = ev.getRawY() - mLastY; //原始值减掉最终值..
mLastY = ev.getRawY(); //获取原始值..
if (mEnablePullRefresh && getFirstVisiblePosition() == 0 && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) { //这里是对坐标的判断过程..if条件表示满足上拉条件..
updateHeaderHeight(deltaY / OFFSET_RADIO);
} else if (mEnablePullLoad && !mPullLoading && getLastVisiblePosition() == mTotalItemCount - 1 && deltaY<0) { //这个表示下拉过程..
startLoadMore();
}
break;
case MotionEvent.ACTION_UP: //抬起操作的执行过程..
mLastY = -1;
if (getFirstVisiblePosition() == 0) {
//需要刷新的条件
if (mEnablePullRefresh && mHeaderView.getVisiableHeight() >= mHeaderViewHeight) {
mPullRefreshing = true;
mHeaderView.setState(AbListViewHeader.STATE_REFRESHING);
if (mListViewListener != null) {
//刷新
mListViewListener.onRefresh();
}
}
if(mEnablePullRefresh){
//弹回
resetHeaderHeight();
}
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
那么有了触摸事件的监听之后..当触摸事件发生之后就可以执行相关的监听事件..这样就可以去执行下拉和上拉过程中需要执行的任务了...那么现在就说一下ListView实现下拉刷新的过程...它的布局文件其实也是和普通View的下拉定义一样..
<com.ab.view.pullview.AbPullListView
android:id="@+id/mListView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#00000000"
android:divider="@drawable/list_divider"
android:dividerHeight="1dip"
android:fastScrollEnabled="true">
</com.ab.view.pullview.AbPullListView>
我们需要获取它的ID信息..然后再进行动态布局..这就是ListView的刷新过程..首先我们需要允许它能够进行下拉和上拉的操作..否则是无法滑动的..然后设置下拉时显示的东西..我这里是定义了一个动画效果..表示当前的这个View中的东西要实现旋转效果..效果就是普通View中定义的那个效果..
//获取ListView对象
mAbPullListView = (AbPullListView)this.findViewById(R.id.mListView);
//打开关闭下拉刷新加载更多功能
mAbPullListView.setPullRefreshEnable(true);
mAbPullListView.setPullLoadEnable(true);
//设置进度条的样式
mAbPullListView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
mAbPullListView.getFooterView().setFooterProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
然后需要设置相关的监听事件...监听事件就需要重写这两个方法了..前面已经涉及到了这两个方法..就不再进行细说了..一个方法表示的是上拉过程需要进行重新刷新视图,一个表示的是下拉刷新需要加载更多的视图..两个方法..我们需要自定义去实现..
mAbPullListView.setAbOnListViewListener(new AbOnListViewListener(){
@Override
public void onRefresh() {
mAbTaskQueue.execute(item1);
}
@Override
public void onLoadMore() {
mAbTaskQueue.execute(item2);
}
});
我自己定义了两个异步任务..这两个异步任务用于对数据进行下载的一个操作..那么数据在下载之后是需要更新到ListView中的Item上的..那么Item的数据更新就需要适配器来完成了..因此ListView的适配器不可或缺的一个重要部分..在适配器内部去设置相关样式的显示..当发生下拉或者是上拉等操作的时候..我们去唤醒适配器去更新数据就可以了..更新数据的方法调用..
myListViewAdapter.notifyDataSetChanged();
在这里我只简单的说了一下原理...这个就是任务代码的设置..
final AbTaskItem item1 = new AbTaskItem();
item1.listener = new AbTaskListener() {
@Override
public void update() {
removeProgressDialog();
list.clear();
if(newList!=null && newList.size()>0){
list.addAll(newList);
myListViewAdapter.notifyDataSetChanged();
newList.clear();
}
mAbPullListView.stopRefresh();
}
@Override
public void get() {
try {
Thread.sleep(1000);
currentPage = 1;
newList = new ArrayList<Map<String, Object>>();
Map<String, Object> map = null;
for (int i = 0; i < pageSize; i++) {
map = new HashMap<String, Object>();
map.put("itemsIcon",mPhotoList.get(new Random().nextInt(mPhotoList.size())));
map.put("itemsTitle", "item"+(i+1));
map.put("itemsText", "item..."+(i+1));
newList.add(map);
}
} catch (Exception e) {
}
};
};
总体的过程就是这样..想要做出更好的效果..我们就需要放入我们自己的思想..就能够做出更好的下拉效果了...
3.GridView 下拉刷新..
GridView网格视图..视图以网格的形式进行展现..每一个网格都是一个子视图..原理和ListView基本相同..只不过ListView是一行行的显示..GridView是一块块的去显示..少不了适配器的使用..只要我们获取到网格中的子选项..然后为每一个子选项设置样式..就可以完成一个网格View的设置...网格View的使用也应该不少..只要我们的设置合理..同样能够做出非常漂亮的网格视图..
GridView采用的方式就不是FrameLayout了..它使用的是线性布局..AbPullGridView extends AbBaseGridView implements OnScollListener, OnTouchListener.. AbBaseGridView是继承了LinearLayout..原理采用线性布局的方式将视图进行展示..通过实现滚动监听和触摸监听就能够实现滚动..以及触摸事件的监听设置..有兴趣的还是可以去研究一下源码的实现过程..我就只说一下其中的调用方式..
布局的定义方式仍然是差不多的..仍然是获取ID,然后通过动态加载的方式进行数据的加载..
<com.ab.view.pullview.AbPullGridView
android:id="@+id/mPhotoGridView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.ab.view.pullview.AbPullGridView/>
调用的过程和ListView基本相同..
mAbPullGridView = (AbPullGridView)findViewById(R.id.mPhotoGridView);
//开关默认打开
mAbPullGridView.setPullRefreshEnable(true);
mAbPullGridView.setPullLoadEnable(true);
//设置进度条的样式
mAbPullGridView.getHeaderView().setHeaderProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
mAbPullGridView.getFooterView().setFooterProgressBarDrawable(this.getResources().getDrawable(R.drawable.progress_circular));
mGridView = mAbPullGridView.getGridView();
mGridView.setColumnWidth(150); //设置列的宽度..
mGridView.setGravity(Gravity.CENTER); //对齐方式为中央对其..
mGridView.setHorizontalSpacing(5); //设置水平间距..
mGridView.setNumColumns(GridView.AUTO_FIT); //列的数量为自适应屏幕为限制..
mGridView.setPadding(5, 5, 5, 5); //设置间距..
mGridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
mGridView.setVerticalSpacing(5); //GridView之间的垂直间距..
只是GridView的子选项需要我们设置更多的东西..需要设置子选项的排列以及显示方式..设置好了一个子选项的排列方式,那么其他所有的子选项会按照这同一种方式显示在屏幕之上..这样只是设置好了排布的问题..具体需要显示的东西我们还是需要通过使用适配器来安排子选项需要显示的东西..
同样滑动之后需要执行的监听事件我们是需要进行重写的..
mAbPullGridView.setAbOnListViewListener(new AbOnListViewListener() {
@Override
public void onRefresh() {
//第一次下载数据
mAbTaskQueue.execute(item1);
}
@Override
public void onLoadMore() {
mAbTaskQueue.execute(item2);
}
});
同时我们也可以设置子选项被点击时设置的相关监听..通过设置子选项的监听..可以通过触发子选项执行更多的操作..忘记说了..ListView也是可以触发子选项的监听事件的..比较简单就没必要再贴代码了...
mAbPullGridView.getGridView().setOnItemClickListener(new OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
showToast(""+position);
}
});
这里的异步任务我也就不再进行粘贴了..和ListVIew基本都是相同的..当任务执行完毕之后..通过唤醒适配器去更新数据信息就可以完成了..这样就实现了PullToRefrech下拉刷新..我们知道了调用过程之后..就能够通过自己定义的方式去设置更加良好的刷新效果..调用的过程不是很难..真正难的是我们自己的思想..如何设置出更加良好的UI才是更加重要的..
Android-PullToRefresh下拉刷新库基本用法
PullToRefresh是一套实现非常好的下拉刷新库,它支持:
ListView
ExpandableListView
GridView
WebView
ScrollView
HorizontalScrollView
ViewPager
等多种常用的需要刷新的View类型,而且使用起来也十分方便。
(下载地址:https://github.com/chrisbanes/Android-PullToRefresh)
PullToRefresh基本用法:
1、在布局文件中添加PullToRefresh控件,比如PullToRefreshListView;
2、在Activity中,设置监听器OnRefreshListener以响应用户下拉操作;
3、在监听器的onRefresh()方法中执行数据刷新操作,可以通过AsyncTask来实现;
4、在AsyncTask中获取到数据后,记得调用onRefreshComplete()方法通知PullToRefresh控件数据已获取完毕,可以结束刷新操作。
实例:PullToRefreshDemo
运行效果:
视图结构
代码清单:
布局文件:activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<com.handmark.pulltorefresh.library.PullToRefreshListView
android:id="@+id/pull_to_refresh_listview"
android:layout_height="fill_parent"
android:layout_width="fill_parent" />
</RelativeLayout>
Java源代码文件:MainActivity.java
package com.rainsong.pulltorefreshdemo;
import java.util.Arrays;
import java.util.LinkedList;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.Menu;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
public class MainActivity extends Activity {
private PullToRefreshListView mPullToRefreshListView;
private LinkedList<String> mListItems;
private ArrayAdapter<String> mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set a listener to be invoked when the list should be refreshed.
mPullToRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_to_refresh_listview);
mPullToRefreshListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
@Override
public void onRefresh(PullToRefreshBase<ListView> refreshView) {
String label = DateUtils.formatDateTime(getApplicationContext(), System.currentTimeMillis(),
DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL);
// Update the LastUpdatedLabel
refreshView.getLoadingLayoutProxy().setLastUpdatedLabel(label);
// Do work to refresh the list here.
new GetDataTask().execute();
}
});
ListView actualListView = mPullToRefreshListView.getRefreshableView();
mListItems = new LinkedList<String>();
mListItems.addAll(Arrays.asList(mStrings));
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);
actualListView.setAdapter(mAdapter);
}
private class GetDataTask extends AsyncTask<Void, Void, String[]> {
@Override
protected String[] doInBackground(Void... params) {
// Simulates a background job.
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
}
return mStrings;
}
@Override
protected void onPostExecute(String[] result) {
mListItems.addFirst("Added after refresh...");
mAdapter.notifyDataSetChanged();
// Call onRefreshComplete when the list has been refreshed.
mPullToRefreshListView.onRefreshComplete();
super.onPostExecute(result);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
private String[] mStrings = { "John", "Michelle", "Amy", "Kim", "Mary",
"David", "Sunny", "James", "Maria", "Michael", "Sarah", "Robert",
"Lily", "William", "Jessica", "Paul", "Crystal", "Peter",
"Jennifer", "George", "Rachel", "Thomas", "Lisa", "Daniel", "Elizabeth",
"Kevin" };
}
相关文章
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 这篇文章主要介绍了如何使用JavaScript实现“无缝滚动 自动播放”轮播图效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-08-20
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 这篇文章主要为大家详细介绍了jQuery实现下拉菜单滑动效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-08-09
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
- java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
- TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
android.os.BinderProxy cannot be cast to com解决办法
本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20- 这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
- 下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
- 这篇文章主要介绍了JavaScript 获取滚动条位置并将页面滑动到锚点的的相关资料,帮助大家更好的理解和学习使用JavaScript,感兴趣的朋友可以了解下...2021-02-09
- 这篇文章主要介绍了js实现文字垂直滚动和鼠标悬停效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2016-01-05
- 这篇文章主要为大家详细介绍了uniapp 实现可以左右滑动导航栏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-10-21
- 这篇文章主要为大家详细介绍了使用jQuery.Pin垂直滚动时固定导航的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2017-05-27
- 这篇文章主要介绍了javascript实现无缝上下滚动特效的相关资料,需要的朋友可以参考下...2015-12-18