欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

一看就喜欢的loading动画效果Android分析实现

程序员文章站 2024-02-20 23:58:28
还是比较有新意,复杂度也不是非常高,所以就花时间整理一下,我们先一起看下原gif图效果: 从效果上看,我们需要考虑以下几个问题: 1.叶子的随机产生;...

还是比较有新意,复杂度也不是非常高,所以就花时间整理一下,我们先一起看下原gif图效果:

一看就喜欢的loading动画效果Android分析实现

从效果上看,我们需要考虑以下几个问题:

  • 1.叶子的随机产生;
  • 2.叶子随着一条正余弦曲线移动;
  • 3.叶子在移动的时候旋转,旋转方向随机,正时针或逆时针;
  • 4.叶子遇到进度条,似乎是融合进入;
  • 5.叶子不能超出最左边的弧角;
  • 7.叶子飘出时的角度不是一致,走的曲线的振幅也有差别,否则太有规律性,缺乏美感;

总的看起来,需要注意和麻烦的地方主要是以上几点,当然还有一些细节问题,比如最左边是圆弧等等;
那接下来我们将效果进行分解,然后逐个击破:
整个效果来说,我们需要的图主要是飞动的小叶子和右边旋转的风扇,其他的部分都可以用色值进行绘制,当然我们为了方便,就连底部框一起切了;
先从gif 图里把飞动的小叶子和右边旋转的风扇、底部框抠出来,小叶子图如下:

                         一看就喜欢的loading动画效果Android分析实现                

我们需要处理的主要有两个部分:

  • 1. 随着进度往前绘制的进度条;
  • 2. 不断飞出来的小叶片;

我们先处理第一部分 - 随着进度往前绘制的进度条:
进度条的位置根据外层传入的 progress 进行计算,可以分为图中 1、2、3 三个阶段:

一看就喜欢的loading动画效果Android分析实现

  • 1. 当progress 较小,算出的当前距离还在弧形以内时,需要绘制如图所示 1 区域的弧形,其余部分用白色填充;
  • 2. 当 progress 算出的距离到2时,需要绘制棕色半圆弧形,其余部分用白色矩形填充;
  • 3. 当 progress 算出的距离到3 时,需要绘制棕色半圆弧形,棕色矩形,白色矩形;
  • 4. 当 progress 算出的距离到头时,需要绘制棕色半圆弧形,棕色矩形;(可以合并到3中)

首先根据进度条的宽度和当前进度、总进度算出当前的位置:

//mprogresswidth为进度条的宽度,根据当前进度算出进度条的位置 
mcurrentprogressposition = mprogresswidth * mprogress / total_progress; 

然后按照上面的逻辑进行绘制,其中需要计算上图中的红色弧角角度,计算方法如下:

// 单边角度 
int angle = (int) math.todegrees(math.acos((marcradius - mcurrentprogressposition)/ (float) marcradius)); 

math.acos()  -反余弦函数;
math.todegrees() - 弧度转化为角度,math.toradians 角度转化为弧度
所以圆弧的起始点为:

int startangle = 180 - angle; 

圆弧划过的角度为:
2 * angle 

这一块的代码如下:

