zoukankan      html  css  js  c++  java
  • Android 动画学习笔记

    1、属性动画

    使用 ObjectAnimator 的时候,有一点非常重要,那就是操纵的属性要具有 get,set 方法,不然ObjectAnimator 就无法起效。下面这些事一些常用的,可以直接使用属性动画的属性值。translationX ,translationY,scaleX,ScaleY,pivotX,pivotY,x,y,alpha。

    但是,有些属性是没有 get set 方法的,那么难道我们就不能用了吗?不是的。

    private static class WrapperView{
        private View mTarget;
    
        public WrapperView(View target){
            mTarget = target;
        }
    
        public getWidth(){
            return mTarget.getLayoutParams().width;
        }
    
        public void setWidth(int width){
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayput();
        }
    }
    
    // 通过以上代码给属性包了一层,并给它提供了 get set 的方法。使用如下:
    
    WrapperView wrapper = new WrapperView(mButton);
    ObjectAnimator.ofInt(wrapper,"width",500).setDuration(5000).start();

    类似的,如果想实现视图动画中的AnimationSet,在属性动画中可以使用 PropertyValuesHolder 来实现。

    PropertyValuesHolder pvh1 = PropertyValuesHolder.ofFloat("translationX",300f);
    PropertyValuesHolder pvh2 = PropertyValuesHolder.ofFloat("scaleX",1f, 0, 1f);
    PropertyValuesHolder pvh3 = PropertyValuesHolder.ofFloat("scaleY",1f, 0, 1f);
    PropertyValuesHolder pvh1 = PropertyValuesHolder.ofFloat("translationX",1f, 0, 1f);
    ObjectAnimator.ofPropertyValuesHolder(view, pvh1, pvh2, pvh3).setDuration(1000).start();

    实例:

    只展示 activity 中的代码。点击之后,自身变淡,四个方向上弹出四个图标。再点击一次,收回四个图标。

    public class PropertyTest extends Activity implements View.OnClickListener {
    
        private int[] mRes = {R.id.imageView_a, R.id.imageView_b, R.id.imageView_c,
                R.id.imageView_d, R.id.imageView_e};
        private List<ImageView> mImageViews = new ArrayList<ImageView>();
        private boolean mFlag = true;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.property);
            for (int i = 0; i < mRes.length; i++) {
                ImageView imageView = (ImageView) findViewById(mRes[i]);
                imageView.setOnClickListener(this);
                mImageViews.add(imageView);
            }
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.imageView_a:
                    if (mFlag) {
                        startAnim();
                    } else {
                        closeAnim();
                    }
                    break;
                default:
                    Toast.makeText(PropertyTest.this, "" + v.getId(),
                            Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    
        private void closeAnim() {
            ObjectAnimator animator0 = ObjectAnimator.ofFloat(mImageViews.get(0),
                    "alpha", 0.5F, 1F);
            ObjectAnimator animator1 = ObjectAnimator.ofFloat(mImageViews.get(1),
                    "translationY", 200F, 0);
            ObjectAnimator animator2 = ObjectAnimator.ofFloat(mImageViews.get(2),
                    "translationX", 200F, 0);
            ObjectAnimator animator3 = ObjectAnimator.ofFloat(mImageViews.get(3),
                    "translationY", -200F, 0);
            ObjectAnimator animator4 = ObjectAnimator.ofFloat(mImageViews.get(4),
                    "translationX", -200F, 0);
            AnimatorSet set = new AnimatorSet();
            set.setDuration(500);
            set.setInterpolator(new BounceInterpolator());
            set.playTogether(animator0, animator1, animator2, animator3, animator4);
            set.start();
            mFlag = true;
        }
    
        private void startAnim() {
            ObjectAnimator animator0 = ObjectAnimator.ofFloat(
                    mImageViews.get(0),
                    "alpha",
                    1F,
                    0.5F);
            ObjectAnimator animator1 = ObjectAnimator.ofFloat(
                    mImageViews.get(1),
                    "translationY",
                    200F);
            ObjectAnimator animator2 = ObjectAnimator.ofFloat(
                    mImageViews.get(2),
                    "translationX",
                    200F);
            ObjectAnimator animator3 = ObjectAnimator.ofFloat(
                    mImageViews.get(3),
                    "translationY",
                    -200F);
            ObjectAnimator animator4 = ObjectAnimator.ofFloat(
                    mImageViews.get(4),
                    "translationX",
                    -200F);
            AnimatorSet set = new AnimatorSet();
            set.setDuration(500);
            set.setInterpolator(new BounceInterpolator());
            set.playTogether(
                    animator0,
                    animator1,
                    animator2,
                    animator3,
                    animator4);
            set.start();
            mFlag = false;
        }
    }
    View Code

    2、视图动画

    视图动画主要有四种,平移,旋转,缩放,透明度四种变化。视图动画要避免交互,但是其效率高,使用方便。下面看一个具体的实例:

    xml 文件

    <LinearLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        tools:context=".MainActivity">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Alpha"
            android:layout_margin="10dp"
            android:onClick="btnAlpha" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rotate"
            android:layout_margin="10dp"
            android:onClick="btnRotate" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rotate_self"
            android:layout_margin="10dp"
            android:onClick="btnRotateSelf" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Translate"
            android:layout_margin="10dp"
            android:onClick="btnTranslate" />
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Scale"
            android:layout_margin="10dp"
            android:onClick="btnScale" />
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Scale_Self"
            android:layout_margin="10dp"
            android:onClick="btnScaleSelf" />
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Anim Set"
            android:layout_margin="10dp"
            android:onClick="btnSet" />
    
    </LinearLayout>
    View Code

    设置了 5 个按钮,每个按钮都设置了点击的方法。

    activity 中的代码:注意点击方法传入的是 View,这样就可以直接处理啦。

    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void btnAlpha(View view) {
            AlphaAnimation aa = new AlphaAnimation(0, 1);
            aa.setDuration(1000);
            view.startAnimation(aa);
        }
    
        public void btnRotate(View view) {
            RotateAnimation ra = new RotateAnimation(0, 360, 100, 100);
            ra.setDuration(1000);
            view.startAnimation(ra);
        }
    
        public void btnRotateSelf(View view) {
            RotateAnimation ra = new RotateAnimation(0, 360,
                    RotateAnimation.RELATIVE_TO_SELF, 0.5F,
                    RotateAnimation.RELATIVE_TO_SELF, 0.5F);
            ra.setDuration(1000);
            view.startAnimation(ra);
        }
    
        public void btnTranslate(View view) {
            TranslateAnimation ta = new TranslateAnimation(0, 200, 0, 300);
            ta.setDuration(1000);
            view.startAnimation(ta);
        }
    
        public void btnScale(View view) {
            ScaleAnimation sa = new ScaleAnimation(0, 2, 0, 2);
            sa.setDuration(1000);
            view.startAnimation(sa);
        }
    
        public void btnScaleSelf(View view) {
            ScaleAnimation sa = new ScaleAnimation(0, 1, 0, 1,
                    Animation.RELATIVE_TO_SELF, 0.5F,
                    Animation.RELATIVE_TO_SELF, 0.5F);
            sa.setDuration(1000);
            view.startAnimation(sa);
        }
    
        public void btnSet(View view) {
            AnimationSet as = new AnimationSet(true);
            as.setDuration(1000);
    
            AlphaAnimation aa = new AlphaAnimation(0, 1);
            aa.setDuration(1000);
            as.addAnimation(aa);
    
            TranslateAnimation ta = new TranslateAnimation(0, 100, 0, 200);
            ta.setDuration(1000);
            as.addAnimation(ta);
    
            view.startAnimation(as);
        }
    
    }
    View Code

    3、ValueAnimator

    ValueAnimator 本身不提供任何动画效果,它更像一个数值发生器,用来产生具有一定规律的数字,从而让调用者来控制动画的实现。

    一个完整的动画具有start, repeat, end, cancel 四个过程,通过Android 提供了接口,可以很方便的监听这四个事件,代码如下所示。

    ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", 0.5f);
    anim .addListener(new AnimatorListener(){
        @Override
        public void onAnimationStart(Animator animation){}
        @Override
        public void onAnimationRepeat(Animator animation){}
        @Override
        public void onAnimationEnd(Animator animation){}
        @Override
        public void onAnimationCancel(Animator animation){}
    
    });
    anim.start();

    当然大部分时候,我们都只关心 onAnimationEnd 事件,所以 Android 也提供了一个 AnimatorListenerAdapter 来让我们选择必要的必要的事情进行监听,代码如下所示:

    anim.addListener(new AnimatorListenerAdapter() {
        public void onAnimationEnd(Animator animation) {
            // TODO
        }
    });

    实例:

    点击之后,会弹出一个下拉 item,再点击一次就消失。主要就是通过 ValueAnimator  实现的。通过中间数值的变化,来设置其高度的变化,达到动画的效果。我们监听动画结束的时候,然后就设置让该 item 看不见。

    package com.imooc.anim;
    
    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.LinearLayout;
    
    public class DropTest extends Activity {
    
        private LinearLayout mHiddenView;
        private float mDensity;
        private int mHiddenViewMeasuredHeight;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.drop);
            mHiddenView = (LinearLayout) findViewById(R.id.hidden_view);
            // 获取像素密度
            mDensity = getResources().getDisplayMetrics().density;
            // 获取布局的高度
            mHiddenViewMeasuredHeight = (int) (mDensity * 40 + 0.5);
        }
    
        public void llClick(View view) {
            if (mHiddenView.getVisibility() == View.GONE) {
                // 打开动画
                animateOpen(mHiddenView);
            } else {
                // 关闭动画
                animateClose(mHiddenView);
            }
        }
    
        private void animateOpen(final View view) {
            view.setVisibility(View.VISIBLE);
            ValueAnimator animator = createDropAnimator(
                    view,
                    0,
                    mHiddenViewMeasuredHeight);
            animator.start();
        }
    
        private void animateClose(final View view) {
            int origHeight = view.getHeight();
            ValueAnimator animator = createDropAnimator(view, origHeight, 0);
            animator.addListener(new AnimatorListenerAdapter() {
                public void onAnimationEnd(Animator animation) {
                    view.setVisibility(View.GONE);
                }
            });
            animator.start();
        }
    
        private ValueAnimator createDropAnimator(
                final View view, int start, int end) {
            ValueAnimator animator = ValueAnimator.ofInt(start, end);
            animator.addUpdateListener(
                    new ValueAnimator.AnimatorUpdateListener() {
    
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    int value = (Integer) valueAnimator.getAnimatedValue();
                    ViewGroup.LayoutParams layoutParams =
                            view.getLayoutParams();
                    layoutParams.height = value;
                    view.setLayoutParams(layoutParams);
                }
            });
            return animator;
        }
    }
    View Code

    另一个实例,定时器,将数值的变化体现在界面上。

    public class TimerTest extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.timer);
        }
    
        public void tvTimer(final View view) {
            ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 100);
            valueAnimator.addUpdateListener(
                    new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    ((TextView) view).setText("$ " +
                            (Integer) animation.getAnimatedValue());
                }
            });
            valueAnimator.setDuration(3000);
            valueAnimator.start();
        }
    }

    4、AnimatorSet

    该属性能够精确的控制的发生的顺序。其他方法还有:animSet.play().with(), playSequentially(), playTogether(), before(), after()。

    ObjectAnimator pvh1 = ObjectAnimator.ofFloat(view, "translationX",300f);
    ObjectAnimator pvh2 = ObjectAnimator.ofFloat(view, "scaleX",1f, 0, 1f);
    ObjectAnimator pvh3 = ObjectAnimator.ofFloat(view, "scaleY",1f, 0, 1f);
    AnimatorSet set = new AnimatorSet();
    set.setDuration(1000);
    set.playTogether(pvh1, pvh2, pvh3);
    set.start();

    5、使用 XML 

    一个是属性动画的XML,另一个是帧动画。

    帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于 视图动画,系统提供了另一个类 AnimationDrawable 来使用帧动画。帧动画比较简单,首先需要通过 XML 来定义一个 AnimationDrawable,如下所示。

    文件地址:res/drawable/frame_test.xml

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
        <item android:drawable="@drawable/ic_launcher" android:duration="500"></item>
        <item android:drawable="@drawable/s1" android:duration="500"></item>
        <item android:drawable="@drawable/s2" android:duration="500"></item>
    </animation-list>

    然后将上述的 Drawable 作为 View 的背景并通过 Drawable 来播放动画即可:

    Button btn = (Button) findViewById(R.id.shen);
    btn.setBackgroundResource(R.drawable.frame_test);
    AnimationDrawable drawable = (AnimationDrawable) btn.getBackground();
    drawable.start();

    帧动画比较简单,但是容易引起 OOM (内存不足) , 所以在使用帧动画的时候,尽量避免使用过多尺寸较大的图片。

    6、自定义动画

    创建自定义动画非常简单,只需要实现它的 applyTransformation 的逻辑就可以啦。不过,通常情况下,还需要覆盖父类的 initialize 方法来实现一些初始化工作。

    applyTransformation( float interpolatedTime, Transformation t)

    applyTransformation 有两个参数,第一个是插值器的事件因子,取值范围 0-1。

    第二个参数是矩阵的封装类,一般使用这个类来获得当前的矩阵对象。通过改变获得矩阵对象来将动画实现出来。

    两个实例:一个是电视关闭的动画,一个是自定义的3D自定义动画效果。

    在 3D 自定义动画效果中,我们使用了一个 camera,该类是 android.graphics.Camera中的 camera 类,它封装了 openGL 的 3D 动画,从而可以很方便的创建 3D 动画效果。

     1 public class CustomAnim extends Animation {
     2 
     3     private int mCenterWidth;
     4     private int mCenterHeight;
     5     private Camera mCamera = new Camera();
     6     private float mRotateY = 0.0f;
     7 
     8     @Override
     9     public void initialize(int width,
    10                            int height,
    11                            int parentWidth,
    12                            int parentHeight) {
    13 
    14         super.initialize(width, height, parentWidth, parentHeight);
    15         // 设置默认时长
    16         setDuration(2000);
    17         // 动画结束后保留状态
    18         setFillAfter(true);
    19         // 设置默认插值器
    20         setInterpolator(new BounceInterpolator());
    21         mCenterWidth = width / 2;
    22         mCenterHeight = height / 2;
    23     }
    24 
    25     // 暴露接口-设置旋转角度
    26     public void setRotateY(float rotateY) {
    27         mRotateY = rotateY;
    28     }
    29 
    30     @Override
    31     protected void applyTransformation(
    32             float interpolatedTime,
    33             Transformation t) {
    34         final Matrix matrix = t.getMatrix();
    35         mCamera.save();
    36         // 使用Camera设置旋转的角度
    37         mCamera.rotateY(mRotateY * interpolatedTime);
    38         // 将旋转变换作用到matrix上
    39         mCamera.getMatrix(matrix);
    40         mCamera.restore();
    41         // 通过pre方法设置矩阵作用前的偏移量来改变旋转中心
    42         matrix.preTranslate(mCenterWidth, mCenterHeight);
    43         matrix.postTranslate(-mCenterWidth, -mCenterHeight);
    44     }
    45 }
    View Code
    public class CustomTV extends Animation {
    
        private int mCenterWidth;
        private int mCenterHeight;
        private Camera mCamera = new Camera();
        private float mRotateY = 0.0f;
    
        @Override
        public void initialize(int width,
                               int height,
                               int parentWidth,
                               int parentHeight) {
    
            super.initialize(width, height, parentWidth, parentHeight);
            // 设置默认时长
            setDuration(1000);
            // 动画结束后保留状态
            setFillAfter(true);
            // 设置默认插值器
            setInterpolator(new AccelerateInterpolator());
            mCenterWidth = width / 2;
            mCenterHeight = height / 2;
        }
    
        // 暴露接口-设置旋转角度
        public void setRotateY(float rorateY) {
            mRotateY = rorateY;
        }
    
        @Override
        protected void applyTransformation(
                float interpolatedTime,
                Transformation t) {
            final Matrix matrix = t.getMatrix();
            matrix.preScale(1,
                    1 - interpolatedTime,
                    mCenterWidth,
                    mCenterHeight);
        }
    }
    View Code

    activity 文件的内容:

    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void btnAnim(View view) {
            CustomAnim customAnim = new CustomAnim();
            customAnim.setRotateY(30);
            view.startAnimation(customAnim);
        }
    
        public void imgClose(View view) {
            CustomTV customTV = new CustomTV();
            view.startAnimation(customTV);
        }
    }
    View Code

     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:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context=".MainActivity">
    
        <Button
            android:text="@string/hello_world"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="btnAnim"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="40dp" />
    
        <ImageView
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:id="@+id/imageView"
            android:onClick="imgClose"
            android:background="@mipmap/ic_launcher"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    
    </RelativeLayout>
    View Code

    主要就是两个按钮。

    7、LayoutAnimation 

    LayoutAnimation 作用于 ViewGroup,为 ViewGroup 制定一个动画,这样当它的子元素出场时都会有这种动画效果。这种效果常常被用于 ListView 上。

    LayoutAnimation 也是一个 视图动画,为了给 ViewGroup 子元素加上出场效果,要遵循如下几个步骤。

    定义 LayoutAnimation,如下所示:

    // res/anim/anim_layout.xml
    <layoutAnimation
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:delay="0.5"
        android:animationOrder="reverse"
        android:animation="@anim/anim_item"/>

    android:delay 表示子动画的事件延迟。比如入场动画周期是300ms,那么 0.5 表示每个子元素都要延迟 150 ms 才能播放入场动画。

    android:animationOrder 表示子动画播放顺序,有三种选项:normal(顺序播放), reverse(逆序播放)和 random (随机)。

    android:animation 表示为子元素制定具体的入场动画。

    // res/anim/anim_item.xml
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="3000"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:shareInterpolator="true" >
    
        <alpha
            android:fromAlpha="0.0"
            android:toAlpha="1.0" />
    
        <translate
            android:fromXDelta="1500"
            android:toXDelta="0" />
    </set>

    接下去为 viewGroup 指定  android:layoutAnimation 属性:  android:layoutAnimation="@anim/anim_layout"。这样,它里面的子元素就会有出场动画了。如下所示:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layoutAnimation="@anim/anim_layout">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="shenjaiqi"
            android:id="@+id/shen"/>
    
    </RelativeLayout>

    除了使用 xml 指定 LayoutAnimation 外,还可以通过 LayoutAnimationController 来实现,具体代码如下所示。

    ListView listView = (ListView) layout.findViewById(R.id.list);
    Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_item);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(0.5f);
    controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
    listView.setLayoutAnimation(controller);

    8、Activity 的切换效果

    Activity 有默认的切换效果,但是这个效果我们可以自定义的,主要用到 

    overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在 

    startActivity 或 finish() 之后被调用才能生效,它的参数含义如下:

      enterAnim: Activity 被打开的时候,所需的动画资源 id;

      exitanim:Activity 被关闭的时候,所需的动画资源 id;

    当启动一个 Activity 的时候,可以按照下面方式添加自定义效果:

    Intent intent = new Intent(this, TestActivity.class);
    startActivity(intent);
    overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);

    当 Activity 推出的时候,也可以按照下面的方式添加自定义切换效果:

       @Override
      public void finish() {
            super.finish();
            overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
        }

    使用动画的注意事项:

    1、OOM问题:主要出现在帧动画中,图片较多且大的话极易出现OOM;

    2、内存泄漏:属性动画中,有一类无限循环的动画,要在 Activity 退出时及时停止;视图动画一般不存在此类问题;

    3、兼容性问题:动画在 3.0 一下的系统有兼容性问题,要做好适配工作;

    4、View 动画是对 View 的影响做动画,并不是真正的改变 View 的状态,因此有时候会出现动画完成后View 无法隐藏的现象,即 setVisibility(View.GONE) 失效了,这个时候只要调用 view.clearAnimation() 清除 View 动画即可解决此问题。

    5、不要使用px: 尽量使用 dp,

    6、动画元素的交互:在 3.0 以下的系统,动画完成后,仍是在老位置触发单机事件;3.0 开始,属性动画的淡季事件触发为移动后的位置,View 动画不变。

    7、硬件加速:使用过程中,建议开启硬件加速,提高动画的流畅性。

  • 相关阅读:
    Chrome浏览器与常用插件推荐
    时间戳 转换24小时制
    fis3 开启相对地址
    web手机端禁止滑动,web手机端禁止上下滑动。
    rem的用法
    手机端复制,pc端复制
    ruby 镜像安装
    使用Potree渲染大规模点云-踩坑记录
    移动端真机调试神器-spy-debugger
    手撕Promise.any
  • 原文地址:https://www.cnblogs.com/huansky/p/7401342.html
Copyright © 2011-2022 走看看