美文网首页
Android 动画的使用

Android 动画的使用

作者: simplehych | 来源:发表于2019-08-13 20:04 被阅读0次

最新总结,请移步 Android 动画解析

0x01 分类

视图动画(逐帧动画/补间动画)
属性动画
转场动画

1. 逐帧动画

frame-by-frame animation

包位置:android.graphics.drawable
类名:AnimationDrawable

将多张图片,连贯起来进行播放,类似动画片的工作性质。

2. 补间动画

tweened animation

包位置:android.view.animation
类名:Animation

仅对 View 进行淡入淡出 alpha、平移 translate、旋转 rotate、缩放 scale 四种操作。不可对 View 的属性做操作,例如颜色、长度等

AlphaAnimation
TraslateAnimation
RotateAnimation
ScaleAnimation
AnimationSet

Interpolator
AnimationUtils

3. 属性动画

Property Animator

包位置:android.animation
类名:Animator
Android3.0引入,弥补了补间动画的缺陷(只能对 View 进行操作)

ViewPropertyAnimator(包位于android.view,内部使用ValueAnimator)

ObjectAnimator
ValueAnimator

AnimatorInflater

PropertyValuesHolder 对同一动画属性操作
AnimatorSet 组合多个动画

Interpolator
TypeEvaluator

0x02 使用

1. 逐帧动画

方式1 XML实现

animation-list 标签指定;
oneshot 是否仅播放一次 false;
duration 每一帧的显示时长

# 注意位置 res/drawable/frame_animation.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/img_0"
        android:duration="50" />
    <item
        android:drawable="@drawable/img_1"
        android:duration="50" />
    <item
        android:drawable="@drawable/img_2"
        android:duration="50" />
</animation-list >

--- // 代码加载
ImageView imageView  = new ImageView(this);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();
// 播放开始
animationDrawable.start();
// 播放结束
animationDrawable.stop();

方式2 代码实现

AnimationDrawable frameAnimation = new AnimationDrawable();
// 参数1 图片,参数2 出现时间
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_0), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_1), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_2), 50);
// 是否重复
frameAnimation.setOneShot(false); 
// 播放开始
frameAnimation.start();
// 播放结束
frameAnimation.stop();

优缺点

缺点:使用大量图片,易导致OOM
建议:直接使用 gif,大小可以明显改善

2. 补间动画

方式1 XML 实现

  1. 平移动画
# 注意位置 res/anim/view_animation.xml
<translate
 android:fromXDelta="0" // 水平x方向的起始值
 android:fromYDelta="0" // 竖直y方向的起始值
 android:toXDelta="500" // 水平x方向的结束值
 android:toYDelta="500" // 竖直y方向的结束值
/>
  1. 透明动画
<alpha
 android:fromAlpha="1.0" // 开始透明度 0.0-1.0
 android:toAlpha="0.0" //结束透明度 0.0-1.0
/>
  1. 旋转动画
<rotate
 android:fromDegrees="0" // 开始的角度
 android:toDegrees="180" // 结束的角度
 android:pivotX="20%" // 旋转中心点 x 坐标
 android:pivotY="20%" // 旋转中心点 y 坐标
/>
  1. 缩放动画
<scale
 android:fromXScale="0.0" // 起始缩放x的倍数
 android:fromYScale="0.0" // 起始缩放y的倍数
 android:toXScale="1.0" // 结束缩放x的倍数
 android:toYScale="2.0" // 结束缩放y的倍数
 android:pivotX="50%" // 缩放中心点的x坐标
 android:pivotY="50%" // 缩放中心点的y坐标
/>

pivotX / pivotY 的取值有三个类型

  1. 数字, eg:android:pivotX="50", 该View左上角在x方向上平移50px的点,对应Java代码中设置参数 Animation.ABSOLUTE
  2. 百分比, eg:android:pivotX="50%", 该View左上角在x方向上平移自身宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_SELF
  3. 百分比p (parent), eg:android:pivotX="50%p"该View左上角在x方向上平移父布局宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_PARENT
  1. 公共属性