// mprogresswidth为进度条的宽度,根据当前进度算出进度条的位置 
mcurrentprogressposition = mprogresswidth * mprogress / total_progress; 
// 即当前位置在图中所示1范围内 
if (mcurrentprogressposition < marcradius) { 
  log.i(tag, "mprogress = " + mprogress + "---mcurrentprogressposition = " 
      + mcurrentprogressposition 
      + "--marcprogresswidth" + marcradius); 
  // 1.绘制白色arc,绘制orange arc 
  // 2.绘制白色矩形 
 
  // 1.绘制白色arc 
  canvas.drawarc(marcrectf, 90, 180, false, mwhitepaint); 
 
  // 2.绘制白色矩形 
  mwhiterectf.left = marcrightlocation; 
  canvas.drawrect(mwhiterectf, mwhitepaint); 
 
  // 3.绘制棕色 arc 
  // 单边角度 
  int angle = (int) math.todegrees(math.acos((marcradius - mcurrentprogressposition) 
      / (float) marcradius)); 
  // 起始的位置 
  int startangle = 180 - angle; 
  // 扫过的角度 
  int sweepangle = 2 * angle; 
  log.i(tag, "startangle = " + startangle); 
  canvas.drawarc(marcrectf, startangle, sweepangle, false, morangepaint); 
} else { 
  log.i(tag, "mprogress = " + mprogress + "---transfer-----mcurrentprogressposition = " 
      + mcurrentprogressposition 
      + "--marcprogresswidth" + marcradius); 
  // 1.绘制white rect 
  // 2.绘制orange arc 
  // 3.绘制orange rect 
   
  // 1.绘制white rect 
  mwhiterectf.left = mcurrentprogressposition; 
  canvas.drawrect(mwhiterectf, mwhitepaint); 
   
  // 2.绘制orange arc 
  canvas.drawarc(marcrectf, 90, 180, false, morangepaint); 
  // 3.绘制orange rect 
  morangerectf.left = marcrightlocation; 
  morangerectf.right = mcurrentprogressposition; 
  canvas.drawrect(morangerectf, morangepaint); 
 
} 



接下来再来看叶子部分:
首先根据效果情况基本确定出 曲线函数,标准函数方程为:y = a(wx+q)+h,其中w影响周期,a影响振幅 ,周期t= 2 * math.pi/w;
根据效果可以看出,周期大致为总进度长度,所以确定w=(float) ((float) 2 * math.pi /mprogresswidth);

仔细观察效果,我们可以发现,叶子飘动的过程中振幅不是完全一致的,产生一种错落的效果,既然如此,我们给叶子定义一个type,根据type 确定不同的振幅;
我们创建一个叶子对象:

private class leaf { 
 
   // 在绘制部分的位置 
   float x, y; 
   // 控制叶子飘动的幅度 
   starttype type; 
   // 旋转角度 
   int rotateangle; 
   // 旋转方向--0代表顺时针,1代表逆时针 
   int rotatedirection; 
   // 起始时间(ms) 
   long starttime; 
 } 

类型采用枚举进行定义,其实就是用来区分不同的振幅:

private enum starttype { 
  little, middle, big 
} 

创建一个leaffactory类用于创建一个或多个叶子信息:

private class leaffactory { 
  private static final int max_leafs = 6; 
  random random = new random(); 
 
  // 生成一个叶子信息 
  public leaf generateleaf() { 
    leaf leaf = new leaf(); 
    int randomtype = random.nextint(3); 
    // 随时类型- 随机振幅 
    starttype type = starttype.middle; 
    switch (randomtype) { 
      case 0: 
        break; 
      case 1: 
        type = starttype.little; 
        break; 
      case 2: 
        type = starttype.big; 
        break; 
      default: 
        break; 
    } 
    leaf.type = type; 
    // 随机起始的旋转角度 
    leaf.rotateangle = random.nextint(360); 
    // 随机旋转方向(顺时针或逆时针) 
    leaf.rotatedirection = random.nextint(2); 
    // 为了产生交错的感觉,让开始的时间有一定的随机性 
    maddtime += random.nextint((int) (leaf_float_time * 1.5)); 
    leaf.starttime = system.currenttimemillis() + maddtime; 
    return leaf; 
  } 
 
  // 根据最大叶子数产生叶子信息 
  public list<leaf> generateleafs() { 
    return generateleafs(max_leafs); 
  } 
 
  // 根据传入的叶子数量产生叶子信息 
  public list<leaf> generateleafs(int leafsize) { 
    list<leaf> leafs = new linkedlist<leaf>(); 
    for (int i = 0; i < leafsize; i++) { 
      leafs.add(generateleaf()); 
    } 
    return leafs; 
  } 
} 

定义两个常亮分别记录中等振幅和之间的振幅差:

// 中等振幅大小 
private static final int middle_amplitude = 13; 
// 不同类型之间的振幅差距 
private static final int amplitude_disparity = 5; 
[html] view plain copy 在code上查看代码片派生到我的代码片
// 中等振幅大小 
private int mmiddleamplitude = middle_amplitude; 
// 振幅差 
private int mamplitudedisparity = amplitude_disparity; 

有了以上信息,我们则可以获取到叶子的y值:

// 通过叶子信息获取当前叶子的y值 
private int getlocationy(leaf leaf) { 
  // y = a(wx+q)+h 
  float w = (float) ((float) 2 * math.pi / mprogresswidth); 
  float a = mmiddleamplitude; 
  switch (leaf.type) { 
    case little: 
      // 小振幅 = 中等振幅 - 振幅差 
      a = mmiddleamplitude - mamplitudedisparity; 
      break; 
    case middle: 
      a = mmiddleamplitude; 
      break; 
    case big: 
      // 小振幅 = 中等振幅 + 振幅差 
      a = mmiddleamplitude + mamplitudedisparity; 
      break; 
    default: 
      break; 
  } 
  log.i(tag, "---a = " + a + "---w = " + w + "--leaf.x = " + leaf.x); 
  return (int) (a * math.sin(w * leaf.x)) + marcradius * 2 / 3; 
} 

接下来,我们开始绘制叶子:

/** 
 * 绘制叶子 
 *  
 * @param canvas 
 */ 
private void drawleafs(canvas canvas) { 
  long currenttime = system.currenttimemillis(); 
  for (int i = 0; i < mleafinfos.size(); i++) { 
    leaf leaf = mleafinfos.get(i); 
    if (currenttime > leaf.starttime && leaf.starttime != 0) { 
      // 绘制叶子--根据叶子的类型和当前时间得出叶子的(x,y) 
      getleaflocation(leaf, currenttime); 
      // 根据时间计算旋转角度 
      canvas.save(); 
      // 通过matrix控制叶子旋转 
      matrix matrix = new matrix(); 
      float transx = mleftmargin + leaf.x; 
      float transy = mleftmargin + leaf.y; 
      log.i(tag, "left.x = " + leaf.x + "--leaf.y=" + leaf.y); 
      matrix.posttranslate(transx, transy); 
      // 通过时间关联旋转角度,则可以直接通过修改leaf_rotate_time调节叶子旋转快慢 
      float rotatefraction = ((currenttime - leaf.starttime) % leaf_rotate_time) 
          / (float) leaf_rotate_time; 
      int angle = (int) (rotatefraction * 360); 
      // 根据叶子旋转方向确定叶子旋转角度 
      int rotate = leaf.rotatedirection == 0 ? angle + leaf.rotateangle : -angle 
          + leaf.rotateangle; 
      matrix.postrotate(rotate, transx 
          + mleafwidth / 2, transy + mleafheight / 2); 
      canvas.drawbitmap(mleafbitmap, matrix, mbitmappaint); 
      canvas.restore(); 
    } else { 
      continue; 
    } 
  } 
} 

最后,向外层暴露几个接口:

/** 
 * 设置中等振幅 
 *  
 * @param amplitude 
 */ 
public void setmiddleamplitude(int amplitude) { 
  this.mmiddleamplitude = amplitude; 
} 
 
/** 
 * 设置振幅差 
 *  
 * @param disparity 
 */ 
public void setmplitudedisparity(int disparity) { 
  this.mamplitudedisparity = disparity; 
} 
 
/** 
 * 获取中等振幅 
 *  
 * @param amplitude 
 */ 
public int getmiddleamplitude() { 
  return mmiddleamplitude; 
} 
 
/** 
 * 获取振幅差 
 *  
 * @param disparity 
 */ 
public int getmplitudedisparity() { 
  return mamplitudedisparity; 
} 
 
/** 
 * 设置进度 
 *  
 * @param progress 
 */ 
