前些天看到這個效果圖
android
github地址 : github.com/qdxxxx/Bezi…
多謝老鐵隨手就是一個star,抱拳。
[標題黨通常是: 轉瘋了,項目集成此酷炫動畫只要3步!]git
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}複製代碼
dependencies {
compile 'com.github.qdxxxx:BezierViewPager:v1.0.5'
}複製代碼
<qdx.bezierviewpager_compile.vPage.BezierViewPager
android:id="@+id/view_page"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<qdx.bezierviewpager_compile.BezierRoundView
android:id="@+id/bezRound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>複製代碼
CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext());
cardAdapter.addImgUrlList(imgList); //放置圖片url的list
BezierViewPager viewPager = (BezierViewPager) findViewById(R.id.view_page);
viewPager.setAdapter(cardAdapter);
BezierRoundView bezRound = (BezierRoundView) findViewById(R.id.bezRound);
bezRound.attach2ViewPage(viewPager);複製代碼
name | format | 中文解釋 |
---|---|---|
color_bez | color | 貝塞爾圓球顏色 |
color_touch | color | 觸摸反饋 |
color_stroke | color | 圓框的顏色 |
time_animator | integer | 動畫時間 |
round_count | integer | 圓框數量,即Adapter.getCount |
radius | dimension | 貝塞爾圓球半徑,圓框半徑爲(radius-2) |
attach2ViewPage | BezierViewPager | 綁定指定的ViewPager(處理滑動時觸摸事件) 並自動設置round_count |
name | format | 中文解釋 |
---|---|---|
showTransformer | float | ViewPager滑動到當前顯示頁的放大比例 |
name | format | 中文解釋 |
---|---|---|
addImgUrlList | List | 包含圖片地址的list |
setOnCardItemClickListener | OnCardItemClickListener | 當前ViewPager點擊事件 返回CurPosition |
setMaxElevationFactor | integer | Adapter裏CardView最大的Elevation |
[建議先看這篇文章]
github
首先,咱們須要繪製P0,而後 cubicTo p1,p2,p3,再cubicTo p4,p5.p6……
原諒我用這麼簡單粗暴的方式畫圓…canvas
private PointF p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11;
複製代碼
p0 = new PointF(0, -mRadius);//mRadius圓的半徑
p6 = new PointF(0, mRadius);
p1 = new PointF(mRadius * bezFactor, -mRadius);//bezFactor即0.5519...
p5 = new PointF(mRadius * bezFactor, mRadius);
p2 = new PointF(mRadius, -mRadius * bezFactor);
p4 = new PointF(mRadius, mRadius * bezFactor);
p3 = new PointF(mRadius, 0);
p9 = new PointF(-mRadius, 0);
p11 = new PointF(-mRadius * bezFactor, -mRadius);
p7 = new PointF(-mRadius * bezFactor, mRadius);
p10 = new PointF(-mRadius, -mRadius * bezFactor);
p8 = new PointF(-mRadius, mRadius * bezFactor);複製代碼
再繪製path數組
mPath.moveTo(p0.x, p0.y);
mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
mPath.cubicTo(p4.x, p4.y, p5.x, p5.y, p6.x, p6.y);
mPath.cubicTo(p7.x, p7.y, p8.x, p8.y, p9.x, p9.y);
mPath.cubicTo(p10.x, p10.y, p11.x, p11.y, p0.x, p0.y);
mPath.close();複製代碼
一個貝(ri)塞(ben)爾(guo)圓(qi)栩栩如生。bash
咱們嘗試經過手指滑動改變,p2,p3,p4的x軸座標來觀察圓的變化app
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_DOWN:
p2 = new PointF(event.getX() - mWidth / 2, -mRadius * bezFactor);
p3 = new PointF(event.getX() - mWidth / 2, 0);
p4 = new PointF(event.getX() - mWidth / 2, mRadius * bezFactor);
invalidate();
break;
}
return true;
}複製代碼
首先咱們不考慮反彈效果,圓的變化有3種狀態maven
老樣子,咱們用ValueAnimator來模擬一下[0,1]變化的值。【由於ViewPager的onPageScrolled監聽中positionOffset是[0,1)變化的,相似。】
ide
//展現動畫
private ValueAnimator animatorStart;
private TimeInterpolator timeInterpolator = new DecelerateInterpolator();
private float animatedValue; //[0,1]的值
public void startAnimator() {
if (animatorStart != null) {
if (animatorStart.isRunning()) {
return;
}
animatorStart.start();
} else {
animatorStart = ValueAnimator.ofFloat(0, 1f).setDuration(1500);
animatorStart.setInterpolator(timeInterpolator);
animatorStart.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedValue = (float) animation.getAnimatedValue();
invalidate();
}
});
animatorStart.start();
}
}複製代碼
private float rRadio=1; //P2,3,4 x軸倍數
private float lRadio=1; //P8,9,10倍數
private float tbRadio=1; //y軸縮放倍數
private float disL = 0.5f; //離開圓的閾值
private float disM = 0.8f; //最大值的閾值
private float disA = 0.9f; //到達下個圓框的閾值複製代碼
if (0 < animatedValue && animatedValue <= disL) { //還沒離開圓框的時候
rRadio = 1f + animatedValue * 2; //[1,2]
}
if (disL < animatedValue && animatedValue <= disM) {//離開圓框,至最大值區域
rRadio = 2 - range0Until1(disL, disM) * 0.5f; // [2,1.5]
lRadio = 1 + range0Until1(disL, disM) * 0.5f; // [1,1.5]
}
if (disM < animatedValue && animatedValue <= disA) { //從最大值,至到達下一個圓框
rRadio = 1.5f - range0Until1(disM, disA) * 0.5f; // [1.5,1]
lRadio = 1.5f - range0Until1(disM, disA) * 0.5f; // [1.5,1]
}複製代碼
/**
* 將值域轉化爲[0,1]
*
* @param minValue 大於等於
* @param maxValue 小於等於
* @return 根據當前 animatedValue,返回 [0,1] 對應的數值
*/
private float range0Until1(float minValue, float maxValue) {
return (animatedValue - minValue) / (maxValue - minValue);
}複製代碼
請再次原諒我用這麼簡單粗暴的方式畫圓…佈局
mPath.moveTo(p0.x, p0.y * tbRadio);
mPath.cubicTo(p1.x, p1.y * tbRadio, p2.x * rRadio, p2.y, p3.x * rRadio, p3.y);
mPath.cubicTo(p4.x * rRadio, p4.y, p5.x, p5.y * tbRadio, p6.x, p6.y * tbRadio);
mPath.cubicTo(p7.x, p7.y * tbRadio, p8.x * lRadio, p8.y, p9.x * lRadio, p9.y);
mPath.cubicTo(p10.x * lRadio, p10.y, p11.x, p11.y * tbRadio, p0.x, p0.y * tbRadio);
mPath.close();
複製代碼
理清了上面這些代碼,一個有靈性的貝塞爾圓就即將繪製成功。咱們再加上離開圓至到達下一個圓框這個區域y軸變化,[p,5,6,7, 1,0,11],效果就以下所示。
這時候咱們已經將貝塞爾圓的運動方式給表達出來了,再加上一些效果[位移/反彈/翻轉],咱們就能模擬出貝塞爾圓從一個圓框進入下一個圓框的動畫了。
在上面的基礎上,咱們加上反彈效果
if (0 < animatedValue && animatedValue <= disL) { //還沒離開圓框的時候
rRadio = 1f + animatedValue * 2; //[1,2]
}
if (disL < animatedValue && animatedValue <= disM) {//離開圓框,至最大值區域
rRadio = 2 - range0Until1(disL, disM) * 0.5f; // [2,1.5]
lRadio = 1 + range0Until1(disL, disM) * 0.5f; // [1,1.5]
tbRadio = 1 - range0Until1(disL, disM) / 3; // [1 , 2/3]
}
if (disM < animatedValue && animatedValue <= disA) { //從最大值,至到達下一個圓框
rRadio = 1.5f - range0Until1(disM, disA) * 0.5f; // [1.5,1]
lRadio = 1.5f - range0Until1(disM, disA) * (1.5f - boundRadio); //反彈效果,進場 內彈boundRadio lRadio =[1.5,boundRadio]
tbRadio = (range0Until1(disM, disA) + 2) / 3; // [ 2/3,1]
}
if (disA < animatedValue && animatedValue <= 1f) {//到達圓框,lRadio=[boundRadio,1]
rRadio = 1;
tbRadio = 1;
lRadio = boundRadio + range0Until1(disA, 1) * (1 - boundRadio); //反彈效果,飽和
}複製代碼
再加上位移效果。一開始我在想,貝塞爾圓要不斷的變化形態,還要移動位置。豈不至關的麻煩。後來把它分解成變化狀態+不斷位移效果。
boolean isTrans = false;
float transX = 1f;
if (disL <= animatedValue && animatedValue <= disA) { //離開圓框,至到達下一個圓框
isTrans = true;
//咱們設置2個圓框距離爲mWidth / 2f
transX = mWidth / 2f * range0Until1(disL, disA); //[0,mWidth / 2f]
}
if (disA < animatedValue && animatedValue <= 1) {//到達下一個圓
isTrans = true;
transX = mWidth / 2;
}
if (isTrans) {
canvas.translate(transX, 0);
}複製代碼
至此貝塞爾圓球進入右側圓框的效果已經實現,那麼若是圓球要從右側圓框進入左側圓框呢?
【題外話:寫完上面這個效果已是月黑風高的時候了,腦神經即將進入假死狀態,我心想,雖然複雜了點,可是應該仍是能夠作的出來的,腦殼運行的速度根本跟不上敲代碼的速度。根據位移方向的判斷從而設定lRadio和rRadio。有點自信回頭的趕腳。。。休息了一覺次日醒來天啊嚕,爲何不用Matrix,只要用path.transform(matrix),就能夠作到鏡像path,因此適當的休息有助於提高效率。】
matrix_bounceL = new Matrix();
matrix_bounceL.preScale(-1, 1);
mPath.transform(matrix_bounceL);
複製代碼
關聯ViewPager總共有2個要點
首先咱們來了解一下onPageScrolled
這個方法中2個咱們要用到的參數
咱們功能需求分析一下:
以前咱們用ValueAnimator
來模擬運動狀態,如今咱們可使用positionOffset
關聯到ViewPager
animatedValue = positionOffset;
direction = ((position + positionOffset) - curPos > 0); //運動方向。 true爲右邊(手往左滑動)
nextPos = direction ? curPos + 1 : curPos - 1; //右 +1 左 -1
if (!direction) //若是是向左
animatedValue = 1 - animatedValue; //讓 animatedValue 不論是左滑仍是右滑,都從[0,1)開始計算
if (positionOffset == 0) {
curPos = position;
nextPos = position;
}
複製代碼
以上代碼還需動手調試,看看log才能更明白的領悟。
從上面的gif能夠發現若是緩慢的滑動,pos的位置正確的,可是若是快速滑動,就會發現問題 : [例如0快速滑動到2,貝塞爾圓球會從0滑動到1,再從0滑動到2],打了Log以後咱們才發現原來快速滑動的時候,positionOffset到達下一個pos不會置爲0!!發現問題後就好解決了。咱們加上這一段代碼就能夠解決該問題。(快速滑動可能存在或多或少的問題,我也是花了些時間去測試的。)
//快速滑動的時候,positionOffset有可能不會置於0
if (direction && position + positionOffset > nextPos) { //向右,並且
curPos = position;
nextPos = position + 1;
} else if (!direction && position + positionOffset < nextPos) {
curPos = position;
nextPos = position - 1;
}複製代碼
onDraw
咱們先要得到每一個圓框的圓心x軸座標
private float[] bezPos; //記錄每個圓心x軸的位置
bezPos = new float[default_round_count]; //根據圓框個數
for (int i = 0; i < default_round_count; i++) {
bezPos[i] = mWidth / (default_round_count + 1) * (i + 1);
}
複製代碼
假設咱們的default_round_count 即圓框個數爲4,那麼咱們就要分紅 4+1 份,再綜合上述的求圓心代碼,應該會更清晰一點。
根據curPos和nextPos繪製貝塞爾圓球,po出onDraw代碼
canvas.translate(0, mHeight / 2);
mBezPath.reset();
for (int i = 0; i < default_round_count; i++) {
canvas.drawCircle(bezPos[i], 0, mRadius - 2, mRoundStrokePaint); //繪製圓框
}
if (animatedValue == 1) {
canvas.drawCircle(bezPos[nextPos], 0, mRadius, mBezPaint);
return;
}
canvas.translate(bezPos[curPos], 0); //根據curPos,移動到當前圓框位置
if (0 < animatedValue && animatedValue <= disL) {
rRadio = 1f + animatedValue * 2; // [1,2]
lRadio = 1f;
tbRadio = 1f;
}
if (disL < animatedValue && animatedValue <= disM) {
rRadio = 2 - range0Until1(disL, disM) * 0.5f; // [2,1.5]
lRadio = 1 + range0Until1(disL, disM) * 0.5f; // [1,1.5]
tbRadio = 1 - range0Until1(disL, disM) / 3; // [1 , 2/3]
}
if (disM < animatedValue && animatedValue <= disA) {
rRadio = 1.5f - range0Until1(disM, disA) * 0.5f; // [1.5,1]
lRadio = 1.5f - range0Until1(disM, disA) * (1.5f - boundRadio); //反彈效果,進場 內彈boundRadio
tbRadio = (range0Until1(disM, disA) + 2) / 3; // [ 2/3,1]
}
if (disA < animatedValue && animatedValue <= 1f) {
rRadio = 1;
tbRadio = 1;
lRadio = boundRadio + range0Until1(disA, 1) * (1 - boundRadio); //反彈效果,飽和
}
if (animatedValue == 1 || animatedValue == 0) { //防止極其粗暴的滑動
rRadio = 1f;
lRadio = 1f;
tbRadio = 1f;
}
boolean isTrans = false; //根據nextPos和curPos求出位移距離
float transX = (nextPos - curPos) * (mWidth / (default_round_count + 1));
if (disL <= animatedValue && animatedValue <= disA) {
isTrans = true;
transX = transX * (animatedValue - disL) / (disA - disL);
}
if (disA < animatedValue && animatedValue <= 1) {
isTrans = true;
}
if (isTrans) {
canvas.translate(transX, 0);
}
mBezPath.moveTo(p0.x, p0.y * tbRadio);
mBezPath.cubicTo(p1.x, p1.y * tbRadio, p2.x * rRadio, p2.y, p3.x * rRadio, p3.y);
mBezPath.cubicTo(p4.x * rRadio, p4.y, p5.x, p5.y * tbRadio, p6.x, p6.y * tbRadio);
mBezPath.cubicTo(p7.x, p7.y * tbRadio, p8.x * lRadio, p8.y, p9.x * lRadio, p9.y);
mBezPath.cubicTo(p10.x * lRadio, p10.y, p11.x, p11.y * tbRadio, p0.x, p0.y * tbRadio);
mBezPath.close();
if (!direction) {
mBezPath.transform(matrix_bounceL);
}
canvas.drawPath(mBezPath, mBezPaint);
if (isTrans) {
canvas.save();
}
複製代碼
咱們須要判斷是否點擊到了圓框上,和點擊了具體哪一個圓框。
在onPageScrolled
方法的時候不進行處理,而是經過ValueAnimator
來模擬數值。從而繪製貝塞爾圓球效果。
private float[] xPivotPos; //根據圓心x軸+mRadius,劃分紅不一樣的區域 ,主要爲了判斷觸摸x軸的位置
xPivotPos = new float[default_round_count];
for (int i = 0; i < default_round_count; i++) {
xPivotPos[i] = mWidth / (default_round_count + 1) * (i + 1) + mRadius;
}複製代碼
針對x軸 : 個人作法是用一個數組xPivotPos 存儲每一個圓框最邊緣的位置,即圓心+mRadius,而後咱們觸摸的時候,就能夠找到當前觸摸touchPos是屬於哪一個(圓框+mRadius)範圍內。只要x >=bezPos[touchPos]-mRadius,就能夠清楚的知道是否觸摸到了該區域的圓框範圍。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
if (y <= mHeight / 2 + mRadius && y >= mHeight / 2 - mRadius && !isAniming) { //先判斷y,若是y點擊是在圓y軸的範圍
int pos = -Arrays.binarySearch(xPivotPos, x) - 1;
if (pos >= 0 && pos < default_round_count && x + mRadius >= bezPos[pos]) {
nextPos = pos;
if (mViewPage != null && curPos != nextPos) {
mViewPage.setCurrentItem(pos);
isAniming = true;
direction = (curPos < pos);
startAnimator(); //咱們經過ValueAnimator來模擬具體的值,不使用ViewPager的onPageScrolled方法。
}
}
return true;
}
break;
}
return super.onTouchEvent(event);
}複製代碼
至此咱們BezierRoundView的用法和繪製方法已經講解完了,下面來看一下ViewPager是怎麼實現切換效果的。
【靈魂畫家】
上圖針對的是ViewPager設置Padding以後,
CardPagerAdapter是咱們繼承PagerAdapter
的類,adapter裏的佈局是cardView
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardView"
app:cardCornerRadius="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardPreventCornerOverlap="true"
app:cardUseCompatPadding="true">
<!--cardUseCompatPadding 設置陰影以後自動縮小布局大小-->
<ImageView
android:id="@+id/item_iv"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v7.widget.CardView>複製代碼
先來了解一下cardView setCardElevation(float)
方法。【針對CardViewApi21】
if (!cardView.getUseCompatPadding()) {
cardView.setShadowPadding(0, 0, 0, 0);
return;
}
float elevation = getMaxElevation(cardView);
final float radius = getRadius(cardView);
int hPadding = (int) Math.ceil(RoundRectDrawableWithShadow
.calculateHorizontalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
int vPadding = (int) Math.ceil(RoundRectDrawableWithShadow
.calculateVerticalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
cardView.setShadowPadding(hPadding, vPadding, hPadding, vPadding);複製代碼
static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
boolean addPaddingForCorners) {
if (addPaddingForCorners) {
return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
} else {
return maxShadowSize * SHADOW_MULTIPLIER;
}
}
static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
boolean addPaddingForCorners) {
if (addPaddingForCorners) {
return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
} else {
return maxShadowSize;
}
}複製代碼
下面看一下效果測試。
咱們來看一下ViewPager左右設置Padding爲mWidth / 10的效果
viewPager.setPadding(mWidth / 10, 0, mWidth / 10, 0);
viewPager.setClipToPadding(false);複製代碼
再來看一下CardPagerAdapter設置MaxElevationFactor爲mWidth / 10的效果【adapter.xml的cardCornerRadius不設值,cardUseCompatPadding必定要設置true!!】
int maxFactor = mWidth / 10;
cardAdapter.setMaxElevationFactor(maxFactor);複製代碼
具體我也不贅述了,看圖應該能分析出二者的不一樣。
因此如今綜上所述,制定一個需求
也就是說當咱們知道圖片的寬高比例以後,代碼裏面咱們要動態的去調整和設置並保持這個寬高比例。
【這邊有個坑就是設置setMaxElevation它的寬高比是不可抗的,因此咱們只能在setPadding的時候,去調節這個比例】
【setMaxElevation
寬的Padding爲maxFactor + 0.3*CornerRadius 【0.3≈≈ (1 - COS_45)】
高的Padding爲maxFactor*1.5f + 0.3*CornerRadius】
可是!
setMaxElevation
的狀況下,在去設置padding,那麼如何保證咱們的寬高比?具體請看以下代碼分析。【能夠經過去掉adapter.xml 裏ImagerView 的android:scaleType=」fitXY」屬性測試一下寬高比例是否調試正確
//已知圖片的寬爲1920,高1080.
int mWidth = getWindowManager().getDefaultDisplay().getWidth();
float heightRatio = 0.565f; //高是寬的 0.565 ,根據圖片比例
CardPagerAdapter cardAdapter = new CardPagerAdapter(getApplicationContext());
cardAdapter.addImgUrlList(imgList);//添加加載的圖片集合
//設置陰影大小,即vPage 左右兩個圖片相距邊框 maxFactor + 0.3*CornerRadius *2
//設置陰影大小,即vPage 上下圖片相距邊框 maxFactor*1.5f + 0.3*CornerRadius
int maxFactor = mWidth / 25;
cardAdapter.setMaxElevationFactor(maxFactor);
int mWidthPading = mWidth / 8;
//由於咱們adapter裏的cardView CornerRadius已經寫死爲10dp,因此0.3*CornerRadius=3
//設置Elevation以後,控件寬度要減去 (maxFactor + dp2px(3)) * heightRatio
//heightMore 設置Elevation以後,控件高度 比 控件寬度* heightRatio 多出的部分
float heightMore = (1.5f * maxFactor + dp2px(3)) - (maxFactor + dp2px(3)) * heightRatio;
int mHeightPading = (int) (mWidthPading * heightRatio - heightMore);
BezierViewPager viewPager = (BezierViewPager) findViewById(R.id.view_page);
viewPager.setLayoutParams(new RelativeLayout.LayoutParams(mWidth, (int) (mWidth * heightRatio)));
viewPager.setPadding(mWidthPading, mHeightPading, mWidthPading, mHeightPading);
viewPager.setClipToPadding(false);
viewPager.setAdapter(cardAdapter);複製代碼
改方法是設置ViewPager移動的時候,cardView放大效果和Elevation陰影效果,具體過程能夠自行在ShadowTransformer
查看,實現過程上文基本也有覆蓋。
零零碎碎也搗鼓了一陣子的自定義View,我在想既然邁出這一步了,就得作好它。
人生老是要有信仰,有夢想才能一直前行,哪怕走的再慢,也是在前行。
若是這篇文章寫的還湊合或者勾引發了你的鬥志的話,歡迎點個star
github.com/qdxxxx/Bezi…