android:duration="3000" // 动画时长
android:fillAfter="true" // 动画结束后,是否留在结束位置,优先级高于fillBefore属性,默认是false
android:fillBefore="false" //动画结束后,是否留在开始位置,经试验证明只有fillBefore=false没有fillAfter属性时,不会停留在结束位置,默认是true
android:fillEnabled="true" //是否应用fillBefore的值,对fillAfter无影响,默认是true
android:interpolator="@android:anim/overshoot_interpolator"
android:repeatCount="0" //infinite无限重复
android:repeatMode="restart" // restart正序重播/reverse反转重播

android:shareInterpolator="true"  // 组合动画属性,组合动画是否和集合(<set></set>)共享一个插值器,去过不指定,子动画需要单独设定
android:startOffset="100" //组合动画默认是全部动画同时开始,如果不同动画不同开始需要使用该属性延迟开始时间

  1. 在代码中使用
// 步骤1:创建 需要设置动画的 视图View
Button mButton = (Button) findViewById(R.id.Button);
// 步骤2:创建 动画对象 并传入设置的动画效果xml文件
Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
// 步骤3:播放动画
mButton.startAnimation(translateAnimation);

方式2 代码实现

Button mButton = (Button) findViewById(R.id.Button);

// 组合动画设置
// 步骤1:创建组合动画shareInterpolator对象(设置为true)
AnimationSet setAnimation = new AnimationSet(true);

// 步骤2:设置组合动画的属性
// 特别说明以下情况
// 因为在下面的旋转动画设置了无限循环(RepeatCount = INFINITE)
// 所以动画不会结束,而是无限循环
// 所以组合动画的下面两行设置是无效的
setAnimation.setRepeatMode(Animation.RESTART);
setAnimation.setRepeatCount(1);// 设置了循环一次,但无效

// 步骤3:逐个创建子动画(方式同单个动画创建方式,此处不作过多描述)

// 子动画1:旋转动画
Animation rotate = new RotateAnimation(0,360,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotate.setDuration(1000);
rotate.setRepeatMode(Animation.RESTART);
rotate.setRepeatCount(Animation.INFINITE);

// 子动画2:平移动画
Animation translate = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,-0.5f,
TranslateAnimation.RELATIVE_TO_PARENT,0.5f,
TranslateAnimation.RELATIVE_TO_SELF,0
,TranslateAnimation.RELATIVE_TO_SELF,0);
translate.setDuration(10000);

// 子动画3:透明度动画
Animation alpha = new AlphaAnimation(1,0);
alpha.setDuration(3000);
alpha.setStartOffset(7000);

// 子动画4:缩放动画
Animation scale1 = new ScaleAnimation(1,0.5f,1,0.5f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
scale1.setDuration(1000);
scale1.setStartOffset(4000);

// 步骤4:将创建的子动画添加到组合动画里
setAnimation.addAnimation(alpha);
setAnimation.addAnimation(rotate);
setAnimation.addAnimation(translate);
setAnimation.addAnimation(scale1);

// 步骤5:播放动画
mButton.startAnimation(setAnimation);

优缺点

缺点:

  1. 作用对象局限:根据包分类可知 android.view.animation,只针对View进行动画操作
  2. 只改变视觉效果,没有改变属性,例如点击只在原始位置有效(经API27测试,当属性动画View完全不可见时,点击位置和范围为原始位置)
  3. 动画效果单一

3. 属性动画

ObjectAnimator extends ValueAnimator extends Animator 位于 android.animation

ViewPropertyAnimator 位于 android.view ,其本质内部使用ValueAnimator

ViewPropertyAnimator - ObjectAnimator - ValueAnimator

使用难度越来越高,但越来越灵活。

举个例子:

View view = new View(this);

# ViewPropertyAnimator
view.animate().alphaBy(0.8f) // xxxBy 以当前状态为参考,translate等同理, 默认时间为300ms

# ObjectAnimator
ObjectAnimator objectAnimator = Object.ofFloat(view, "alpha", 1.0f, 0.0f, 1.0f);
objectAnimator.start();
// "alpha" 的本质不是view的属性,而是view的setAlpha 和 getAlpha方法,然后再setAlpha方法中调用 invalidate() 方法不断重绘。方法名严格,属性名可以不同,如mAlpha等,待查看源码体现

# ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f);
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
 public void onAnimationUpdate(ValueAnimator animation) {
        float animatedValue = (float) animation.getAnimatedValue();
        view.setAlpha(animatedValue);
 }
});
valueAnimator.start();