public void setprogress(int progress) { 
  this.mprogress = progress; 
  postinvalidate(); 
} 
 
/** 
 * 设置叶子飘完一个周期所花的时间 
 *  
 * @param time 
 */ 
public void setleaffloattime(long time) { 
  this.mleaffloattime = time; 
} 
 
/** 
 * 设置叶子旋转一周所花的时间 
 *  
 * @param time 
 */ 
public void setleafrotatetime(long time) { 
  this.mleafrotatetime = time; 

这些接口用来干嘛呢?用于把我们的动效做成完全可手动调节的,这样做有什么好处呢?
1. 更加便于产品、射鸡湿查看效果,避免yy,自己手动调节,不会出现要你一遍遍的改参数安装、查看、再改、再查看... ... n遍之后说 “这好像不是我想要的” -- 瞬间天崩地裂,天昏地暗,感觉被全世界抛弃;
2. 便于体现你是一个考虑全面,思维缜密,会编程、会设计的艺术家,当然这纯属yy,主要还是方便大家;

如此一来,射鸡湿们只需要不断的调节即可实时的看到展现的效果,最后只需要把最终的参数反馈过来即可,万事大吉,一了百了;
当然,如果对方是个漂亮的妹子,而你又苦于没有机会搭讪,以上内容就当我没说,尽情的不按要求写吧,她肯定会主动找你的,说不定连饭都反过来请了... ...

好啦,言归正传,完成收尾部分,我们让所有的参数都可调节起来:
把剩下的layout 和activity贴出来:
activity:

public class leafloadingactivity extends activity implements onseekbarchangelistener, 
    onclicklistener { 
 
  handler mhandler = new handler() { 
    public void handlemessage(message msg) { 
      switch (msg.what) { 
        case refresh_progress: 
          if (mprogress < 40) { 
            mprogress += 1; 
            // 随机800ms以内刷新一次 
            mhandler.sendemptymessagedelayed(refresh_progress, 
                new random().nextint(800)); 
            mleafloadingview.setprogress(mprogress); 
          } else { 
            mprogress += 1; 
            // 随机1200ms以内刷新一次 
            mhandler.sendemptymessagedelayed(refresh_progress, 
                new random().nextint(1200)); 
            mleafloadingview.setprogress(mprogress); 
 
          } 
          break; 
 
        default: 
          break; 
      } 
    }; 
  }; 
 
  private static final int refresh_progress = 0x10; 
  private leafloadingview mleafloadingview; 
  private seekbar mampireseekbar; 
  private seekbar mdistanceseekbar; 
  private textview mmplitudetext; 
  private textview mdisparitytext; 
  private view mfanview; 
  private button mclearbutton; 
  private int mprogress = 0; 
 
  private textview mprogresstext; 
  private view maddprogress; 
  private seekbar mfloattimeseekbar; 
 
  private seekbar mrotatetimeseekbar; 
  private textview mfloattimetext; 
  private textview mrotatetimetext; 
 
  @override 
  protected void oncreate(bundle savedinstancestate) { 
    super.oncreate(savedinstancestate); 
    setcontentview(r.layout.leaf_loading_layout); 
    initviews(); 
    mhandler.sendemptymessagedelayed(refresh_progress, 3000); 
  } 
 
  private void initviews() { 
    mfanview = findviewbyid(r.id.fan_pic); 
    rotateanimation rotateanimation = dxanimationutils.initrotateanimation(false, 1500, true, 
        animation.infinite); 
    mfanview.startanimation(rotateanimation); 
    mclearbutton = (button) findviewbyid(r.id.clear_progress); 
    mclearbutton.setonclicklistener(this); 
 
    mleafloadingview = (leafloadingview) findviewbyid(r.id.leaf_loading); 
    mmplitudetext = (textview) findviewbyid(r.id.text_ampair); 
    mmplitudetext.settext(getstring(r.string.current_mplitude, 
        mleafloadingview.getmiddleamplitude())); 
 
    mdisparitytext = (textview) findviewbyid(r.id.text_disparity); 
    mdisparitytext.settext(getstring(r.string.current_disparity, 
        mleafloadingview.getmplitudedisparity())); 
 
    mampireseekbar = (seekbar) findviewbyid(r.id.seekbar_ampair); 
    mampireseekbar.setonseekbarchangelistener(this); 
    mampireseekbar.setprogress(mleafloadingview.getmiddleamplitude()); 
    mampireseekbar.setmax(50); 
 
    mdistanceseekbar = (seekbar) findviewbyid(r.id.seekbar_distance); 
    mdistanceseekbar.setonseekbarchangelistener(this); 
    mdistanceseekbar.setprogress(mleafloadingview.getmplitudedisparity()); 
    mdistanceseekbar.setmax(20); 
 
    maddprogress = findviewbyid(r.id.add_progress); 
    maddprogress.setonclicklistener(this); 
    mprogresstext = (textview) findviewbyid(r.id.text_progress); 
 
    mfloattimetext = (textview) findviewbyid(r.id.text_float_time); 
    mfloattimeseekbar = (seekbar) findviewbyid(r.id.seekbar_float_time); 
    mfloattimeseekbar.setonseekbarchangelistener(this); 
    mfloattimeseekbar.setmax(5000); 
    mfloattimeseekbar.setprogress((int) mleafloadingview.getleaffloattime()); 
    mfloattimetext.settext(getresources().getstring(r.string.current_float_time, 
        mleafloadingview.getleaffloattime())); 
 
    mrotatetimetext = (textview) findviewbyid(r.id.text_rotate_time); 
    mrotatetimeseekbar = (seekbar) findviewbyid(r.id.seekbar_rotate_time); 
    mrotatetimeseekbar.setonseekbarchangelistener(this); 
    mrotatetimeseekbar.setmax(5000); 
    mrotatetimeseekbar.setprogress((int) mleafloadingview.getleafrotatetime()); 
    mrotatetimetext.settext(getresources().getstring(r.string.current_float_time, 
        mleafloadingview.getleafrotatetime())); 
  } 
 
  @override 
  public void onprogresschanged(seekbar seekbar, int progress, boolean fromuser) { 
    if (seekbar == mampireseekbar) { 
      mleafloadingview.setmiddleamplitude(progress); 
      mmplitudetext.settext(getstring(r.string.current_mplitude, 
          progress)); 
    } else if (seekbar == mdistanceseekbar) { 
      mleafloadingview.setmplitudedisparity(progress); 
      mdisparitytext.settext(getstring(r.string.current_disparity, 
          progress)); 
    } else if (seekbar == mfloattimeseekbar) { 
      mleafloadingview.setleaffloattime(progress); 
      mfloattimetext.settext(getresources().getstring(r.string.current_float_time, 
          progress)); 
    } 
    else if (seekbar == mrotatetimeseekbar) { 
      mleafloadingview.setleafrotatetime(progress); 
      mrotatetimetext.settext(getresources().getstring(r.string.current_rotate_time, 
          progress)); 
    } 
 
  } 
 
  @override 
  public void onstarttrackingtouch(seekbar seekbar) { 
 
  } 
 
  @override 
  public void onstoptrackingtouch(seekbar seekbar) { 
 
  } 
 
  @override 
  public void onclick(view v) { 
    if (v == mclearbutton) { 
      mleafloadingview.setprogress(0); 
      mhandler.removecallbacksandmessages(null); 
      mprogress = 0; 
    } else if (v == maddprogress) { 
      mprogress++; 
      mleafloadingview.setprogress(mprogress); 
      mprogresstext.settext(string.valueof(mprogress)); 
    } 
  } 
} 

layout:

<?xml version="1.0" encoding="utf-8"?> 
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent" 
  android:background="#fed255" 
  android:orientation="vertical" > 
 
  <textview 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center_horizontal" 
    android:layout_margintop="100dp" 
    android:text="loading ..." 
    android:textcolor="#ffa800" 
    android:textsize=" 30dp" /> 
 
  <relativelayout 
    android:id="@+id/leaf_content" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:layout_margintop="50dp" > 
 
    <com.baidu.batterysaverdemo.ui.leafloadingview 
      android:id="@+id/leaf_loading" 
      android:layout_width="302dp" 
      android:layout_height="61dp" 
      android:layout_centerhorizontal="true" /> 
 
    <imageview 
      android:id="@+id/fan_pic" 
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:layout_alignparentright="true" 
      android:layout_centervertical="true" 
      android:layout_marginright="35dp" 
      android:src="@drawable/fengshan" /> 
  </relativelayout> 
 
  <scrollview 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" > 
 
    <linearlayout 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      android:orientation="vertical" > 
 
      <linearlayout 
        android:id="@+id/seek_content_one" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginleft="15dp" 
        android:layout_marginright="15dp" 
        android:layout_margintop="15dp" > 
 
        <textview 
          android:id="@+id/text_ampair" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:layout_gravity="center_vertical" 
          android:textcolor="#ffffa800" 
          android:textsize="15dp" /> 
 
        <seekbar 
          android:id="@+id/seekbar_ampair" 
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_marginleft="5dp" 
          android:layout_weight="1" /> 
      </linearlayout> 
 
      <linearlayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginleft="15dp" 
        android:layout_marginright="15dp" 
        android:layout_margintop="15dp" 
        android:orientation="horizontal" > 
 
        <textview 
          android:id="@+id/text_disparity" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:layout_gravity="center_vertical" 
          android:textcolor="#ffffa800" 
          android:textsize="15dp" /> 
 
        <seekbar 
          android:id="@+id/seekbar_distance" 
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_marginleft="5dp" 
          android:layout_weight="1" /> 
      </linearlayout> 
 
      <linearlayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginleft="15dp" 
        android:layout_marginright="15dp" 
        android:layout_margintop="15dp" 
        android:orientation="horizontal" > 
 
        <textview 
          android:id="@+id/text_float_time" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:layout_gravity="center_vertical" 
          android:textcolor="#ffffa800" 
          android:textsize="15dp" /> 
 
        <seekbar 
          android:id="@+id/seekbar_float_time" 
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_marginleft="5dp" 
          android:layout_weight="1" /> 
      </linearlayout> 
 
      <linearlayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginleft="15dp" 
        android:layout_marginright="15dp" 
        android:layout_margintop="15dp" 
        android:orientation="horizontal" > 
 
        <textview 
          android:id="@+id/text_rotate_time" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:layout_gravity="center_vertical" 
          android:textcolor="#ffffa800" 
          android:textsize="15dp" /> 
 
        <seekbar 
          android:id="@+id/seekbar_rotate_time" 
          android:layout_width="0dp" 
          android:layout_height="wrap_content" 
          android:layout_marginleft="5dp" 
          android:layout_weight="1" /> 
      </linearlayout> 
 
      <button 
        android:id="@+id/clear_progress" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_margintop="15dp" 
        android:text="去除进度条,玩转弧线" 
        android:textsize="18dp" /> 
 
      <linearlayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_marginleft="15dp" 
        android:layout_marginright="15dp" 
        android:layout_margintop="15dp" 
        android:orientation="horizontal" > 
 
        <button 
          android:id="@+id/add_progress" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:text="增加进度: " 
          android:textsize="18dp" /> 
 
        <textview 
          android:id="@+id/text_progress" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:layout_gravity="center_vertical" 
          android:textcolor="#ffffa800" 
          android:textsize="15dp" /> 
      </linearlayout> 
    </linearlayout> 
  </scrollview> 
 
</linearlayout> 

最终效果如下,本来录了20+s,但是ps只能转5s,所以有兴趣的大家自己运行的玩吧:

一看就喜欢的loading动画效果Android分析实现

以上就是本文的全部内容,希望对大家的学习有所帮助。