├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── taovo │ │ └── rjp │ │ └── propertyanim │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── taovo │ │ │ └── rjp │ │ │ └── propertyanim │ │ │ ├── ChangePageActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── ScrollNumberActivity.java │ │ │ ├── SeeBookActivity.java │ │ │ ├── bullet_screen │ │ │ ├── Bullet.java │ │ │ ├── BulletActivity.java │ │ │ ├── BulletScreenView.java │ │ │ └── BulletView.java │ │ │ ├── evaluator │ │ │ ├── BookEvaluator.java │ │ │ ├── BookValue.java │ │ │ ├── NumberEvaluator.java │ │ │ └── README.md │ │ │ └── view │ │ │ ├── BookOpenView.java │ │ │ ├── BookView.java │ │ │ └── README.md │ └── res │ │ ├── drawable │ │ ├── bg_bullet_view.xml │ │ └── test.png │ │ ├── layout │ │ ├── activity_bullet.xml │ │ ├── activity_change_page.xml │ │ ├── activity_main.xml │ │ ├── activity_scroll_number.xml │ │ ├── activity_see_book.xml │ │ ├── layout_book_open_view.xml │ │ ├── layout_book_view.xml │ │ └── layout_bullet_view.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── headview.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── zan.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── taovo │ └── rjp │ └── propertyanim │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | /.idea 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 一、先看网易中奖奖池的效果 2 | ![wangyi.gif](http://upload-images.jianshu.io/upload_images/5994029-7cda47520a8caf9e.gif?imageMogr2/auto-orient/strip) 3 | 4 | ### 二、思路 5 | 明显的动画效果,而且是随着时间的推移,前面的数字暂停下来。数字的滚动用线程?!当然可以实现。但是前面的数字停下,这种效果不好处理。我开始想的是从最大的数字开始跑,一直减到0,在跑的过程中比对数字的位数,高位的就截取最终的结果,低位的显示当前的计数。同样的很麻烦,而且控制不了显示的位数,万一显示上亿,GG了。 6 | 换思路,我发现网易的动画里每个位置上的数字都是很随机的出现,不是规律的递减。那就获取最终结果的位数,然后线程里随机0到9: 7 | ``` 8 | StringBuilder sb = new StringBuilder(); 9 | for (int i = 0; i < size; i++) { 10 | sb.append((int) Math.ceil(Math.random() * 9)); 11 | } 12 | return sb.toString(); 13 | ``` 14 | 如果size是5,返回的一直是一个五位数,当很快速的刷新的时候,效果就很动画了。 15 | 上述代码实际测试可以实现。但是要想实现前面的数字暂停下来的效果,又没有了思路。 16 | 17 | ### 三、巧合 18 | 在搜索的时候,发现属性动画里有一个参数fraction,一直是从0到1(左右包含)的变化,感觉很有戏。因为这个暂停的数字看作进度,也是从\[0,1\]区间里变化。 19 | 20 | #### 3.1 定义估值器 21 | ``` 22 | public class NumberEvaluator implements TypeEvaluator { 23 | 24 | @Override 25 | public String evaluate(float fraction, String startValue, String endValue) { 26 | int size = endValue.length(); 27 | StringBuilder sb = new StringBuilder(); 28 | for (int i = 0; i < size; i++) { 29 | if(i * 1.0 / size > fraction) { 30 | sb.append((int) Math.ceil(Math.random() * 9)); 31 | }else{ 32 | sb.append(String.valueOf(endValue).charAt(i)); 33 | } 34 | } 35 | return sb.toString(); 36 | } 37 | } 38 | ``` 39 | 这个类就是传入一个开始值和一个结束值,然后返回无数的中间值,这个fraction默认0到1变化(不知道可不可以改变)。我们遍历显示的位数,如果它与整体的比值大于fraction,说明进度还没有到,如果小于,说明已经过了这个进度,画个图示意一下: 40 | 41 | ![test.png](http://upload-images.jianshu.io/upload_images/5994029-8f598a46c167e6d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 42 | 43 | 就是随着时间的流逝,返回最终值得前几位和随机值的组合。 44 | 45 | ### 四、代码实现 46 | ``` 47 | ValueAnimator numberAnim = ValueAnimator.ofObject(new NumberEvaluator(), "0", "676767676767"); 48 | numberAnim.setDuration(3000); 49 | numberAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 50 | @Override 51 | public void onAnimationUpdate(ValueAnimator animation) { 52 | String number = (String) animation.getAnimatedValue(); 53 | tvNumber.setText(number); 54 | } 55 | }); 56 | numberAnim.start(); 57 | numberAnim.addListener(new Animator.AnimatorListener() { 58 | @Override 59 | public void onAnimationStart(Animator animation) { 60 | 61 | } 62 | 63 | @Override 64 | public void onAnimationEnd(Animator animation) { 65 | tvNumber.setText("676767676767"); 66 | } 67 | 68 | @Override 69 | public void onAnimationCancel(Animator animation) { 70 | 71 | } 72 | 73 | @Override 74 | public void onAnimationRepeat(Animator animation) { 75 | 76 | } 77 | }); 78 | ``` 79 | new一个定义的估值器扔进去,起始值不关心,传空,传一个最终的显示值,然后动画里不断的监听拿到中间值设置进去,动画结束,把最终的值设置上,结束。 80 | 81 | ### 五、最终效果 82 | 83 | ![number.gif](http://upload-images.jianshu.io/upload_images/5994029-566fb988aa6b2e86.gif?imageMogr2/auto-orient/strip) 84 | 85 | 附上[简书](http://www.jianshu.com/p/b5f76b572f81)地址,有兴趣可以肛一波。 86 | 87 | 88 | 89 | ### 一、先看掌阅动画效果 90 | ![掌阅动画.gif](http://upload-images.jianshu.io/upload_images/5994029-239783fcfe5f9c6d.gif?imageMogr2/auto-orient/strip) 91 | 92 | ### 二、分析 93 | 从动画上看,可以拆解分为三部分进行: 94 | 1. 书壳的翻转动画; 95 | 2. 书扉页的放大动画; 96 | 3. 书壳的放大动画。(这个动画和扉页是同步进行的,但是仔细看,可以看到,书壳的高度变化速率和扉页是一样的,但是宽度的速率要低于扉页) 97 | 98 | ### 三、代码实现 99 | 我们选择用属性动画实现,那么首先要确定哪些属性是变化的。首先扉页放大的过程中,宽高是变化的,随着宽高变化,位置也需要调整,至少需要记录left和top。那就是至少需要四个值。但是宽可以通过 100 | > 宽度 = right - left 101 | > 高度 = bottom - top 102 | 103 | 索性我们建立一个对象来存储每一本书的(left, top, right, bottom)。 104 | ``` 105 | public class BookValue { 106 | private int left; 107 | private int top; 108 | private int right; 109 | private int bottom; 110 | } 111 | ``` 112 | 这样我们问题就变成了,如果知道起始和终点的BookValue,那么就可以计算出中间如何一个位置的BookValue。有没有很熟悉?没错就是属性动画的自定义估值器。 113 | ``` 114 | public class BookEvaluator implements TypeEvaluator { 115 | @Override 116 | public BookValue evaluate(float fraction, BookValue startValue, BookValue endValue) { 117 | int startLeft = startValue.getLeft(); 118 | int startTop = startValue.getTop(); 119 | int startRight = startValue.getRight(); 120 | int startBottom = startValue.getBottom(); 121 | 122 | int endLeft = endValue.getLeft(); 123 | int endTop = endValue.getTop(); 124 | int endRight = endValue.getRight(); 125 | int endBottom = endValue.getBottom(); 126 | 127 | BookValue bookValue = new BookValue(); 128 | bookValue.setLeft((int) (startLeft - fraction * (startLeft - endLeft))); 129 | bookValue.setTop((int) (startTop - fraction * (startTop - endTop))); 130 | bookValue.setRight((int) (startRight - fraction * (startRight - endRight))); 131 | bookValue.setBottom((int) (startBottom - fraction * (startBottom - endBottom))); 132 | 133 | return bookValue; 134 | } 135 | } 136 | ``` 137 | 新建一个估值器,传进去开始和结束的BookValue,计算返回中间的BookValue。计算不说了,那么怎么确定开始和结束位置呢,结束位置很好确定,就是整个屏幕。(实际操作中发现需要减去状态栏的高度,如果追求精度的话需要考虑,我们这里没有考虑) 138 | 那么起始位置怎么确定呢?我们点击的时候是可以拿到当前这个View的,这个View的属性里面就带了需要的位置信息。 139 | ``` 140 | convertView.setOnClickListener(new View.OnClickListener() { 141 | @Override 142 | public void onClick(View v) { 143 | ViewGroup window = (ViewGroup) getWindow().getDecorView(); 144 | BookValue startValue = new BookValue(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 145 | BookValue endValue = new BookValue(window.getLeft(), window.getTop(), window.getRight(), window.getBottom()); 146 | 147 | BookView bookView = new BookView(mContext); 148 | window.addView(bookView); 149 | bookView.startAnim(startValue, endValue); 150 | } 151 | }); 152 | ``` 153 | 获取屏幕的DecorView,可以说这个view的位置就是结束位置。onClick里面的view就是点击的开始位置。点击的view里面有两个子View,一个是书壳一个是书扉页,我们可以把两个view组合成一个自定义的ViewGroup: 154 | ``` 155 | public class BookView extends FrameLayout{ 156 | 157 | private TextView tvBookName; 158 | private TextView tvBookAuthor; 159 | 160 | public BookView(@NonNull Context context) { 161 | super(context); 162 | initView(context); 163 | } 164 | 165 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs) { 166 | super(context, attrs); 167 | initView(context); 168 | } 169 | 170 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 171 | super(context, attrs, defStyleAttr); 172 | initView(context); 173 | } 174 | 175 | public void initView(Context context) { 176 | LayoutInflater.from(context).inflate(R.layout.layout_book_view, this); 177 | setBackgroundColor(Color.TRANSPARENT); 178 | tvBookName = (TextView) findViewById(R.id.tv_book_name); 179 | tvBookAuthor = (TextView) findViewById(R.id.tv_book_author); 180 | } 181 | } 182 | ``` 183 | BookView继承自Framelayout,再看它的布局: 184 | ``` 185 | 186 | 189 | 190 | 199 | 200 | 209 | 210 | ``` 211 | 只有两个TextView,因为BookView是Framelayout,所以书壳在扉页的下面。细心的人可以看到BookView我们设置背景透明的,方便动画实现,省去了还要计算外层布局的位置,直接让它透明的充满全屏。 212 | 我们来看BookView里面的实现: 213 | ``` 214 | /** 215 | * 开启动画 216 | */ 217 | public void startAnim(BookValue startValue, BookValue endValue) { 218 | ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue); 219 | valueAnimator.setDuration(3000); 220 | valueAnimator.addUpdateListener(this); 221 | valueAnimator.addListener(this); 222 | 223 | tvBookName.setPivotX(0); 224 | tvBookName.setPivotY(500); 225 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180); 226 | objectAnimator.setDuration(1000); 227 | objectAnimator.setStartDelay(300); 228 | 229 | AnimatorSet animatorSet = new AnimatorSet(); 230 | animatorSet.playTogether(valueAnimator, objectAnimator); 231 | animatorSet.start(); 232 | } 233 | ``` 234 | 暴露一个开启动画的方法,动画上面知道肯定是两个(实际上是三个,这里简化了,所以效果也有点粗糙): 235 | > ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue); 236 | 237 | 新建一个估值器,直接扔进属性动画里,这里不需要指定动画的作用对象,因为它实际就是开启了一个计算,不断的回调计算结果。那么回调去哪了呢?后面再说。再看第二个动画: 238 | ``` 239 | tvBookName.setPivotX(0); 240 | tvBookName.setPivotY(500); 241 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180); 242 | objectAnimator.setDuration(1000); 243 | objectAnimator.setStartDelay(300); 244 | ``` 245 | 这个动画是作用于书壳上的,首先要设定锚点,不然它会默认中间翻转。属性动画设置rotationY属性变化,谁的属性?第一个参数tvBookName。怎么变化?从0到-180,也就是逆时针。 246 | 锚点的PivotX等于0很好理解,就是绕y轴翻转,但是PivotY怎么理解呢?我也不清楚,但是设置了一个很大的值,它和屏幕会有一个夹角,很接近掌阅的效果。 247 | 还有一个延迟300ms,书壳是先随着扉页一起放大,再翻转,效果上就很真实了。 248 | 249 | 上面有说计算结果回调去哪了,有重写的方法回调: 250 | ``` 251 | @Override 252 | public void onAnimationUpdate(ValueAnimator animation) { 253 | BookValue midBookValue = (BookValue) animation.getAnimatedValue(); 254 | 255 | tvBookName.setX(midBookValue.getLeft()); 256 | tvBookName.setY(midBookValue.getTop()); 257 | ViewGroup.LayoutParams layoutParams = tvBookName.getLayoutParams(); 258 | layoutParams.width = midBookValue.getRight() - midBookValue.getLeft(); 259 | layoutParams.height = midBookValue.getBottom() - midBookValue.getTop(); 260 | tvBookName.setLayoutParams(layoutParams); 261 | 262 | tvBookAuthor.setX(midBookValue.getLeft()); 263 | tvBookAuthor.setY(midBookValue.getTop()); 264 | ViewGroup.LayoutParams layoutParams1 = tvBookAuthor.getLayoutParams(); 265 | layoutParams1.width = midBookValue.getRight() - midBookValue.getLeft(); 266 | layoutParams1.height = midBookValue.getBottom() - midBookValue.getTop(); 267 | tvBookAuthor.setLayoutParams(layoutParams1); 268 | } 269 | ``` 270 | 首先获取midBookValue,这个值是不断的返回的,所以下面的方法要不断的修改作用对象的属性,就形成了动画的视觉效果。那么我们需要修改哪些属性呢? 271 | 首先修改X与Y,这个书壳就会产生偏移(这里不能去设置left和top,会有坑,一直显示在屏幕的原点位置。至于原因,还没发现)。 272 | 然后修改书壳的宽高,是通过获取LayoutParams修改width和height实现的。 273 | 书扉页也需要同样的操作。 274 | 以上就是全部的思路。 275 | ### 四、最终效果 276 | ![电子书动画.gif](http://upload-images.jianshu.io/upload_images/5994029-b0dd7dee9c3f9a26.gif?imageMogr2/auto-orient/strip) 277 | 278 | 这样看起来有点粗糙,是因为书壳和扉页宽度的变化是一致的,我们可以将书壳的宽度做一下限制,或者将他们的动画分开来写。 279 | 280 | 附上[简书](http://www.jianshu.com/p/f104a287ebfa)地址,有兴趣可以肛一波。 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.taovo.rjp.propertyanim" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.3.1' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9' 29 | testCompile 'junit:junit:4.12' 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/taovo/rjp/propertyanim/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.taovo.rjp.propertyanim", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/ChangePageActivity.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.GridView; 10 | 11 | import com.taovo.rjp.propertyanim.evaluator.BookValue; 12 | import com.taovo.rjp.propertyanim.view.BookOpenView; 13 | 14 | public class ChangePageActivity extends Activity { 15 | private Context mContext; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_change_page); 21 | mContext = this; 22 | 23 | GridView gridView = (GridView) findViewById(R.id.grid_view); 24 | gridView.setAdapter(new GridAdapter()); 25 | } 26 | 27 | public class GridAdapter extends BaseAdapter{ 28 | 29 | @Override 30 | public int getCount() { 31 | return 9; 32 | } 33 | 34 | @Override 35 | public Object getItem(int position) { 36 | return null; 37 | } 38 | 39 | @Override 40 | public long getItemId(int position) { 41 | return 0; 42 | } 43 | 44 | @Override 45 | public View getView(int position, View convertView, ViewGroup parent) { 46 | if(convertView == null){ 47 | convertView = new BookOpenView(mContext); 48 | } 49 | 50 | convertView.setOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View v) { 53 | ViewGroup window = (ViewGroup) getWindow().getDecorView(); 54 | BookValue startValue = new BookValue(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 55 | BookValue endValue = new BookValue(window.getLeft(), window.getTop(), window.getRight(), window.getBottom()); 56 | 57 | BookOpenView bookOpenView = new BookOpenView(mContext); 58 | window.addView(bookOpenView); 59 | bookOpenView.startAnim(startValue, endValue); 60 | } 61 | }); 62 | 63 | return convertView; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.taovo.rjp.propertyanim.bullet_screen.BulletActivity; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_main); 16 | 17 | } 18 | 19 | public void number(View view){ 20 | startActivity(new Intent(this, ScrollNumberActivity.class)); 21 | } 22 | 23 | public void openBook(View view){ 24 | startActivity(new Intent(this, ChangePageActivity.class)); 25 | } 26 | 27 | public void scrollBook(View view){ 28 | startActivity(new Intent(this, SeeBookActivity.class)); 29 | } 30 | 31 | public void bullet(View view){ 32 | startActivity(new Intent(this, BulletActivity.class)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/ScrollNumberActivity.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.widget.TextView; 8 | 9 | import com.taovo.rjp.propertyanim.evaluator.NumberEvaluator; 10 | 11 | public class ScrollNumberActivity extends Activity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_scroll_number); 17 | 18 | final TextView tvNumber1 = (TextView) findViewById(R.id.tv_number1); 19 | 20 | ValueAnimator numberAnim = ValueAnimator.ofObject(new NumberEvaluator(), "", "676767676767"); 21 | numberAnim.setDuration(3000); 22 | numberAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 23 | @Override 24 | public void onAnimationUpdate(ValueAnimator animation) { 25 | String number = (String) animation.getAnimatedValue(); 26 | tvNumber1.setText(number); 27 | } 28 | }); 29 | numberAnim.start(); 30 | numberAnim.addListener(new Animator.AnimatorListener() { 31 | @Override 32 | public void onAnimationStart(Animator animation) { 33 | 34 | } 35 | 36 | @Override 37 | public void onAnimationEnd(Animator animation) { 38 | tvNumber1.setText("676767676767"); 39 | } 40 | 41 | @Override 42 | public void onAnimationCancel(Animator animation) { 43 | 44 | } 45 | 46 | @Override 47 | public void onAnimationRepeat(Animator animation) { 48 | 49 | } 50 | }); 51 | 52 | final TextView tvNumber2 = (TextView) findViewById(R.id.tv_number2); 53 | 54 | ValueAnimator numberAnim2 = ValueAnimator.ofObject(new NumberEvaluator(), "", "345364"); 55 | numberAnim2.setDuration(3000); 56 | numberAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 57 | @Override 58 | public void onAnimationUpdate(ValueAnimator animation) { 59 | String number = (String) animation.getAnimatedValue(); 60 | tvNumber2.setText(number); 61 | } 62 | }); 63 | numberAnim2.start(); 64 | numberAnim2.addListener(new Animator.AnimatorListener() { 65 | @Override 66 | public void onAnimationStart(Animator animation) { 67 | 68 | } 69 | 70 | @Override 71 | public void onAnimationEnd(Animator animation) { 72 | tvNumber2.setText("345364"); 73 | } 74 | 75 | @Override 76 | public void onAnimationCancel(Animator animation) { 77 | 78 | } 79 | 80 | @Override 81 | public void onAnimationRepeat(Animator animation) { 82 | 83 | } 84 | }); 85 | 86 | 87 | final TextView tvNumber3 = (TextView) findViewById(R.id.tv_number3); 88 | 89 | ValueAnimator numberAnim3 = ValueAnimator.ofObject(new NumberEvaluator(), "", "48"); 90 | numberAnim3.setDuration(3000); 91 | numberAnim3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 92 | @Override 93 | public void onAnimationUpdate(ValueAnimator animation) { 94 | String number = (String) animation.getAnimatedValue(); 95 | tvNumber3.setText(number); 96 | } 97 | }); 98 | numberAnim3.start(); 99 | numberAnim3.addListener(new Animator.AnimatorListener() { 100 | @Override 101 | public void onAnimationStart(Animator animation) { 102 | 103 | } 104 | 105 | @Override 106 | public void onAnimationEnd(Animator animation) { 107 | tvNumber3.setText("48"); 108 | } 109 | 110 | @Override 111 | public void onAnimationCancel(Animator animation) { 112 | 113 | } 114 | 115 | @Override 116 | public void onAnimationRepeat(Animator animation) { 117 | 118 | } 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/SeeBookActivity.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | import com.taovo.rjp.propertyanim.view.BookView; 11 | 12 | public class SeeBookActivity extends Activity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_see_book); 18 | 19 | TextView tvPageOne = (TextView) findViewById(R.id.page_one); 20 | TextView tvPageTwo = (TextView) findViewById(R.id.page_two); 21 | 22 | BookView bookView = (BookView) findViewById(R.id.book_view); 23 | 24 | bookView.setTextViews(tvPageOne, tvPageTwo); 25 | } 26 | 27 | public Bitmap getViewBitmap(View view) { 28 | int width = view.getMeasuredWidth(); 29 | int height = view.getMeasuredHeight(); 30 | Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 31 | Canvas canvas = new Canvas(bitmap); 32 | view.draw(canvas); 33 | return bitmap; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/bullet_screen/Bullet.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.bullet_screen; 2 | 3 | /** 4 | * Created by Administrator on 2017/9/3. 5 | */ 6 | 7 | public class Bullet { 8 | private String headUrl; 9 | private String title; 10 | private int zan; 11 | 12 | public Bullet(String title){ 13 | this.title = title; 14 | } 15 | 16 | public String getHeadUrl() { 17 | return headUrl; 18 | } 19 | 20 | public void setHeadUrl(String headUrl) { 21 | this.headUrl = headUrl; 22 | } 23 | 24 | public String getTitle() { 25 | return title; 26 | } 27 | 28 | public void setTitle(String title) { 29 | this.title = title; 30 | } 31 | 32 | public int getZan() { 33 | return zan; 34 | } 35 | 36 | public void setZan(int zan) { 37 | this.zan = zan; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/bullet_screen/BulletActivity.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.bullet_screen; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import com.taovo.rjp.propertyanim.R; 8 | 9 | import java.util.ArrayList; 10 | 11 | public class BulletActivity extends Activity { 12 | 13 | private BulletScreenView bulletScreenView; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_bullet); 19 | 20 | bulletScreenView = (BulletScreenView) findViewById(R.id.bullet_screen_view); 21 | 22 | } 23 | 24 | public void add(View view) { 25 | ArrayList bullets = new ArrayList<>(); 26 | bullets.add(new Bullet("haokanhoakannak")); 27 | bullets.add(new Bullet("gartaytryzhghjdfgsgdfggggggdfggg")); 28 | bullets.add(new Bullet("125364747")); 29 | bullets.add(new Bullet("gggggggggggggggggggggggggggg")); 30 | bullets.add(new Bullet("666666666")); 31 | bullets.add(new Bullet("666")); 32 | bullets.add(new Bullet("999999999999")); 33 | bulletScreenView.addBullet(bullets); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/bullet_screen/BulletScreenView.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.bullet_screen; 2 | 3 | import android.content.Context; 4 | import android.graphics.Point; 5 | import android.graphics.Rect; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.FrameLayout; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static android.R.attr.y; 15 | 16 | /** 17 | * Created by Administrator on 2017/9/3. 18 | */ 19 | 20 | public class BulletScreenView extends FrameLayout { 21 | private Context mContext; 22 | private int mWidth; 23 | private int mHeight; 24 | private int bulletHeight; 25 | private int space; 26 | private List rightRect = new ArrayList<>(); 27 | private List bulletViews = new ArrayList<>(); 28 | 29 | public BulletScreenView(Context context) { 30 | super(context); 31 | initView(context, null); 32 | } 33 | 34 | public BulletScreenView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | initView(context, attrs); 37 | } 38 | 39 | public BulletScreenView(Context context, AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | initView(context, attrs); 42 | } 43 | 44 | @Override 45 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 46 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 47 | mWidth = getMeasuredWidth(); 48 | mHeight = getMeasuredHeight(); 49 | 50 | if(mHeight != 0) { 51 | rightRect.clear(); 52 | int lineCount = mHeight / bulletHeight - 1; 53 | int totalSpace = mHeight - lineCount * bulletHeight; 54 | space = totalSpace / (lineCount + 1); 55 | for (int i = 0; i < lineCount; i++) { 56 | rightRect.add(new Rect(mWidth, (bulletHeight + space) * i, mWidth + 100, (bulletHeight + space) * (i + 1))); 57 | } 58 | } 59 | } 60 | 61 | public void initView(Context context, AttributeSet attrs) { 62 | mContext = context; 63 | 64 | BulletView bulletView = new BulletView(mContext); 65 | //动态测量view的宽度 66 | int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 67 | bulletView.measure(spec,spec); 68 | bulletHeight = bulletView.getMeasuredHeight(); 69 | } 70 | 71 | public void addBullet(List bullets) { 72 | for (Bullet bullet : bullets) { 73 | BulletView bulletView = new BulletView(mContext); 74 | bulletView.setData(bullet); 75 | int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 76 | bulletView.measure(spec,spec); 77 | int measuredWidth = bulletView.getMeasuredWidth(); 78 | FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 79 | params.topMargin = seekLineTop(Math.random() * mHeight); 80 | addView(bulletView, params); 81 | bulletViews.add(bulletView); 82 | bulletView.startAnim(new Point(mWidth, y), new Point(-measuredWidth, y), 5000); 83 | } 84 | } 85 | 86 | /** 87 | * 找到一个合适的高度 88 | * @param randomHeight 89 | * @return 90 | */ 91 | private int seekLineTop(double randomHeight) { 92 | for (Rect rect : rightRect) { 93 | if(rect.contains(mWidth + 1, (int) randomHeight)){ 94 | return rect.top + space; 95 | } 96 | } 97 | return 0; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/bullet_screen/BulletView.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.bullet_screen; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ObjectAnimator; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.graphics.Point; 8 | import android.util.AttributeSet; 9 | import android.view.LayoutInflater; 10 | import android.view.MotionEvent; 11 | import android.view.ViewGroup; 12 | import android.view.animation.AccelerateDecelerateInterpolator; 13 | import android.widget.ImageView; 14 | import android.widget.LinearLayout; 15 | import android.widget.TextView; 16 | 17 | import com.taovo.rjp.propertyanim.R; 18 | 19 | /** 20 | * Created by Administrator on 2017/9/3. 21 | */ 22 | 23 | public class BulletView extends LinearLayout { 24 | 25 | private ObjectAnimator animator; 26 | private ImageView ivHeadView; 27 | private TextView tvTitle; 28 | private TextView tvZan; 29 | 30 | public BulletView(Context context) { 31 | super(context); 32 | initView(context); 33 | } 34 | 35 | public BulletView(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | initView(context); 38 | } 39 | 40 | public BulletView(Context context, AttributeSet attrs, int defStyleAttr) { 41 | super(context, attrs, defStyleAttr); 42 | initView(context); 43 | } 44 | 45 | public void initView(Context context){ 46 | LayoutInflater.from(context).inflate(R.layout.layout_bullet_view, this); 47 | ivHeadView = (ImageView) findViewById(R.id.iv_head_view); 48 | tvTitle = (TextView) findViewById(R.id.tv_title); 49 | tvZan = (TextView) findViewById(R.id.tv_zan); 50 | 51 | } 52 | 53 | public void setData(Bullet bullet){ 54 | tvTitle.setText(bullet.getTitle()); 55 | } 56 | 57 | @Override 58 | public boolean onTouchEvent(MotionEvent event) { 59 | if(event.getAction() == MotionEvent.ACTION_DOWN){ 60 | animator.removeAllListeners(); 61 | animator.cancel(); 62 | } 63 | if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){ 64 | animator.start(); 65 | } 66 | return super.onTouchEvent(event); 67 | } 68 | 69 | /** 70 | * 开启动画 用duration控制速度变化 71 | * @param start 72 | * @param end 73 | * @param duration 74 | */ 75 | public void startAnim(Point start, Point end, int duration){ 76 | animator = ObjectAnimator.ofFloat(this, "translationX", start.x, end.x); 77 | animator.setDuration(duration); 78 | animator.setInterpolator(new AccelerateDecelerateInterpolator()); 79 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 80 | @Override 81 | public void onAnimationUpdate(ValueAnimator animation) { 82 | float animatedValue = (Float) animation.getAnimatedValue(); 83 | 84 | } 85 | }); 86 | animator.addListener(new Animator.AnimatorListener() { 87 | @Override 88 | public void onAnimationStart(Animator animation) { 89 | 90 | } 91 | 92 | @Override 93 | public void onAnimationEnd(Animator animation) { 94 | ViewGroup parent = (ViewGroup) getParent(); 95 | parent.removeView(BulletView.this); 96 | } 97 | 98 | @Override 99 | public void onAnimationCancel(Animator animation) { 100 | 101 | } 102 | 103 | @Override 104 | public void onAnimationRepeat(Animator animation) { 105 | 106 | } 107 | }); 108 | animator.start(); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/evaluator/BookEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.evaluator; 2 | 3 | import android.animation.TypeEvaluator; 4 | 5 | /** 6 | * @author Gimpo create on 2017/9/5 11:22 7 | * @email : jimbo922@163.com 8 | */ 9 | 10 | public class BookEvaluator implements TypeEvaluator { 11 | @Override 12 | public BookValue evaluate(float fraction, BookValue startValue, BookValue endValue) { 13 | int startLeft = startValue.getLeft(); 14 | int startTop = startValue.getTop(); 15 | int startRight = startValue.getRight(); 16 | int startBottom = startValue.getBottom(); 17 | 18 | int endLeft = endValue.getLeft(); 19 | int endTop = endValue.getTop(); 20 | int endRight = endValue.getRight(); 21 | int endBottom = endValue.getBottom(); 22 | 23 | BookValue bookValue = new BookValue(); 24 | bookValue.setLeft((int) (startLeft - fraction * (startLeft - endLeft))); 25 | bookValue.setTop((int) (startTop - fraction * (startTop - endTop))); 26 | bookValue.setRight((int) (startRight - fraction * (startRight - endRight))); 27 | bookValue.setBottom((int) (startBottom - fraction * (startBottom - endBottom))); 28 | 29 | return bookValue; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/evaluator/BookValue.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.evaluator; 2 | 3 | /** 4 | * @author Gimpo create on 2017/9/5 11:24 5 | * @email : jimbo922@163.com 6 | */ 7 | 8 | public class BookValue { 9 | private int left; 10 | private int top; 11 | private int right; 12 | private int bottom; 13 | 14 | public BookValue(){} 15 | 16 | public BookValue(int left, int top, int right, int bottom){ 17 | this.left = left; 18 | this.top = top; 19 | this.right = right; 20 | this.bottom = bottom; 21 | } 22 | 23 | public int getRight() { 24 | return right; 25 | } 26 | 27 | public void setRight(int right) { 28 | this.right = right; 29 | } 30 | 31 | public int getBottom() { 32 | return bottom; 33 | } 34 | 35 | public void setBottom(int bottom) { 36 | this.bottom = bottom; 37 | } 38 | 39 | public int getLeft() { 40 | return left; 41 | } 42 | 43 | public void setLeft(int left) { 44 | this.left = left; 45 | } 46 | 47 | public int getTop() { 48 | return top; 49 | } 50 | 51 | public void setTop(int top) { 52 | this.top = top; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/evaluator/NumberEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.evaluator; 2 | 3 | import android.animation.TypeEvaluator; 4 | 5 | /** 6 | * @author Gimpo create on 2017/9/4 15:25 7 | * @email : jimbo922@163.com 8 | */ 9 | 10 | public class NumberEvaluator implements TypeEvaluator { 11 | 12 | @Override 13 | public String evaluate(float fraction, String startValue, String endValue) { 14 | int size = endValue.length(); 15 | StringBuilder sb = new StringBuilder(); 16 | for (int i = 0; i < size; i++) { 17 | if(i * 1.0 / size > fraction) { 18 | sb.append((int) Math.ceil(Math.random() * 9)); 19 | }else{ 20 | sb.append(String.valueOf(endValue).charAt(i)); 21 | } 22 | } 23 | return sb.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/evaluator/README.md: -------------------------------------------------------------------------------- 1 | ### 一、先看掌阅动画效果 2 | ![掌阅动画.gif](http://upload-images.jianshu.io/upload_images/5994029-239783fcfe5f9c6d.gif?imageMogr2/auto-orient/strip) 3 | 4 | ### 二、分析 5 | 从动画上看,可以拆解分为三部分进行: 6 | 1. 书壳的翻转动画; 7 | 2. 书扉页的放大动画; 8 | 3. 书壳的放大动画。(这个动画和扉页是同步进行的,但是仔细看,可以看到,书壳的高度变化速率和扉页是一样的,但是宽度的速率要低于扉页) 9 | 10 | ### 三、代码实现 11 | 我们选择用属性动画实现,那么首先要确定哪些属性是变化的。首先扉页放大的过程中,宽高是变化的,随着宽高变化,位置也需要调整,至少需要记录left和top。那就是至少需要四个值。但是宽可以通过 12 | > 宽度 = right - left 13 | > 高度 = bottom - top 14 | 15 | 索性我们建立一个对象来存储每一本书的(left, top, right, bottom)。 16 | ``` 17 | public class BookValue { 18 | private int left; 19 | private int top; 20 | private int right; 21 | private int bottom; 22 | } 23 | ``` 24 | 这样我们问题就变成了,如果知道起始和终点的BookValue,那么就可以计算出中间如何一个位置的BookValue。有没有很熟悉?没错就是属性动画的自定义估值器。 25 | ``` 26 | public class BookEvaluator implements TypeEvaluator { 27 | @Override 28 | public BookValue evaluate(float fraction, BookValue startValue, BookValue endValue) { 29 | int startLeft = startValue.getLeft(); 30 | int startTop = startValue.getTop(); 31 | int startRight = startValue.getRight(); 32 | int startBottom = startValue.getBottom(); 33 | 34 | int endLeft = endValue.getLeft(); 35 | int endTop = endValue.getTop(); 36 | int endRight = endValue.getRight(); 37 | int endBottom = endValue.getBottom(); 38 | 39 | BookValue bookValue = new BookValue(); 40 | bookValue.setLeft((int) (startLeft - fraction * (startLeft - endLeft))); 41 | bookValue.setTop((int) (startTop - fraction * (startTop - endTop))); 42 | bookValue.setRight((int) (startRight - fraction * (startRight - endRight))); 43 | bookValue.setBottom((int) (startBottom - fraction * (startBottom - endBottom))); 44 | 45 | return bookValue; 46 | } 47 | } 48 | ``` 49 | 新建一个估值器,传进去开始和结束的BookValue,计算返回中间的BookValue。计算不说了,那么怎么确定开始和结束位置呢,结束位置很好确定,就是整个屏幕。(实际操作中发现需要减去状态栏的高度,如果追求精度的话需要考虑,我们这里没有考虑) 50 | 那么起始位置怎么确定呢?我们点击的时候是可以拿到当前这个View的,这个View的属性里面就带了需要的位置信息。 51 | ``` 52 | convertView.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | ViewGroup window = (ViewGroup) getWindow().getDecorView(); 56 | BookValue startValue = new BookValue(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 57 | BookValue endValue = new BookValue(window.getLeft(), window.getTop(), window.getRight(), window.getBottom()); 58 | 59 | BookView bookView = new BookView(mContext); 60 | window.addView(bookView); 61 | bookView.startAnim(startValue, endValue); 62 | } 63 | }); 64 | ``` 65 | 获取屏幕的DecorView,可以说这个view的位置就是结束位置。onClick里面的view就是点击的开始位置。点击的view里面有两个子View,一个是书壳一个是书扉页,我们可以把两个view组合成一个自定义的ViewGroup: 66 | ``` 67 | public class BookView extends FrameLayout{ 68 | 69 | private TextView tvBookName; 70 | private TextView tvBookAuthor; 71 | 72 | public BookView(@NonNull Context context) { 73 | super(context); 74 | initView(context); 75 | } 76 | 77 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs) { 78 | super(context, attrs); 79 | initView(context); 80 | } 81 | 82 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 83 | super(context, attrs, defStyleAttr); 84 | initView(context); 85 | } 86 | 87 | public void initView(Context context) { 88 | LayoutInflater.from(context).inflate(R.layout.layout_book_view, this); 89 | setBackgroundColor(Color.TRANSPARENT); 90 | tvBookName = (TextView) findViewById(R.id.tv_book_name); 91 | tvBookAuthor = (TextView) findViewById(R.id.tv_book_author); 92 | } 93 | } 94 | ``` 95 | BookView继承自Framelayout,再看它的布局: 96 | ``` 97 | 98 | 101 | 102 | 111 | 112 | 121 | 122 | ``` 123 | 只有两个TextView,因为BookView是Framelayout,所以书壳在扉页的下面。细心的人可以看到BookView我们设置背景透明的,方便动画实现,省去了还要计算外层布局的位置,直接让它透明的充满全屏。 124 | 我们来看BookView里面的实现: 125 | ``` 126 | /** 127 | * 开启动画 128 | */ 129 | public void startAnim(BookValue startValue, BookValue endValue) { 130 | ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue); 131 | valueAnimator.setDuration(3000); 132 | valueAnimator.addUpdateListener(this); 133 | valueAnimator.addListener(this); 134 | 135 | tvBookName.setPivotX(0); 136 | tvBookName.setPivotY(500); 137 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180); 138 | objectAnimator.setDuration(1000); 139 | objectAnimator.setStartDelay(300); 140 | 141 | AnimatorSet animatorSet = new AnimatorSet(); 142 | animatorSet.playTogether(valueAnimator, objectAnimator); 143 | animatorSet.start(); 144 | } 145 | ``` 146 | 暴露一个开启动画的方法,动画上面知道肯定是两个(实际上是三个,这里简化了,所以效果也有点粗糙): 147 | > ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue); 148 | 149 | 新建一个估值器,直接扔进属性动画里,这里不需要指定动画的作用对象,因为它实际就是开启了一个计算,不断的回调计算结果。那么回调去哪了呢?后面再说。再看第二个动画: 150 | ``` 151 | tvBookName.setPivotX(0); 152 | tvBookName.setPivotY(500); 153 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180); 154 | objectAnimator.setDuration(1000); 155 | objectAnimator.setStartDelay(300); 156 | ``` 157 | 这个动画是作用于书壳上的,首先要设定锚点,不然它会默认中间翻转。属性动画设置rotationY属性变化,谁的属性?第一个参数tvBookName。怎么变化?从0到-180,也就是逆时针。 158 | 锚点的PivotX等于0很好理解,就是绕y轴翻转,但是PivotY怎么理解呢?我也不清楚,但是设置了一个很大的值,它和屏幕会有一个夹角,很接近掌阅的效果。 159 | 还有一个延迟300ms,书壳是先随着扉页一起放大,再翻转,效果上就很真实了。 160 | 161 | 上面有说计算结果回调去哪了,有重写的方法回调: 162 | ``` 163 | @Override 164 | public void onAnimationUpdate(ValueAnimator animation) { 165 | BookValue midBookValue = (BookValue) animation.getAnimatedValue(); 166 | 167 | tvBookName.setX(midBookValue.getLeft()); 168 | tvBookName.setY(midBookValue.getTop()); 169 | ViewGroup.LayoutParams layoutParams = tvBookName.getLayoutParams(); 170 | layoutParams.width = midBookValue.getRight() - midBookValue.getLeft(); 171 | layoutParams.height = midBookValue.getBottom() - midBookValue.getTop(); 172 | tvBookName.setLayoutParams(layoutParams); 173 | 174 | tvBookAuthor.setX(midBookValue.getLeft()); 175 | tvBookAuthor.setY(midBookValue.getTop()); 176 | ViewGroup.LayoutParams layoutParams1 = tvBookAuthor.getLayoutParams(); 177 | layoutParams1.width = midBookValue.getRight() - midBookValue.getLeft(); 178 | layoutParams1.height = midBookValue.getBottom() - midBookValue.getTop(); 179 | tvBookAuthor.setLayoutParams(layoutParams1); 180 | } 181 | ``` 182 | 首先获取midBookValue,这个值是不断的返回的,所以下面的方法要不断的修改作用对象的属性,就形成了动画的视觉效果。那么我们需要修改哪些属性呢? 183 | 首先修改X与Y,这个书壳就会产生偏移(这里不能去设置left和top,会有坑,一直显示在屏幕的原点位置。至于原因,还没发现)。 184 | 然后修改书壳的宽高,是通过获取LayoutParams修改width和height实现的。 185 | 书扉页也需要同样的操作。 186 | 以上就是全部的思路。 187 | ### 四、最终效果 188 | ![电子书动画.gif](http://upload-images.jianshu.io/upload_images/5994029-b0dd7dee9c3f9a26.gif?imageMogr2/auto-orient/strip) 189 | 190 | 这样看起来有点粗糙,是因为书壳和扉页宽度的变化是一致的,我们可以将书壳的宽度做一下限制,或者将他们的动画分开来写。 191 | 192 | 附上[简书](http://www.jianshu.com/p/f104a287ebfa)地址,有兴趣可以肛一波。 -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/view/BookOpenView.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ObjectAnimator; 6 | import android.animation.ValueAnimator; 7 | import android.content.Context; 8 | import android.graphics.Color; 9 | import android.support.annotation.AttrRes; 10 | import android.support.annotation.NonNull; 11 | import android.support.annotation.Nullable; 12 | import android.util.AttributeSet; 13 | import android.view.LayoutInflater; 14 | import android.view.ViewGroup; 15 | import android.widget.FrameLayout; 16 | import android.widget.TextView; 17 | 18 | import com.taovo.rjp.propertyanim.R; 19 | import com.taovo.rjp.propertyanim.evaluator.BookEvaluator; 20 | import com.taovo.rjp.propertyanim.evaluator.BookValue; 21 | 22 | /** 23 | * @author Gimpo create on 2017/9/5 10:31 24 | * @email : jimbo922@163.com 25 | */ 26 | 27 | public class BookOpenView extends FrameLayout implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { 28 | 29 | private TextView tvBookName; 30 | private TextView tvBookAuthor; 31 | 32 | public BookOpenView(@NonNull Context context) { 33 | super(context); 34 | initView(context); 35 | } 36 | 37 | public BookOpenView(@NonNull Context context, @Nullable AttributeSet attrs) { 38 | super(context, attrs); 39 | initView(context); 40 | } 41 | 42 | public BookOpenView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 43 | super(context, attrs, defStyleAttr); 44 | initView(context); 45 | } 46 | 47 | public void initView(Context context) { 48 | LayoutInflater.from(context).inflate(R.layout.layout_book_open_view, this); 49 | setBackgroundColor(Color.TRANSPARENT); 50 | tvBookName = (TextView) findViewById(R.id.tv_book_name); 51 | tvBookAuthor = (TextView) findViewById(R.id.tv_book_author); 52 | } 53 | 54 | /** 55 | * 开启动画 56 | */ 57 | public void startAnim(BookValue startValue, BookValue endValue) { 58 | ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue); 59 | valueAnimator.setDuration(3000); 60 | valueAnimator.addUpdateListener(this); 61 | valueAnimator.addListener(this); 62 | 63 | tvBookName.setPivotX(0); 64 | tvBookName.setPivotY(500); 65 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180); 66 | objectAnimator.setDuration(1000); 67 | objectAnimator.setStartDelay(300); 68 | 69 | AnimatorSet animatorSet = new AnimatorSet(); 70 | animatorSet.playTogether(valueAnimator, objectAnimator); 71 | animatorSet.start(); 72 | } 73 | 74 | @Override 75 | public void onAnimationUpdate(ValueAnimator animation) { 76 | BookValue midBookValue = (BookValue) animation.getAnimatedValue(); 77 | 78 | tvBookName.setX(midBookValue.getLeft()); 79 | tvBookName.setY(midBookValue.getTop()); 80 | ViewGroup.LayoutParams layoutParams = tvBookName.getLayoutParams(); 81 | layoutParams.width = midBookValue.getRight() - midBookValue.getLeft(); 82 | layoutParams.height = midBookValue.getBottom() - midBookValue.getTop(); 83 | tvBookName.setLayoutParams(layoutParams); 84 | 85 | tvBookAuthor.setX(midBookValue.getLeft()); 86 | tvBookAuthor.setY(midBookValue.getTop()); 87 | ViewGroup.LayoutParams layoutParams1 = tvBookAuthor.getLayoutParams(); 88 | layoutParams1.width = midBookValue.getRight() - midBookValue.getLeft(); 89 | layoutParams1.height = midBookValue.getBottom() - midBookValue.getTop(); 90 | tvBookAuthor.setLayoutParams(layoutParams1); 91 | } 92 | 93 | @Override 94 | public void onAnimationStart(Animator animation) { 95 | 96 | } 97 | 98 | @Override 99 | public void onAnimationEnd(Animator animation) { 100 | ViewGroup parent = (ViewGroup) getParent(); 101 | parent.removeView(this); 102 | } 103 | 104 | @Override 105 | public void onAnimationCancel(Animator animation) { 106 | 107 | } 108 | 109 | @Override 110 | public void onAnimationRepeat(Animator animation) { 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/view/BookView.java: -------------------------------------------------------------------------------- 1 | package com.taovo.rjp.propertyanim.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.ColorMatrix; 7 | import android.graphics.ColorMatrixColorFilter; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.graphics.Path; 11 | import android.graphics.PointF; 12 | import android.graphics.Region; 13 | import android.graphics.drawable.GradientDrawable; 14 | import android.support.annotation.Nullable; 15 | import android.util.AttributeSet; 16 | import android.util.Log; 17 | import android.view.MotionEvent; 18 | import android.view.View; 19 | import android.widget.Scroller; 20 | 21 | /** 22 | * @author Gimpo create on 2017/9/6 17:50 23 | * @email : jimbo922@163.com 24 | */ 25 | 26 | public class BookView extends View { 27 | 28 | private int mScreenWidth = 0; // 屏幕宽 29 | private int mScreenHeight = 0; // 屏幕高 30 | private int mCornerX = 1; // 拖拽点对应的页脚 31 | private int mCornerY = 1; 32 | private Path mPath0; 33 | private Path mPath1; 34 | Bitmap mCurPageBitmap = null; // 当前页 35 | Bitmap mNextPageBitmap = null; 36 | 37 | PointF mTouch = new PointF(); // 拖拽点 38 | PointF mBezierStart1 = new PointF(); // 贝塞尔曲线起始点 39 | PointF mBezierControl1 = new PointF(); // 贝塞尔曲线控制点 40 | PointF mBeziervertex1 = new PointF(); // 贝塞尔曲线顶点 41 | PointF mBezierEnd1 = new PointF(); // 贝塞尔曲线结束点 42 | 43 | PointF mBezierStart2 = new PointF(); // 另一条贝塞尔曲线 44 | PointF mBezierControl2 = new PointF(); 45 | PointF mBeziervertex2 = new PointF(); 46 | PointF mBezierEnd2 = new PointF(); 47 | 48 | float mMiddleX; 49 | float mMiddleY; 50 | float mDegrees; 51 | float mTouchToCornerDis; 52 | ColorMatrixColorFilter mColorMatrixFilter; 53 | Matrix mMatrix; 54 | float[] mMatrixArray = { 0, 0, 0, 0, 0, 0, 0, 0, 1.0f }; 55 | 56 | boolean mIsRTandLB; // 是否属于右上左下 57 | private float mMaxLength ; 58 | int[] mBackShadowColors;// 背面颜色组 59 | int[] mFrontShadowColors;// 前面颜色组 60 | GradientDrawable mBackShadowDrawableLR; // 有阴影的GradientDrawable 61 | GradientDrawable mBackShadowDrawableRL; 62 | GradientDrawable mFolderShadowDrawableLR; 63 | GradientDrawable mFolderShadowDrawableRL; 64 | 65 | GradientDrawable mFrontShadowDrawableHBT; 66 | GradientDrawable mFrontShadowDrawableHTB; 67 | GradientDrawable mFrontShadowDrawableVLR; 68 | GradientDrawable mFrontShadowDrawableVRL; 69 | 70 | Paint mPaint; 71 | Scroller mScroller; 72 | 73 | private float actiondownX,actiondownY; 74 | private View curView; 75 | private View nextView; 76 | 77 | public BookView(Context context) { 78 | super(context, null); 79 | } 80 | 81 | public BookView(Context context, @Nullable AttributeSet attrs) { 82 | super(context, attrs); 83 | mPath0 = new Path(); 84 | mPath1 = new Path(); 85 | 86 | mPaint = new Paint(); 87 | mPaint.setStyle(Paint.Style.FILL); 88 | mPaint.setAntiAlias(true); 89 | 90 | createDrawable(); 91 | 92 | ColorMatrix cm = new ColorMatrix();//设置颜色数组 93 | float array[] = { 0.55f, 0, 0, 0, 80.0f, 0, 0.55f, 0, 0, 80.0f, 0, 0, 94 | 0.55f, 0, 80.0f, 0, 0, 0, 0.2f, 0 }; 95 | cm.set(array); 96 | mColorMatrixFilter = new ColorMatrixColorFilter(cm); 97 | mMatrix = new Matrix(); 98 | mScroller = new Scroller(getContext()); 99 | 100 | mTouch.x = 0.01f; // 不让x,y为0,否则在点计算时会有问题 101 | mTouch.y = 0.01f; 102 | } 103 | 104 | /** 105 | * 创建阴影的GradientDrawable 106 | */ 107 | private void createDrawable() { 108 | int[] color = { 0x333333, 0xb0333333 }; 109 | mFolderShadowDrawableRL = new GradientDrawable( 110 | GradientDrawable.Orientation.RIGHT_LEFT, color); 111 | mFolderShadowDrawableRL 112 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 113 | 114 | mFolderShadowDrawableLR = new GradientDrawable( 115 | GradientDrawable.Orientation.LEFT_RIGHT, color); 116 | mFolderShadowDrawableLR 117 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 118 | 119 | mBackShadowColors = new int[] { 0xff111111, 0x111111 }; 120 | mBackShadowDrawableRL = new GradientDrawable( 121 | GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors); 122 | mBackShadowDrawableRL.setGradientType(GradientDrawable.LINEAR_GRADIENT); 123 | 124 | mBackShadowDrawableLR = new GradientDrawable( 125 | GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors); 126 | mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT); 127 | 128 | mFrontShadowColors = new int[] { 0x80111111, 0x111111 }; 129 | mFrontShadowDrawableVLR = new GradientDrawable( 130 | GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors); 131 | mFrontShadowDrawableVLR 132 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 133 | mFrontShadowDrawableVRL = new GradientDrawable( 134 | GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors); 135 | mFrontShadowDrawableVRL 136 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 137 | 138 | mFrontShadowDrawableHTB = new GradientDrawable( 139 | GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors); 140 | mFrontShadowDrawableHTB 141 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 142 | 143 | mFrontShadowDrawableHBT = new GradientDrawable( 144 | GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors); 145 | mFrontShadowDrawableHBT 146 | .setGradientType(GradientDrawable.LINEAR_GRADIENT); 147 | } 148 | 149 | @Override 150 | public boolean onTouchEvent(MotionEvent event) { 151 | if (event.getAction() == MotionEvent.ACTION_MOVE) { 152 | mTouch.x = event.getX(); 153 | mTouch.y = event.getY(); 154 | this.postInvalidate(); 155 | } 156 | 157 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 158 | mTouch.x = event.getX(); 159 | mTouch.y = event.getY(); 160 | actiondownX = event.getX(); 161 | actiondownY = event.getY(); 162 | calcCornerXY(mTouch.x, mTouch.y); 163 | if(curView != null) { 164 | curView.buildDrawingCache(); 165 | mCurPageBitmap = curView.getDrawingCache(); 166 | nextView.buildDrawingCache(); 167 | mNextPageBitmap = nextView.getDrawingCache(); 168 | } 169 | } 170 | if (event.getAction() == MotionEvent.ACTION_UP) { 171 | startAnimation(1200); 172 | this.postInvalidate(); 173 | } 174 | if (event.getAction() == MotionEvent.ACTION_MOVE 175 | && event.getMetaState() == 1) { 176 | mTouch.x = event.getX(); 177 | mTouch.y = event.getY(); 178 | startAnimation(1200); 179 | this.postInvalidate(); 180 | } 181 | // return super.onTouchEvent(event); 182 | return true; 183 | } 184 | 185 | /** 186 | * 计算拖拽点对应的拖拽脚 187 | * 188 | * @param x 189 | * @param y 190 | */ 191 | public void calcCornerXY(float x, float y) { 192 | // Log.i("hck", "PageWidget x:" + x + " y" + y); 193 | if (x <= mScreenWidth / 2) 194 | mCornerX = 0; 195 | else 196 | mCornerX = mScreenWidth; 197 | if (y <= mScreenHeight / 2) 198 | mCornerY = 0; 199 | else 200 | mCornerY = mScreenHeight; 201 | if ((mCornerX == 0 && mCornerY == mScreenHeight) 202 | || (mCornerX == mScreenWidth && mCornerY == 0)) 203 | mIsRTandLB = true; 204 | else 205 | mIsRTandLB = false; 206 | } 207 | 208 | public void setBitmaps(Bitmap bm1, Bitmap bm2) { 209 | mCurPageBitmap = bm1; 210 | mNextPageBitmap = bm2; 211 | } 212 | 213 | public void setTextViews(View curView, View nextView) { 214 | this.curView = curView; 215 | this.nextView = nextView; 216 | } 217 | 218 | @Override 219 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 220 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 221 | mScreenWidth = getMeasuredWidth(); 222 | mScreenHeight = getMeasuredHeight(); 223 | mMaxLength = (float) Math.hypot(mScreenWidth, mScreenHeight); 224 | } 225 | 226 | @Override 227 | protected void onDraw(Canvas canvas) { 228 | canvas.drawColor(0xFFAAAAAA); 229 | if(mCurPageBitmap != null) { 230 | calcPoints(); 231 | drawCurrentPageArea(canvas, mCurPageBitmap, mPath0); 232 | drawNextPageAreaAndShadow(canvas, mNextPageBitmap); 233 | drawCurrentPageShadow(canvas); 234 | drawCurrentBackArea(canvas, mCurPageBitmap); 235 | } 236 | } 237 | 238 | private void calcPoints() { 239 | mMiddleX = (mTouch.x + mCornerX) / 2; 240 | mMiddleY = (mTouch.y + mCornerY) / 2; 241 | mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) 242 | * (mCornerY - mMiddleY) / (mCornerX - mMiddleX); 243 | mBezierControl1.y = mCornerY; 244 | mBezierControl2.x = mCornerX; 245 | // mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 246 | // * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); 247 | 248 | float f4 = mCornerY-mMiddleY; 249 | if (f4 == 0) { 250 | mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 251 | * (mCornerX - mMiddleX) / 0.1f; 252 | // Log.d("PageWidget",""+f4); 253 | }else { 254 | mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 255 | * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); 256 | // Log.d("PageWidget","没有进入if判断"+ mBezierControl2.y + ""); 257 | } 258 | 259 | // Log.i("hmg", "mTouchX " + mTouch.x + " mTouchY " + mTouch.y); 260 | // Log.i("hmg", "mBezierControl1.x " + mBezierControl1.x 261 | // + " mBezierControl1.y " + mBezierControl1.y); 262 | // Log.i("hmg", "mBezierControl2.x " + mBezierControl2.x 263 | // + " mBezierControl2.y " + mBezierControl2.y); 264 | 265 | mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) 266 | / 2; 267 | mBezierStart1.y = mCornerY; 268 | 269 | // 当mBezierStart1.x < 0或者mBezierStart1.x > 480时 270 | // 如果继续翻页,会出现BUG故在此限制 271 | if (mTouch.x > 0 && mTouch.x < mScreenWidth) { 272 | if (mBezierStart1.x < 0 || mBezierStart1.x > mScreenWidth) { 273 | if (mBezierStart1.x < 0) 274 | mBezierStart1.x = mScreenWidth - mBezierStart1.x; 275 | 276 | float f1 = Math.abs(mCornerX - mTouch.x); 277 | float f2 = mScreenWidth * f1 / mBezierStart1.x; 278 | mTouch.x = Math.abs(mCornerX - f2); 279 | 280 | float f3 = Math.abs(mCornerX - mTouch.x) 281 | * Math.abs(mCornerY - mTouch.y) / f1; 282 | mTouch.y = Math.abs(mCornerY - f3); 283 | 284 | mMiddleX = (mTouch.x + mCornerX) / 2; 285 | mMiddleY = (mTouch.y + mCornerY) / 2; 286 | 287 | mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) 288 | * (mCornerY - mMiddleY) / (mCornerX - mMiddleX); 289 | mBezierControl1.y = mCornerY; 290 | 291 | mBezierControl2.x = mCornerX; 292 | // mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 293 | // * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); 294 | 295 | float f5 = mCornerY-mMiddleY; 296 | if (f5 == 0) { 297 | mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 298 | * (mCornerX - mMiddleX) / 0.1f; 299 | }else { 300 | mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) 301 | * (mCornerX - mMiddleX) / (mCornerY - mMiddleY); 302 | // Log.d("PageWidget", mBezierControl2.y + ""); 303 | } 304 | 305 | 306 | 307 | // Log.i("hmg", "mTouchX --> " + mTouch.x + " mTouchY--> " 308 | // + mTouch.y); 309 | // Log.i("hmg", "mBezierControl1.x-- " + mBezierControl1.x 310 | // + " mBezierControl1.y -- " + mBezierControl1.y); 311 | // Log.i("hmg", "mBezierControl2.x -- " + mBezierControl2.x 312 | // + " mBezierControl2.y -- " + mBezierControl2.y); 313 | mBezierStart1.x = mBezierControl1.x 314 | - (mCornerX - mBezierControl1.x) / 2; 315 | } 316 | } 317 | mBezierStart2.x = mCornerX; 318 | mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) 319 | / 2; 320 | 321 | mTouchToCornerDis = (float) Math.hypot((mTouch.x - mCornerX), 322 | (mTouch.y - mCornerY)); 323 | 324 | mBezierEnd1 = getCross(mTouch, mBezierControl1, mBezierStart1, 325 | mBezierStart2); 326 | mBezierEnd2 = getCross(mTouch, mBezierControl2, mBezierStart1, 327 | mBezierStart2); 328 | 329 | // Log.i("hmg", "mBezierEnd1.x " + mBezierEnd1.x + " mBezierEnd1.y " 330 | // + mBezierEnd1.y); 331 | // Log.i("hmg", "mBezierEnd2.x " + mBezierEnd2.x + " mBezierEnd2.y " 332 | // + mBezierEnd2.y); 333 | 334 | /* 335 | * mBeziervertex1.x 推导 336 | * ((mBezierStart1.x+mBezierEnd1.x)/2+mBezierControl1.x)/2 化简等价于 337 | * (mBezierStart1.x+ 2*mBezierControl1.x+mBezierEnd1.x) / 4 338 | */ 339 | mBeziervertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4; 340 | mBeziervertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4; 341 | mBeziervertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4; 342 | mBeziervertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4; 343 | } 344 | 345 | /** 346 | * 求解直线P1P2和直线P3P4的交点坐标 347 | * 348 | * @param P1 349 | * @param P2 350 | * @param P3 351 | * @param P4 352 | * @return 353 | */ 354 | public PointF getCross(PointF P1, PointF P2, PointF P3, PointF P4) { 355 | PointF CrossP = new PointF(); 356 | // 二元函数通式: y=ax+b 357 | float a1 = (P2.y - P1.y) / (P2.x - P1.x); 358 | float b1 = ((P1.x * P2.y) - (P2.x * P1.y)) / (P1.x - P2.x); 359 | 360 | float a2 = (P4.y - P3.y) / (P4.x - P3.x); 361 | float b2 = ((P3.x * P4.y) - (P4.x * P3.y)) / (P3.x - P4.x); 362 | CrossP.x = (b2 - b1) / (a1 - a2); 363 | CrossP.y = a1 * CrossP.x + b1; 364 | return CrossP; 365 | } 366 | 367 | /** 368 | * 绘制翻起页背面 369 | * 370 | * @param canvas 371 | * @param bitmap 372 | */ 373 | private void drawCurrentBackArea(Canvas canvas, Bitmap bitmap) { 374 | int i = (int) (mBezierStart1.x + mBezierControl1.x) / 2; 375 | float f1 = Math.abs(i - mBezierControl1.x); 376 | int i1 = (int) (mBezierStart2.y + mBezierControl2.y) / 2; 377 | float f2 = Math.abs(i1 - mBezierControl2.y); 378 | float f3 = Math.min(f1, f2); 379 | mPath1.reset(); 380 | mPath1.moveTo(mBeziervertex2.x, mBeziervertex2.y); 381 | mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y); 382 | mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y); 383 | mPath1.lineTo(mTouch.x, mTouch.y); 384 | mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y); 385 | mPath1.close(); 386 | GradientDrawable mFolderShadowDrawable; 387 | int left; 388 | int right; 389 | if (mIsRTandLB) { 390 | left = (int) (mBezierStart1.x - 1); 391 | right = (int) (mBezierStart1.x + f3 + 1); 392 | mFolderShadowDrawable = mFolderShadowDrawableLR; 393 | } else { 394 | left = (int) (mBezierStart1.x - f3 - 1); 395 | right = (int) (mBezierStart1.x + 1); 396 | mFolderShadowDrawable = mFolderShadowDrawableRL; 397 | } 398 | canvas.save(); 399 | try { 400 | canvas.clipPath(mPath0); 401 | canvas.clipPath(mPath1, Region.Op.INTERSECT); 402 | } catch (Exception e) { 403 | } 404 | 405 | 406 | mPaint.setColorFilter(mColorMatrixFilter); 407 | 408 | float dis = (float) Math.hypot(mCornerX - mBezierControl1.x, 409 | mBezierControl2.y - mCornerY); 410 | float f8 = (mCornerX - mBezierControl1.x) / dis; 411 | float f9 = (mBezierControl2.y - mCornerY) / dis; 412 | mMatrixArray[0] = 1 - 2 * f9 * f9; 413 | mMatrixArray[1] = 2 * f8 * f9; 414 | mMatrixArray[3] = mMatrixArray[1]; 415 | mMatrixArray[4] = 1 - 2 * f8 * f8; 416 | mMatrix.reset(); 417 | mMatrix.setValues(mMatrixArray); 418 | mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y); 419 | mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y); 420 | canvas.drawBitmap(bitmap, mMatrix, mPaint); 421 | // canvas.drawBitmap(bitmap, mMatrix, null); 422 | mPaint.setColorFilter(null); 423 | canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y); 424 | mFolderShadowDrawable.setBounds(left, (int) mBezierStart1.y, right, 425 | (int) (mBezierStart1.y + mMaxLength)); 426 | mFolderShadowDrawable.draw(canvas); 427 | canvas.restore(); 428 | } 429 | 430 | /** 431 | * 绘制翻起页的阴影 432 | * 433 | * @param canvas 434 | */ 435 | public void drawCurrentPageShadow(Canvas canvas) { 436 | double degree; 437 | if (mIsRTandLB) { 438 | degree = Math.PI 439 | / 4 440 | - Math.atan2(mBezierControl1.y - mTouch.y, mTouch.x 441 | - mBezierControl1.x); 442 | } else { 443 | degree = Math.PI 444 | / 4 445 | - Math.atan2(mTouch.y - mBezierControl1.y, mTouch.x 446 | - mBezierControl1.x); 447 | } 448 | // 翻起页阴影顶点与touch点的距离 449 | double d1 = (float) 25 * 1.414 * Math.cos(degree); 450 | double d2 = (float) 25 * 1.414 * Math.sin(degree); 451 | float x = (float) (mTouch.x + d1); 452 | float y; 453 | if (mIsRTandLB) { 454 | y = (float) (mTouch.y + d2); 455 | } else { 456 | y = (float) (mTouch.y - d2); 457 | } 458 | mPath1.reset(); 459 | mPath1.moveTo(x, y); 460 | mPath1.lineTo(mTouch.x, mTouch.y); 461 | mPath1.lineTo(mBezierControl1.x, mBezierControl1.y); 462 | mPath1.lineTo(mBezierStart1.x, mBezierStart1.y); 463 | mPath1.close(); 464 | float rotateDegrees; 465 | canvas.save(); 466 | try { 467 | canvas.clipPath(mPath0, Region.Op.XOR); 468 | canvas.clipPath(mPath1, Region.Op.INTERSECT); 469 | } catch (Exception e) { 470 | // TODO: handle exception 471 | } 472 | 473 | int leftx; 474 | int rightx; 475 | GradientDrawable mCurrentPageShadow; 476 | if (mIsRTandLB) { 477 | leftx = (int) (mBezierControl1.x); 478 | rightx = (int) mBezierControl1.x + 25; 479 | mCurrentPageShadow = mFrontShadowDrawableVLR; 480 | } else { 481 | leftx = (int) (mBezierControl1.x - 25); 482 | rightx = (int) mBezierControl1.x + 1; 483 | mCurrentPageShadow = mFrontShadowDrawableVRL; 484 | } 485 | 486 | rotateDegrees = (float) Math.toDegrees(Math.atan2(mTouch.x 487 | - mBezierControl1.x, mBezierControl1.y - mTouch.y)); 488 | canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y); 489 | mCurrentPageShadow.setBounds(leftx, 490 | (int) (mBezierControl1.y - mMaxLength), rightx, 491 | (int) (mBezierControl1.y)); 492 | mCurrentPageShadow.draw(canvas); 493 | canvas.restore(); 494 | 495 | mPath1.reset(); 496 | mPath1.moveTo(x, y); 497 | mPath1.lineTo(mTouch.x, mTouch.y); 498 | mPath1.lineTo(mBezierControl2.x, mBezierControl2.y); 499 | mPath1.lineTo(mBezierStart2.x, mBezierStart2.y); 500 | mPath1.close(); 501 | canvas.save(); 502 | try { 503 | canvas.clipPath(mPath0, Region.Op.XOR); 504 | canvas.clipPath(mPath1, Region.Op.INTERSECT); 505 | } catch (Exception e) { 506 | } 507 | 508 | if (mIsRTandLB) { 509 | leftx = (int) (mBezierControl2.y); 510 | rightx = (int) (mBezierControl2.y + 25); 511 | mCurrentPageShadow = mFrontShadowDrawableHTB; 512 | } else { 513 | leftx = (int) (mBezierControl2.y - 25); 514 | rightx = (int) (mBezierControl2.y + 1); 515 | mCurrentPageShadow = mFrontShadowDrawableHBT; 516 | } 517 | rotateDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl2.y 518 | - mTouch.y, mBezierControl2.x - mTouch.x)); 519 | canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y); 520 | float temp; 521 | if (mBezierControl2.y < 0) 522 | temp = mBezierControl2.y - mScreenHeight; 523 | else 524 | temp = mBezierControl2.y; 525 | 526 | int hmg = (int) Math.hypot(mBezierControl2.x, temp); 527 | if (hmg > mMaxLength) 528 | mCurrentPageShadow 529 | .setBounds((int) (mBezierControl2.x - 25) - hmg, leftx, 530 | (int) (mBezierControl2.x + mMaxLength) - hmg, 531 | rightx); 532 | else 533 | mCurrentPageShadow.setBounds( 534 | (int) (mBezierControl2.x - mMaxLength), leftx, 535 | (int) (mBezierControl2.x), rightx); 536 | 537 | // Log.i("hmg", "mBezierControl2.x " + mBezierControl2.x 538 | // + " mBezierControl2.y " + mBezierControl2.y); 539 | mCurrentPageShadow.draw(canvas); 540 | canvas.restore(); 541 | } 542 | 543 | private void drawNextPageAreaAndShadow(Canvas canvas, Bitmap bitmap) { 544 | mPath1.reset(); 545 | mPath1.moveTo(mBezierStart1.x, mBezierStart1.y); 546 | mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y); 547 | mPath1.lineTo(mBeziervertex2.x, mBeziervertex2.y); 548 | mPath1.lineTo(mBezierStart2.x, mBezierStart2.y); 549 | mPath1.lineTo(mCornerX, mCornerY); 550 | mPath1.close(); 551 | 552 | mDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl1.x 553 | - mCornerX, mBezierControl2.y - mCornerY)); 554 | int leftx; 555 | int rightx; 556 | GradientDrawable mBackShadowDrawable; 557 | if (mIsRTandLB) { //左下及右上 558 | leftx = (int) (mBezierStart1.x); 559 | rightx = (int) (mBezierStart1.x + mTouchToCornerDis / 4); 560 | mBackShadowDrawable = mBackShadowDrawableLR; 561 | } else { 562 | leftx = (int) (mBezierStart1.x - mTouchToCornerDis / 4); 563 | rightx = (int) mBezierStart1.x; 564 | mBackShadowDrawable = mBackShadowDrawableRL; 565 | } 566 | canvas.save(); 567 | try { 568 | canvas.clipPath(mPath0); 569 | canvas.clipPath(mPath1, Region.Op.INTERSECT); 570 | } catch (Exception e) { 571 | } 572 | 573 | 574 | canvas.drawBitmap(bitmap, 0, 0, null); 575 | canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y); 576 | mBackShadowDrawable.setBounds(leftx, (int) mBezierStart1.y, rightx, 577 | (int) (mMaxLength + mBezierStart1.y));//左上及右下角的xy坐标值,构成一个矩形 578 | mBackShadowDrawable.draw(canvas); 579 | canvas.restore(); 580 | } 581 | 582 | private void drawCurrentPageArea(Canvas canvas, Bitmap bitmap, Path path) { 583 | mPath0.reset(); 584 | mPath0.moveTo(mBezierStart1.x, mBezierStart1.y); 585 | mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, 586 | mBezierEnd1.y); 587 | mPath0.lineTo(mTouch.x, mTouch.y); 588 | mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y); 589 | mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, 590 | mBezierStart2.y); 591 | mPath0.lineTo(mCornerX, mCornerY); 592 | mPath0.close(); 593 | 594 | canvas.save(); 595 | canvas.clipPath(path, Region.Op.XOR); 596 | canvas.drawBitmap(bitmap, 0, 0, null); 597 | try { 598 | canvas.restore(); 599 | } catch (Exception e) { 600 | 601 | } 602 | 603 | } 604 | 605 | @Override 606 | public void computeScroll() { 607 | super.computeScroll(); 608 | if (mScroller.computeScrollOffset()) { 609 | float x = mScroller.getCurrX(); 610 | float y = mScroller.getCurrY(); 611 | mTouch.x = x; 612 | mTouch.y = y; 613 | postInvalidate(); 614 | } 615 | } 616 | 617 | private void startAnimation(int delayMillis) { 618 | int dx, dy; 619 | // dx 水平方向滑动的距离,负值会使滚动向左滚动 620 | // dy 垂直方向滑动的距离,负值会使滚动向上滚动 621 | if (mCornerX > 0) { 622 | dx = -(int) (mScreenWidth + mTouch.x); 623 | } else { 624 | dx = (int) (mScreenWidth - mTouch.x + mScreenWidth); 625 | } 626 | if (mCornerY > 0) { 627 | dy = (int) (mScreenHeight - mTouch.y); 628 | } else { 629 | dy = (int) (1 - mTouch.y); // 防止mTouch.y最终变为0 630 | } 631 | mScroller.startScroll((int) mTouch.x, (int) mTouch.y, dx, dy, 632 | delayMillis); 633 | } 634 | 635 | public void abortAnimation() { 636 | if (!mScroller.isFinished()) { 637 | mScroller.abortAnimation(); 638 | } 639 | } 640 | 641 | /** 642 | * 是否能够拖动过去 643 | * 644 | * @return 645 | */ 646 | public boolean canDragOver() { 647 | if (mTouchToCornerDis > mScreenWidth / 10) 648 | return true; 649 | return false; 650 | } 651 | 652 | /** 653 | * 是否从左边翻向右边 654 | * 655 | * @return 656 | */ 657 | public String DragToRight() { 658 | // if (mCornerX > 0) 659 | // return false; 660 | // return true; 661 | 662 | if (actiondownX>mScreenWidth/3.0 && actiondownX < (mScreenWidth * 2.0 / 3.0) ) { 663 | Log.d("PageWidget","是否进入此语句"); 664 | return "popview"; 665 | 666 | } else if (actiondownX < mScreenWidth / 3.0) { 667 | 668 | Log.d("PageWidget", "mScreenWidth / 3.0=" + mScreenWidth / 3.0); 669 | return "right"; 670 | 671 | } else if (actiondownX > mScreenWidth*2.0 /3 ) { 672 | 673 | return "left"; 674 | } 675 | 676 | return null; 677 | } 678 | 679 | 680 | public boolean right() { 681 | if (mCornerX > -4) 682 | return false; 683 | return true; 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /app/src/main/java/com/taovo/rjp/propertyanim/view/README.md: -------------------------------------------------------------------------------- 1 | ### 一、电子书翻页贝塞尔曲线实现 2 | 前一篇说到电子书的打开效果,电子书的翻页效果更加绚丽。今天找到一个开源的[仿掌阅](https://github.com/Focfa/Jreader)项目,下载了源码看了一下,效果真的不错。 3 | 代码也是来自于何大神的博客,如果不知道何大神的博客地址,我给个传送: 4 | [Android 实现书籍翻页效果----原理篇](http://blog.csdn.net/hmg25/article/details/6306479) 5 | [Android 实现书籍翻页效果----源码篇](http://blog.csdn.net/hmg25/article/details/6319664) 6 | [Android 实现书籍翻页效果----完结篇](http://blog.csdn.net/hmg25/article/details/6342539) 7 | [Android 实现书籍翻页效果---番外篇之光影效果](http://blog.csdn.net/hmg25/article/details/6366279) 8 | [Android 实现书籍翻页效果----升级篇](http://blog.csdn.net/hmg25/article/details/6419694) 9 | 看完再把demo下载下来琢磨一下,基本怎么实现的也就会个7788了。 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bullet_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjpacket/PropertyAnim/33bda30c0c48b93c76b1664b5b4c436b1c05342d/app/src/main/res/drawable/test.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_bullet.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 15 | 16 | 17 | 18 |