3.1 ViewPropertyAnimator

为了满足面向对象编程思想,引入了ViewPropertyAnimator,可以链式调用。但是没有 和setRepeatMode() 和 setRepeatCount() 方法

View 中的方法 功能 对应 ViewPropertyAnimator 的方法
setTranslationX() 设置x轴偏移 translationX() translationXBy()
setTranslationY() 设置y轴偏移 translationY() translationYBy()
setTranslationZ() 设置z轴偏移 translationZ() translationZBy()
setX() 设置x轴绝对位置 x() xBy()
setY() 设置y轴绝对位置 y() yBy()
setZ() 设置z轴绝对位置 z() zBy()
setRotation() 设置平面旋转 rotation() rotationBy()
setRotationX() 设置沿x轴旋转 rotationX() rotationXBy()
setRotationY() 设置沿y轴旋转 rotationY() rotationYBy()
setScaleX() 设置横向放缩 scaleX() scaleXBy()
setScaleY() 设置纵向放缩 scaleY() scaleYBy()
setAlpha() 设置透明度 alpha() alphaBy()
  1. View#setX()没有动画渐变效果,直接将该View放到设置位置;而 ViewPropertyAnimator#x() 有平移过渡动画。
  2. ViewPropertyAnimator#x() 如果有动画执行x()将取消。例如view.x(500).translationX(50),将只执行translationX(50),不执行x(500)
  3. ViewPropertyAnimator#By() 表示在当前位置的基础上进行操作

3.2 Animator的使用

方式1 XML实现
# 注意位置 res/animator/view_animator.xml

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">
    
    <objectAnimator
        android:duration="2000"
        android:propertyName="translationX"
        android:valueFrom="0"
        android:valueTo="500"
        android:valueType="floatType" />
    
    <set android:ordering="together">
        
        <objectAnimator
            android:duration="3000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360"
            android:valueType="floatType" />
        
        <set android:ordering="sequentially">
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="1"
                android:valueTo="0"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="0"
                android:valueTo="1"
                android:valueType="floatType" />
        </set>
    </set>
</set>


--- // 代码调用,注意区分animation AnimationUtils

Animator animator = AnimatorInflater.loadAnimator(context,R.animator.view_animator);
animator.setTarget(view);
animator.start();

方式2 Java代码实现
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0, 500f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).before(moveIn); //  with / before / after
animSet.setDuration(5000);
animSet.start();
自定义控件实现

步骤

  1. 添加setter / getter 方法
  2. 用ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象(或者使用ValueAnimator的方式)
  3. start()执行动画
public class SportsView extends View {  
    float progress = 0;

    ......

    // 创建 getter 方法
    public float getProgress() {
        return progress;
    }

    // 创建 setter 方法
    public void setProgress(float progress) {
        this.progress = progress;
        invalidate();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ......

        canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);

        ......
    }
}

......

// 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);  
// 执行动画
animator.start(); 

大部分情况下,初始位置和结束位置不能提前确定,所有一般使用代码实现。

对比ObjectAnimator和ValueAnimator的使用

ofFloat 对比

// 注意setTranslationX参数是float类型,ofInt不能用translationX属性
// View # setTranslationX(float translationX)
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationX", 0.0f, 500.0f);
objectAnimator.start();

// value 在回调中更新view的状态
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.0f, 500.0f);
valueAnimator.addUpdateListener(new valueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
          float animatedValue = (float) animation.getAnimatedValue();
          view.setTranslationX(animatedValue);
          view.requestLayout();
      }
});
valueAnimator.start();

ofObject 对比, 需要自定义 TypeEvaluator<T>

ObjectAnimator objectAnimator = ObjectAnimator.ofObject(view, "customFiled", new MyObjectEvaluator(), object1, object2);
objectAnimator.start();
  
---

ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyObjectEvaluator(), object1, object2);
valueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
          MyObject value = (MyObject) animation.getAnimatedValue();
          view.setCustomFiled(value);
          view.requestLayout();
      }
});
valueAnimator.start();

4. 插值器和估值器

4.1 对比区别

类型 定义 作用
插值器 Interpolator 一个辅助动画实现的接口 确定属性值0-1变化规律
估值器 TypeEvaluator 一个协助插值器的接口,属性动画特有属性 计算属性值0-1具体数值(通过变化规律计算数字)

4.2 插值器

4.2.1 本质

根据动画的进度(0% - 100%)计算当前属性值改变的百分比

4.2.2 实现
  1. 实现下面两个接口
    android.animation # interface TimeInterpolator
    android.view.animation / interface Interpolator extends TimeInterpolator

  2. 重写 float getInterpolation(float input) 方法

补间动画实现 Interpolator接口;属性动画实现TimeInterpolator接口。
TimeInterpolator 接口是属性动画中新增的,用于兼容Interpolator 接口,这使原先的Interpolator实现累都可以直接在属性动画使用

4.2.3 举例说明
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {  
      // 仅贴出关键代码
  ...
    public float getInterpolation(float input) {  
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
        // input的运算逻辑如下:
        // 使用了余弦函数,因input的取值范围是0到1,那么cos函数中的取值范围就是π到2π。
        // 而cos(π)的结果是-1,cos(2π)的结果是1
        // 所以该值除以2加上0.5后,getInterpolation()方法最终返回的结果值还是在0到1之间。只不过经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程
        // 所以最终,fraction值 = 运算后的值 = 先加速后减速
        // 所以该差值器是先加速再减速的
    }  
}
4.2.4 列举官方插值器
Java类 描述
LinearInterpolator 匀速
AccelerateInterpolator 加速
DecelerateInterpolator 减速
AccelerateDecelerateInterpolator 先加速后减速(默认)
AnticipateInterpolator 先退后然后加速前进
OvershootInterpolator 完成动画,超出后回到结束位置
AnticipateOvershootInterpolator 先退后再加速前进,超出终点后再回终点
BounceInterpolator 最后阶段弹球效果
CycleInterpolator 周期运动
PathInterpolator 自定义动画完成度/时间完成度曲线(0-1)
FastOutLinearInInterpolator 加速
LinearOutSlowInInterpolator 持续减速
FastOutSlowInInterpolator 先加速再减速

最后三个是Android5.0(API21)新增,和之前的类似,但是轨迹稍有区别

4.3 估值器

4.3.1 本质

根据当前属性值变化的百分比初始值结束值来计算当前属性的具体数值。当前属性值 = 初始值 + (结束值 - 初始值) * 百分比

4.3.2 实现
  1. 实现TypeEvaluator<T> 接口
  2. 重写public T evaluate(float fraction, T startValue, T endValue) 方法
4.3.3 官方示例
public class FloatEvaluator implements TypeEvaluator<Number> {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}
4.3.4 完整示例
public class Point {

    private float x;
    private float y;

    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}
public class PointEvaluator implements TypeEvaluator<Point> {

    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
        float startValueX = startValue.getX();
        float startValueY = startValue.getY();
        float currentX = startValueX + (endValue.getX() - startValueX) * fraction;
        float currentY = startValueY + (endValue.getY() - startValueY) * fraction;
        return new Point(currentX, currentY);
    }
}
public class PointView extends View {

    public static final float RADIUS = 70f;
    private Point ccurrentPoint;
    private Paint mPaint;

    public PointView(Context context) {
        this(context, null);
    }

    public PointView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (ccurrentPoint == null) {
            ccurrentPoint = new Point(RADIUS, RADIUS);
        }
        canvas.drawCircle(ccurrentPoint.getX(), ccurrentPoint.getY(), RADIUS, mPaint);
    }

    public Point getCurrentPoint() {
        return ccurrentPoint;
    }

    public void setCurrentPoint(Point currentPoint) {
        this.ccurrentPoint = currentPoint;
        invalidate();
    }
}
Point pointStart = new Point(70, 70);
Point pointEnd = new Point(700, 1000);

// 使用 ObjectAnimator
ObjectAnimator objectAnimator = ObjectAnimator.ofObject(point_view, "currentPoint", new PointEvaluator(), pointStart, pointEnd);
objectAnimator.setDuration(1000);
objectAnimator.start();
      
// 使用 ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvaluator(), pointStart, pointEnd);
valueAnimator.setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
     @Override
     public void onAnimationUpdate(ValueAnimator animation) {
          Point animatedValue = (Point) animation.getAnimatedValue();
          point_view.setCurrentPoint(animatedValue);
     }
});
valueAnimator.start();

5.设置监听器

添加监听器
ViewPropertyAnimator#setListener()/setUpdateListener()
ObjectAnimator#addListener()/addUpdateListener/addPauseListener()

移除监听器
ViewPropertyAnimator#setListener(null) / setUpdateListener(null) 填null来移除
ObjectAnimator #removeListener() / removeUpdateListener() / removePauseListener()

ObjectAnimator 支持pause()方法暂停
ViewPropertyAnimator 不支持setRepeatMode() / setRepeatCount() 方法
ViewPropertyAnimator 独有withStartAction(Runnable runnable) 和 withEndAction(Runnable runnable) 方法,可设置一次动画开始或结束的监听。即使重新开始动画,也不会回调,是一次性的。而AnimatorListener是持续有效的。
withEndAction() 只有在动画正常结束才会调用,而在动画被取消时是不会执行的。而 AnimatorListener.onAnimationEnd() 在取消之后也会被调用,在调用 onAnimationCancel()之后调用

6. PropertyValuesHolder 同一动画中改变多个属性 ObjectAnimator.ofPropertyValuesHolder

关键字:一边一边,一个动画属性同时执行,区别多个动画先后执行。一个动画需要共享开始时间/结束时间/Interpolator等等设定,PropertyValuesHolder不能有先后次序执行动画了。

很多时候,在同一个动画中需要改变多个属性,例如改变透明度的同时改变尺寸。

使用 ViewPropertyAnimator如下:

view.animate()
        .scaleX(0.0f)
        .scaleY(0.0f)
        .alpha(0.0f)

但是ObjectAnimator,是不能这么用的。需要使用PropertyValuesHolder来同时在一个动画里改变多个属性

PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 0.0f);  
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 0.0f);  
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 0.0f);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)  
animator.start();  

ViewPropertyAnimator 动画完成之后会停留在结束位置,再次点击执行动作操作不会执行动画
ObjectAnimator 动画完成之后会停留在结束位置,再次点击执行动作操作会从最原始状态重新执行一次动画。且Animator没有Animation的setFillAfter() 和setFillBefore()方法
关于点击范围的测试说明(API27),属性动画执行后属性发生变化,即点击范围和位置会更新,但经测试当View完全不可见时,点击位置和范围为原始位置

ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofMultiFloat()
ObjectAnimator.ofPropertyValuesHolder

6.AnimatorSet 多个动画配合执行

关键字:一边一边,一个动画属性同时执行,区别多个动画先后执行。一个动画需要共享开始时间/结束时间/Interpolator等等设定,PropertyValuesHolder不能有先后次序执行动画了。

区别 AnimationSet,只能通过设置单个动画的 setStartOffset 来延迟时间进行先后执行顺序。

animatorSet.play(a1) / playTogether(a1,a2) / playSequentially(a1,a2)
         .with(a3)
         .before(a4)
         .after(a5)

其中以 playXXX 开始得到 AnimatorSet.Builder 对象,然后调用with/before/after进行管理。

7.PropertyValuesHolders.ofKeyframe() 把同一个属性拆分

把一个属性拆分成多段,执行更加精细的属性动画。

Keyframe keyframe1 = Keyframe.ofFloat(0.0f, 0);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("translationX", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextMessage, holder);
animator.start();

0x03 参考资料

感谢以下文章作者
HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)
Android:这是一份全面 & 详细的补间动画使用教程

相关文章

网友评论

      本文标题:Android 动画的使用

      本文链接:https://www.haomeiwen.com/subject/axzkkqtx.html