首先来一张效果图
上方是一个类似于ViewPager的滑动控件(SlidingViewPager),下方则是一个指示器(CustomPagerIndicator),指示器是会出现一个弹性圆的平移.这两2个都是自定义View或者ViewGroup来实现的.
布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.gaoql.customview.SlidingViewPager
android:id="@+id/slidingViewGroup"
android:layout_width="match_parent"
android:background="#363636"
android:layout_above="@+id/customRelativeLayout"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_camera_enhance_black_24dp"
android:clickable="true"
/>
<ImageView
android:id="@+id/iv2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_cloud_black_24dp"
android:clickable="true"
/>
<ImageView
android:id="@+id/iv3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_assignment_black_24dp"
android:clickable="true"
/>
</com.gaoql.customview.SlidingViewPager>
<com.gaoql.customview.CustomRelativeLayout
android:id="@+id/customRelativeLayout"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#363636">
<com.gaoql.customview.CustomPagerIndicator
xmlns:circleButton="http://schemas.android.com/apk/res-auto"
android:id="@+id/customviewgroup"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.gaoql.customview.CircleButton
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_camera"
circleButton:radius="40dp" />
<com.gaoql.customview.CircleButton
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_cloud"
circleButton:radius="40dp" />
<com.gaoql.customview.CircleButton
android:id="@+id/btn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_setting"
circleButton:radius="40dp" />
</com.gaoql.customview.CustomPagerIndicator>
</com.gaoql.customview.CustomRelativeLayout>
</RelativeLayout>
布局就是一个SlidingViewPager,和一个CustomPagerIndicator,还有CustomRelativeLayout(是干什么用的呢?下面再说)
一. SlidingViewPager
带有手势探测器GestureDetector和滑动Sroller的自定义ViewGroup,继承自LinearLayout
大体实现的效果
- 1 布局只需简单地水平排列
- 2 页面需要跟随手指移动而产生被拖拽的效果
- 3 拖动距离不足某一个距离(最小滑动距离,比如屏幕的三分之一)回弹,大于某一个距离则前进或后退一页
当然手指在页面上抛掷也要做到滑动下一页面或者上一页面
1-比较简单,onLayout中横向排列子View即可
2-拖拽可以利用srcollBy来做
3-需要比较2个触摸事件间的偏移量的差值和你自定义的最小滑动距离(和系统的最小滑动距离)比较再做处理
代码设计思路
主要对down,move,up事件的处理
1. down事件,记录当前的偏移量和触摸点x
2. move事件,主要做拖拽,那么就是求2次触摸点的x坐标差值,然后scrollBy
3. up事件,手指松开,当手指松开不足最小滑动距离回弹,满足且大于则前进或者后退
代码实现
下面是具体的SlidingViewPager实现
- 布局和测量
scrollTo(currentPageIndex*mWidth, 0); 这里直接展示出第一页的内容
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
scrollTo(currentPageIndex*mWidth, 0);
}
- 事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
View childView = getChildAt(0);
childViewWidth = childView.getRight()-childView.getLeft();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
offsetX=getScrollX();
mDownX = x;
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (!isCanSliding) {
isCanSliding = isCanSliding(ev);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
dispatchTouchEvent 中主要判断isCanSliding 是否可以去滑动,
isCanSliding 的判断逻辑如下
private boolean isCanSliding(MotionEvent ev){
float currentX = ev.getX();
float currentY = ev.getY();
if(Math.abs(currentX-mDownX) > Math.abs(currentY-mDownY) && Math.abs(currentX-mDownX)>minScrollDistance){
//X方向的距离大于y的滑动距离 && x方向的滑动距离大于系统最短滑动距离,认为是水平滑动
return true;
}
return false;
}
然后在onInterceptTouchEvent中return isCanSliding ,想法是为了处理当在垂直滑动的控件,比如RecycleView,使用该组件的时候出现的滑动冲突,可以试试看.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(getIndicator()!=null&&getIndicator().getTranslateState()==CustomPagerIndicator.STATE_MOVING){
//发现正在滑动的,调用父类的方法分发掉该事件,不再拦截
return false;
}
return isCanSliding;
}
isCanSliding=true ,就被拦截掉啦,来到了onTouchEvent 执行滑动的代码.当false就把事件给childView
@Override
public boolean onTouchEvent(MotionEvent event) {
obtainVelocityTracker(event);
float x = event.getX();
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
dx = x - mDownX;
mDownX = x;
if (currentPageIndex == 0 && dx > 0 || currentPageIndex == getChildCount() - 1 && dx < 0) {
break;
}
scrollBy((int) -dx, 0);
break;
case MotionEvent.ACTION_UP:
if(isCanSliding) {
int scrollX = getScrollX();
int delta = scrollX - offsetX;
//计算出一秒移动1000像素的速率 1000 表示每秒多少像素(pix/second),1代表每微秒多少像素(pix/millisecond)
velocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
float velocityX = velocityTracker.getXVelocity();
float velocityY = velocityTracker.getYVelocity();
if (Math.abs(delta) < childViewWidth / 3) {
// 小于三分之一,弹回去
Log.e(TAG, "onTouchEvent ACTION_UP back 1 ");
state = State.None;
requestUpdateState(state,delta);
} else if (Math.abs(velocityX) <= configuration.getScaledMinimumFlingVelocity() && Math.abs(velocityY) <= configuration.getScaledMinimumFlingVelocity()) {
//当速度小于系统速度,但过了三分一的距离,此时应该滑动一页
Log.e(TAG, "onTouchEvent ACTION_UP back 2 ");
if (delta > 0) { //左滑趋势
Log.e(TAG,"onTouchEvent page index 2-1 -- "+currentPageIndex);
if (currentPageIndex >=0 ) {
Log.e(TAG, "onTouchEvent ACTION_UP back 2-1 ");
state = State.ToNext;
}
} else {//右滑趋势
Log.e(TAG,"onTouchEvent page index 2-2 -- "+currentPageIndex);
if (currentPageIndex < getChildCount()) {
Log.e(TAG, "onTouchEvent ACTION_UP back 2-2 ");
state = State.ToPre;
}
}
Log.e(TAG,"requestUpdateState 1 "+state);
requestUpdateState(state,delta);
Log.e(TAG,"requestUpdateState 1 addChildViewCenterPointToIndicator "+currentPageIndex);
addChildViewCenterPointToIndicator(currentPageIndex);
Log.i(TAG,"startIndicatorCircleMoving 1");
startIndicatorCircleMoving();
}
}
realseVelocityTracker();
break;
default:
break;
}
return mGestureDetector.onTouchEvent(event) ;
}
关于翻页的状态State,是个枚举类,ToPre–上一页,ToNext–下一页,None–无状态,用于滑动距离不足时回弹的
public enum State {
ToPre,ToNext,None;
}
private void requestUpdateState(State state,int delta){
Log.e(TAG,"page index -"+currentPageIndex);
switch (state){
case None:
stayInCurrentPage();
Log.e(TAG,"stayInCurrentPage - "+currentPageIndex);
break;
case ToNext:
moveToNextPage(delta);
Log.e(TAG,"moveToNextPage - "+currentPageIndex);
break;
case ToPre:
moveToPrePage(delta);
Log.e(TAG,"moveToPrePage - "+currentPageIndex);
break;
default:
break;
}
invalidate();
}
/**
* 下一页,向左
*/
private void moveToNextPage(int delta){
int dx = childViewWidth - delta;
int scrollX = getScrollX();
currentPageIndex++;
mScroller.startScroll(scrollX, 0, dx, 0, 1000);
}
/**
* 上一页,向右
*/
private void moveToPrePage(int delta){
int dx = childViewWidth+delta;
int scrollX = getScrollX();
currentPageIndex--;
mScroller.startScroll(scrollX, 0, -dx, 0, 1000);
}
private void stayInCurrentPage(){
int scrollX = getScrollX();
int detla = currentPageIndex*childViewWidth - scrollX ;
mScroller.startScroll(scrollX,0,detla,0);
}
用到了一个手势检测者GestureDetector。实现了以下回调函数,主要是触发onFling
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.e(TAG,"onFling velocityX-"+velocityX+",velocityY-"+velocityY);
View childView = getChildAt(0);
int width = childView.getRight()-childView.getLeft();
float distanceX = e2.getX()-e1.getX();
int delta = getScrollX() - offsetX;
if(Math.abs(delta)<width/3){
//弹回
Log.e(TAG,"onFling donothing");
state = State.None;
Log.e(TAG,"requestUpdateState 2 "+state);
requestUpdateState(state,delta);
return true;
}
if(Math.abs(distanceX)>=minScrollDistance) {
if(distanceX == 0){
Log.e(TAG,"onFling donothing distanceX =0");
return true;
}
if (distanceX < 0) {
if (currentPageIndex < getChildCount() - 1) {
state = State.ToNext;
Log.e(TAG,"requestUpdateState 3 "+state);
requestUpdateState(state,delta);
}
}
if (distanceX > 0) {
if (currentPageIndex > 0) {
state = State.ToPre;
Log.e(TAG,"requestUpdateState 4 "+state);
requestUpdateState(state,delta);
}
}
Log.e(TAG,"onFling addChildViewCenterPointToIndicator "+currentPageIndex);
addChildViewCenterPointToIndicator(currentPageIndex);
Log.i(TAG,"startIndicatorCircleMoving 2");
startIndicatorCircleMoving();
}
return true;
}
注意:要在onDown()返回true才可以触发onScroll,滑动的时候会多次调用,所以我选择了onFling中搞事情了。
具体地说,典型的触屏事件及其listener执行的流程见下:
1). 单击事件的执行流程:
有两种情况,一种是时间很短,一种时间稍长。
时间很短:onDown ----> onSingleTapUp ----> onSingleTapConfirmed
时间稍长:onDown ----> onShowPress ----> onSingleTapUp ----> onSingleTapConfirmed
2). 长按事件
onDown ----> onShowPress ----> onLongPress
3.抛(fling):手指触动屏幕后,稍微滑动后立即松开:
onDown ----> onScroll ----> onScroll ----> onScroll ----> ……… ----> onFling
4.拖动(drag)
onDown ----> onScroll ----> onScroll ----> onFiling
注意:要在onDown()返回true才可以触发onScroll,滑动的时候会多次调用,所以我选择了onFling中搞事情了。然后在写的时候发现onFling在某种情况下是不会触发的,开始是以为我打开的姿势不对,毛线啊,其实有个滑动速度的问题,看看GestureDetector的源码就知道了
GestureDetector.java
public boolean onTouchEvent(MotionEvent ev) {
//...
case MotionEvent.ACTION_UP:
mStillDown = false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
// Finally, give the up event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mInLongPress) {
mHandler.removeMessages(TAP);
mInLongPress = false;
} else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
} else if (!mIgnoreNextUpEvent) {
// A fling must travel the minimum tap distance
final VelocityTracker velocityTracker = mVelocityTracker;
final int pointerId = ev.getPointerId(0);
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final float velocityY = velocityTracker.getYVelocity(pointerId);
final float velocityX = velocityTracker.getXVelocity(pointerId);
if ((Math.abs(velocityY) > mMinimumFlingVelocity)
|| (Math.abs(velocityX) > mMinimumFlingVelocity)){
handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
}
}
//...
}
哦哦哦(´・_・`)………原来有速度大小的判断,于是才有了上面onInterceptTouchEvent的中ACTION_UP对速度的判断逻辑
(PS:花了好长时间才找到这个问题 啊西吧(╯‵□′)╯︵┻━┻)
好,其实呢,ACTION_UP里面最后执行的就是onFling了…….
if(距离不满足){ 回弹 }else if(速度不满足){ 我自己让他动 } else { onFling }
基本实现就这些了,还有个事件分发机制,是个大招来的。View的事件分发机制说白了就是点击事件的传递,也就是一个Down事件,若干个Move事件,一个Up事件构成的事件序列的传递。
当你手指按了屏幕,点击事件就会遵循Activity->Window->View这一顺序传递。
- dispatchTouchEcent:
只要事件传递到了当前View,那么dispatchTouchEcent方法就一定会被调用。返回结果表示是否消耗当前事件。 - onInterceptTouchEvent:
在dispatchTouchEcent方法内部调用此方法,用来判断是否拦截某个事件。如果当前View拦截了某个事件,那么在这同一个事件序列中,此方法不会再次被调用。返回结果表示是否拦截当前事件。 - onTouchEvent:
在dispatchTouchEcent方法内调用此方法,用来处理事件。返回结果表示是否处理当前事件,如果不处理,那么在同一个事件序列里面,当前View无法再收到后续的事件
-
值得注意的是,某个View一旦决定拦截事件,那么这个事件序列之后的事件都会由它来处理,并且不会再调用onInterceptTouchEvent,即onInterceptTouchEvent对down事件拦截了,后续的move,up事件都被截断了都交给去自身的onTouchEvent中执行了。子View不会收到任何事件,事件分发原理深入探讨还是会收获很多的 ps:推荐一本写得很不错的书 《Android开发艺术探索》
二. 弹性圆PagerIndicator
花的时间最多在这里,磕磕碰碰,走了好多弯路(╯‵□′)╯︵┻━┻………………
得做下事先准备功夫
I have a View,I have a ViewGroup ,Ugh~~,Indicator~~
其实我外面还包了一层布局,具体是干嘛的,不告诉你, o( ̄▽ ̄)o.
在此之前,也许需要些自定义View的知识吧
安卓自定义View教程
这里的弹性圆平移主要用了二次贝塞尔曲线然后结合动画去重绘界面.
贝塞尔曲线是啥, 就是由贝塞尔发明的曲线。
弹性圆的平移
1 我得用二次贝塞尔曲线画个圆,假如半径为R,贝塞尔曲线分为控制点和数据点,当控制点和数据点在一条直线上,而且x坐标或者y坐标相差R*0.551915024494个单位,画出来的贝塞尔曲线是个圆
(此处省去一万字解释为何差个0.551915024494就是个圆)
2 平移,点对点,每个子View(CircleButton)的中心点坐标间的平移,平移过程中分解为5个状态,每个状态是数据点的坐标变化引起圆的形变按钮点击的环涟漪效果
这个较为简单,就是描边
代码设计思路
- 圆的平移就是圆心的平移,圆心坐标集合由一个队列控制,可由动画结合定时器控制队头坐标到队尾坐标的移动,
- 圆的核心就是圆心+半径,圆心当点击到每个子View可以计算出,这时就把坐标入队,半径相应也可以知道的,那么控制点和数据点就可以得出。
- 弹性效果,分解为5个动作时区,
(0,s1] 右半圆拉伸
(s1,s2] 左半圆拉伸,右半圆逐渐恢复
(s2,s3] 左半圆恢复
(s3,s4] 左半圆回弹
(s4,1) 左右半圆一起到终点
代码实现
- 事件分发
/**
* 事件分发
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG,"dispatchTouchEvent");
int a = ev.getAction();
switch (a) {
case MotionEvent.ACTION_DOWN:
ex = ev.getX();
ey = ev.getY();
PointF p = new PointF();
Log.i(TAG, "p-" + ex + "," + ey);
View v = isInChildView(ex, ey);
if (v != null&&!isTranslateOrRippleInProgress()) {
/** 当且仅当 触摸点DOWN在子View内 以及动画结束的时候进入*/
/** 当触摸点DOWN在子View内,触发一个描边波浪效果 */
startRipple();
int top = v.getTop();
int bottom = v.getBottom();
int left = v.getLeft();
int right = v.getRight();
p.x = left + (right - left) / 2f;
p.y = top + (bottom - top) / 2f;
pointLimitQueue.offer(p);
Log.i(TAG, "ps-" + pointLimitQueue.queue.toString());
}
if (pointLimitQueue.size() == 1) {
//如果队列中只有一个点,那么直接调用重绘
invalidate();
} else if (pointLimitQueue.size() > 1) {
//一个点以上,则开始圆形平移的动画
startCircleMoving();
}
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
// Log.e(TAG,"dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"dispatchTouchEvent ACTION_UP");
break;
}
/* if(isTranslateOrRippleInProgress()){
Log.e(TAG,"动画进行中");
return true;
}*/
return super.dispatchTouchEvent(ev);
}
/**
* 事件拦截
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG,"onInterceptTouchEvent");
if(isTranslateOrRippleInProgress()
&&pointLimitQueue.getLast().equals(getChildViewCenterPointToQueue(getAttachView().getCurrentPageIndex()))){
/** 当且仅当 动画进行中以及当前滑动的终点页面的下标所对应的PointF是点队列中的最后一个时候,不需要传递事件给子View,*/
return true;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 事件处理
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
- 圆的实现
主要看下代码注释.写的有点乱.
/**
* 描绘圆的4个点.由动画行进时间决定
*
* @param kx 开始位置到结束位置 平移过程中圆形的中心点x坐标的一次函数的斜率
* @param ky 开始位置到束位置 平移过程中圆形的中心点y坐标的一次函数的斜率
* @param centerX 中心点x
* @param centerY 中心点y
* @param bendingDistance 弹性圆的拉伸长度
* @return
*/
private PointF[] initDataPoint(float kx, float ky, float centerX, float centerY, float bendingDistance) {
for (int i = 0; i < dataPoint.length; i++) {
if (dataPoint[i] == null) {
dataPoint[i] = new PointF(0, 0);
firstDataPoint[i] = new PointF(0, 0);
}
}
boolean isRight = bendingDistance > 0;
float s1 = 0.3f;
float s2 = 0.6f;
float s3 = 0.8f;
float s4 = 0.9f;
if (mInterpolatedTime <= 0) {
dataPoint[0].x = centerX - radius;
dataPoint[0].y = centerY;
dataPoint[1].x = centerX;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = centerX + radius;
dataPoint[2].y = centerY;
dataPoint[3].x = centerX;
dataPoint[3].y = centerY + radius;
firstDataPoint[0].x = centerX - radius;
firstDataPoint[0].y = centerY;
firstDataPoint[1].x = centerX;
firstDataPoint[1].y = centerY - radius;
firstDataPoint[2].x = centerX + radius;
firstDataPoint[2].y = centerY;
firstDataPoint[3].x = centerX;
firstDataPoint[3].y = centerY + radius;
List<PointF> ps = Arrays.asList(firstDataPoint);
// Log.i(TAG,"1-firstDataPoint "+ps.toString() );
List<PointF> ds = Arrays.asList(dataPoint);
// Log.i(TAG,"1-dataPoint "+ds.toString() );
} else if (mInterpolatedTime > 0 && mInterpolatedTime <= s1) {
/** 0 - s1 阶段 水平的2个dataPoint,向平移的方向拉伸bendingDistanced的距离,用时为[0,s1]*/
if (isRight) {
dataPoint[0].x = firstDataPoint[0].x;
dataPoint[0].y = firstDataPoint[0].y;
dataPoint[1].x = firstDataPoint[1].x;
dataPoint[1].y = firstDataPoint[1].y;
dataPoint[2].x = firstDataPoint[2].x + bendingDistance / s1 * mInterpolatedTime;
dataPoint[2].y = firstDataPoint[2].y;
dataPoint[3].x = firstDataPoint[3].x;
dataPoint[3].y = firstDataPoint[3].y;
} else {
dataPoint[0].x = firstDataPoint[0].x + bendingDistance / s1 * mInterpolatedTime;
dataPoint[0].y = firstDataPoint[0].y;
dataPoint[1].x = firstDataPoint[1].x;
dataPoint[1].y = firstDataPoint[1].y;
dataPoint[2].x = firstDataPoint[2].x;
dataPoint[2].y = firstDataPoint[2].y;
dataPoint[3].x = firstDataPoint[3].x;
dataPoint[3].y = firstDataPoint[3].y;
}
List<PointF> ps = Arrays.asList(firstDataPoint);
// Log.i(TAG,"2-firstDataPoint "+ps.toString() );
List<PointF> ds = Arrays.asList(dataPoint);
// Log.i(TAG,"2-dataPoint "+ds.toString() );
} else if (mInterpolatedTime > s1 && mInterpolatedTime <= s2) {
if (isRight) {
/** 向右为例, finalCenterX 是终点s2的中心点坐标,注意:中心点的x行走是规律的是(从头到尾都是 y=kx+b )
* 1. 计算垂直方向上的2个点的一次函数表达式,起点是[0,s1]阶段的结束点就是firstDataPoint[1].x,终点就是finalCenterX了..草稿纸计算得如下
* 2. 计算水平方向上的2个点的一次函数表达式,d0是左的点,起点是d0_s1,结束点是finalCenterX-radius-bendingDistance,为什么减掉bendingDistance,因为要营造左点回来较慢被拉伸的效果
* d2是右点,起点是d2_s1,结束点是 finalCenterX+radius 用时s2-s1..草稿纸计算得如下
* */
float finalCenterX = firstCenterX + kx * s2;
float kMiddle_s2 = (finalCenterX - firstDataPoint[1].x) / (s2 - s1);
float bMiddle_s2 = firstDataPoint[1].x - (finalCenterX - firstDataPoint[1].x) * s1 / (s2 - s1);
float d2_s1 = firstDataPoint[2].x + bendingDistance;
float d0_s1 = firstDataPoint[0].x;
float k0_s2 = (finalCenterX - radius -bendingDistance - d0_s1) / (s2 - s1);
float b0_s2 = firstDataPoint[0].x - (finalCenterX - radius -bendingDistance- d0_s1) * s1 / (s2 - s1);
float k2_s2 = (finalCenterX + radius - d2_s1) / (s2 - s1);
float b2_s2 = d2_s1 - s1 * (finalCenterX + radius - d2_s1) / (s2 - s1);
dataPoint[0].x = k0_s2 * mInterpolatedTime + b0_s2;
dataPoint[0].y = centerY;
dataPoint[1].x = kMiddle_s2 * mInterpolatedTime + bMiddle_s2;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = k2_s2 * mInterpolatedTime + b2_s2;
dataPoint[2].y = centerY;
dataPoint[3].x = kMiddle_s2 * mInterpolatedTime + bMiddle_s2;
dataPoint[3].y = centerY + radius;
} else {
float finalCenterX = firstCenterX + kx * s2;
float kMiddle = (finalCenterX - firstDataPoint[1].x) / (s2 - s1);
float bMiddle = firstDataPoint[1].x - (finalCenterX - firstDataPoint[1].x) * s1 / (s2 - s1);
float d0_s1 = firstDataPoint[0].x + bendingDistance;
float d2_s1 = firstDataPoint[2].x /*+ bendingDistance*/;
float k0_s2 = (finalCenterX - radius - d0_s1) / (s2 - s1);
float b0_s2 = d0_s1 - (finalCenterX - radius - d0_s1) * s1 / (s2 - s1);
float k2_s2 = (finalCenterX + radius- bendingDistance- d2_s1) / (s2 - s1);
float b2_s2 = firstDataPoint[2].x - s1 * (finalCenterX + radius- bendingDistance - d2_s1) / (s2 - s1);
dataPoint[0].x = k0_s2 * mInterpolatedTime + b0_s2;
dataPoint[0].y = centerY;
dataPoint[1].x = kMiddle * mInterpolatedTime + bMiddle;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = k2_s2 * mInterpolatedTime + b2_s2;
dataPoint[2].y = centerY;
dataPoint[3].x = kMiddle * mInterpolatedTime + bMiddle;
dataPoint[3].y = centerY + radius;
}
List<PointF> ps = Arrays.asList(firstDataPoint);
// Log.i(TAG,"3-firstDataPoint "+ps.toString() );
List<PointF> ds = Arrays.asList(dataPoint);
// Log.i(TAG,"3-dataPoint "+ds.toString() );
} else if (mInterpolatedTime > s2 && mInterpolatedTime <= s3) {
if (isRight) {
/** 向右为例,便于理解,这里把上面的代码copy了一份,要通过上面[s1,s2]来计算s2时间的结束点位置
*
* */
float finalCenterX_s2 = firstCenterX + kx * s2;
float kMiddle_s2 = (finalCenterX_s2 - firstDataPoint[1].x) / (s2 - s1);
float bMiddle_s2 = firstDataPoint[1].x - (finalCenterX_s2 - firstDataPoint[1].x) * s1 / (s2 - s1);
float d2_s2 = firstDataPoint[2].x + bendingDistance;
float d0_s2 = firstDataPoint[0].x + bendingDistance;
float k0_s2 = (finalCenterX_s2 - radius - d0_s2) / (s2 - s1);
float b0_s2 = firstDataPoint[0].x - (finalCenterX_s2 - radius - d0_s2) * s1 / (s2 - s1);
float k2_s2 = (finalCenterX_s2 + radius - d2_s2) / (s2 - s1);
float b2_s2 = d2_s2 - s1 * (finalCenterX_s2 + radius - d2_s2) / (s2 - s1);
float finalCenterX_s3 = firstCenterX + kx * s3;
float kMiddle_s3 = (finalCenterX_s3 - (kMiddle_s2 * s2 + bMiddle_s2)) / (s3 - s2);
float bMiddle_s3 = (kMiddle_s2 * s2 + bMiddle_s2) - (finalCenterX_s3 - (kMiddle_s2 * s2 + bMiddle_s2)) * s2 / (s3 - s2);
float k0_s3 = (finalCenterX_s3 - radius - (k0_s2 * s2 + b0_s2)) / (s3 - s2);
float b0_s3 = (k0_s2 * s2 + b0_s2) - (finalCenterX_s3 - radius - (k0_s2 * s2 + b0_s2)) * s2 / (s3 - s2);
float k2_s3 = (finalCenterX_s3 + radius - (k2_s2 * s2 + b2_s2)) / (s3 - s2);
float b2_s3 = k2_s2 * s2 + b2_s2 - s2 * (finalCenterX_s3 + radius - (k2_s2 * s2 + b2_s2)) / (s3 - s2);
dataPoint[0].x = k0_s3 * mInterpolatedTime + b0_s3;
dataPoint[0].y = centerY;
dataPoint[1].x = kMiddle_s3 * mInterpolatedTime + bMiddle_s3;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = k2_s3 * mInterpolatedTime + b2_s3;
dataPoint[2].y = centerY;
dataPoint[3].x = kMiddle_s3 * mInterpolatedTime + bMiddle_s3;
dataPoint[3].y = centerY + radius;
} else {
float finalCenterX_s3 = firstCenterX + kx * s2;
float kMiddle_s3 = (finalCenterX_s3 - firstDataPoint[1].x) / (s2 - s1);
float bMiddle_s3 = firstDataPoint[1].x - (finalCenterX_s3 - firstDataPoint[1].x) * s1 / (s2 - s1);
float d0_s3 = firstDataPoint[0].x + bendingDistance;
float d2_s3 = firstDataPoint[2].x + bendingDistance;
float k0_s3 = (finalCenterX_s3 - radius - d0_s3) / (s2 - s1);
float b0_s3 = d0_s3 - (finalCenterX_s3 - radius - d0_s3) * s1 / (s2 - s1);
float k2_s3 = (finalCenterX_s3 + radius - d2_s3) / (s2 - s1);
float b2_s3 = firstDataPoint[2].x - s1 * (finalCenterX_s3 + radius - d2_s3) / (s2 - s1);
float finalCenterX_s4 = firstCenterX + kx * s3;
float kMiddle_s4 = (finalCenterX_s4 - (kMiddle_s3 * s2 + bMiddle_s3)) / (s3 - s2);
float bMiddle_s4 = kMiddle_s3 * s2 + bMiddle_s3 - (finalCenterX_s4 - (kMiddle_s3 * s2 + bMiddle_s3)) * s2 / (s3 - s2);
float k0_s4 = (finalCenterX_s4 - radius - (k0_s3 * s2 + b0_s3)) / (s3 - s2);
float b0_s4 = (k0_s3 * s2 + b0_s3) - (finalCenterX_s4 - radius - (k0_s3 * s2 + b0_s3)) * s2 / (s3 - s2);
float k2_s4 = (finalCenterX_s4 + radius - (k2_s3 * s2 + b2_s3)) / (s3 - s2);
float b2_s4 = (k2_s3 * s2 + b2_s3) - s2 * (finalCenterX_s4 + radius - (k2_s3 * s2 + b2_s3)) / (s3 - s2);
dataPoint[0].x = k0_s4 * mInterpolatedTime + b0_s4;
dataPoint[0].y = centerY;
dataPoint[1].x = kMiddle_s4 * mInterpolatedTime + bMiddle_s4;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = k2_s4 * mInterpolatedTime + b2_s4;
dataPoint[2].y = centerY;
dataPoint[3].x = kMiddle_s4 * mInterpolatedTime + bMiddle_s4;
dataPoint[3].y = centerY + radius;
}
List<PointF> ps = Arrays.asList(firstDataPoint);
// Log.i(TAG,"4-firstDataPoint "+ps.toString() );
List<PointF> ds = Arrays.asList(dataPoint);
// Log.i(TAG,"4-dataPoint "+ds.toString() );
} else {
if(isRight) {
if(mInterpolatedTime>s3&&mInterpolatedTime<=s4) {
float d = radius;
float k = d*0.25f/(s3-s4);
float b = d*0.25f-k*s3;
dataPoint[0].x = centerX-(d*3f/4f + k*mInterpolatedTime+b);
dataPoint[0].y = centerY;
dataPoint[1].x = centerX;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = centerX + radius;
dataPoint[2].y = centerY;
dataPoint[3].x = centerX;
dataPoint[3].y = centerY + radius;
}else if(mInterpolatedTime>s4&&mInterpolatedTime<=1){
float d = radius;
float k = -d*0.25f/(s4-1);
float b = -k*s4;
dataPoint[0].x = centerX-(d*3f/4f + k*mInterpolatedTime+b);
dataPoint[0].y = centerY;
dataPoint[1].x = centerX;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = centerX + radius;
dataPoint[2].y = centerY;
dataPoint[3].x = centerX;
dataPoint[3].y = centerY + radius;
}
}else {
if(mInterpolatedTime>s3&&mInterpolatedTime<=s4) {
float d = radius;
float k = d*0.25f/(s3-s4);
float b = d*0.25f-k*s3;
dataPoint[0].x = centerX - radius;
dataPoint[0].y = centerY;
dataPoint[1].x = centerX;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = centerX+d*3f/4f +k*mInterpolatedTime+b ;
dataPoint[2].y = centerY;
dataPoint[3].x = centerX;
dataPoint[3].y = centerY + radius;
}else if(mInterpolatedTime>s4&&mInterpolatedTime<=1){
float d = radius;
float k = -d*0.25f/(s4-1);
float b = -k*s4;
dataPoint[0].x = centerX - radius;
dataPoint[0].y = centerY;
dataPoint[1].x = centerX;
dataPoint[1].y = centerY - radius;
dataPoint[2].x = centerX+d*3f/4f +k*mInterpolatedTime+b;
dataPoint[2].y = centerY;
dataPoint[3].x = centerX;
dataPoint[3].y = centerY + radius;
}
}
List<PointF> ps = Arrays.asList(firstDataPoint);
// Log.i(TAG,"5-firstDataPoint "+ps.toString() );
List<PointF> ds = Arrays.asList(dataPoint);
// Log.i(TAG,"5-dataPoint "+ds.toString() );
}
return dataPoint;
}
/**
* 初始化控制点
*
* @param dataPoint 圆的4个数据点
* @return
*/
private PointF[] initCtrlPoint(PointF[] dataPoint) {
for (int i = 0; i < ctrlPoint.length; i++) {
ctrlPoint[i] = new PointF(0, 0);
}
diff = radius * C;
ctrlPoint[0].x = dataPoint[0].x;
ctrlPoint[0].y = dataPoint[0].y - diff;
ctrlPoint[1].x = dataPoint[1].x - diff;
ctrlPoint[1].y = dataPoint[1].y;
ctrlPoint[2].x = dataPoint[1].x + diff;
ctrlPoint[2].y = dataPoint[1].y;
ctrlPoint[3].x = dataPoint[2].x;
ctrlPoint[3].y = dataPoint[2].y - diff;
ctrlPoint[4].y = dataPoint[2].y + diff;
ctrlPoint[4].x = dataPoint[2].x;
ctrlPoint[5].y = dataPoint[3].y;
ctrlPoint[5].x = dataPoint[3].x + diff;
ctrlPoint[6].y = dataPoint[3].y;
ctrlPoint[6].x = dataPoint[3].x - diff;
ctrlPoint[7].x = dataPoint[0].x;
ctrlPoint[7].y = dataPoint[0].y + diff;
return ctrlPoint;
}
/**
* 绘制贝塞尔曲线
*
* @param canvas
*/
private void drawCubicBezier(Canvas canvas) {
/** 清除Path中的内容
reset不保留内部数据结构,但会保留FillType.
rewind会保留内部的数据结构,但不保留FillType */
path.reset();
path.moveTo(dataPoint[0].x, dataPoint[0].y);
path.cubicTo(ctrlPoint[0].x, ctrlPoint[0].y, ctrlPoint[1].x, ctrlPoint[1].y, dataPoint[1].x, dataPoint[1].y);
path.cubicTo(ctrlPoint[2].x, ctrlPoint[2].y, ctrlPoint[3].x, ctrlPoint[3].y, dataPoint[2].x, dataPoint[2].y);
path.cubicTo(ctrlPoint[4].x, ctrlPoint[4].y, ctrlPoint[5].x, ctrlPoint[5].y, dataPoint[3].x, dataPoint[3].y);
path.cubicTo(ctrlPoint[6].x, ctrlPoint[6].y, ctrlPoint[7].x, ctrlPoint[7].y, dataPoint[0].x, dataPoint[0].y);
paint.setColor(getResources().getColor(R.color.colorAccent));
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, paint);
}
- 动画的实现
定时器
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
Log.i(TAG,"Handler开始 " + translateState );
if (translateState != STATE_START &&translateState != STATE_MOVING && pointLimitQueue.size() > 1) {
Log.i(TAG,"startCircleMovingAnimatior");
startCircleMovingAnimatior();
}
break;
case 1:
// 直接移除,定时器停止
Log.i(TAG,"Handler Remove " + translateState);
handler.removeMessages(0);
break;
case 3:
//开始描边波浪动画
startRippleAnimatior();
break;
default:
break;
}
}
};
- CircleMoving动画,学了属性动画的使用,就用它吧,这样平移动画和环涟漪动画就可以同时进行了,其实遇到一个bug,所以就让这两种效果同时进行,之前写的时候,如果先执行平移再到环涟漪,然后还是2个线程跑,环涟漪最后invalidate的时候,此时我的点队列中只有一个点坐标,重绘的时候就会在终点闪烁一下 (⊙﹏⊙)
ps:不会属性动画的可以看下鸿洋大大的csdn博客,介绍得很详细b( ̄▽ ̄)d
/**
* 改用属性动画,同时进行圆形平移和环涟漪
*/
private void startCircleMovingAnimatior(){
translateState = rippleState= STATE_START;
ValueAnimator translateAnimator = ValueAnimator.ofFloat(0f,1f);
translateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mInterpolatedTime = (float)animation.getAnimatedValue();
rippleInterpolatedTime = (float)animation.getAnimatedValue();
Log.e(TAG,"startTranslateAnimatior interpolatedTime "+mInterpolatedTime);
if(mInterpolatedTime!=1){
translateState = rippleState = STATE_MOVING;
}else{
Log.e(TAG,"TranslateAnimation poll() before -- " +pointLimitQueue.queue.toString());
pointLimitQueue.poll();
Log.e(TAG,"TranslateAnimation poll() after -- " +pointLimitQueue.queue.toString());
translateState = rippleState = STATE_STOP;
}
invalidate();
}
});
translateAnimator.setDuration(1000);
translateAnimator.setInterpolator(new LinearInterpolator());
translateAnimator.start();
}
- 环涟漪动画,开放给外部有需要调用一下下
public void startRippleAnimatior(){
rippleState = STATE_START;
ValueAnimator rippleAnimator = ValueAnimator.ofFloat(0f,1f);
rippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
rippleInterpolatedTime = (float)animation.getAnimatedValue();
// Log.e(TAG,"startRippleAnimatior interpolatedTime "+mInterpolatedTime);
if(mInterpolatedTime!=1){
rippleState = STATE_MOVING;
}else{
rippleState = STATE_STOP;
}
invalidate();
}
});
rippleAnimator.setDuration(500);
rippleAnimator.setInterpolator(new LinearInterpolator());
rippleAnimator.start();
}
尤其在处理点队列的时候,注意
- 在动画结束时候停止定时器,动画进行中不可做入队操作,不然后面的流程就紊乱了
- 在动画进行中不可为子View传递事件,防止在动画过程点入队,其实说白了平移的时候按钮不可点击
就这么几句话,其实很头大….debug很难定位问题,每晚静坐电脑前看log看得也醉了….(((φ(◎ロ◎;)φ)))
那么还有一个很头大的就是下面的红色圆在ICON的背面移动!!,其实不算难,我用了一个很笨方法的,简单粗暴
CustomRelativeLayout作用就在这里
CircleButon只是用来画了个白边罢了,然后与SlidingViewPager产生联动
CustomPagerIndicator则是展示了动画效果,但可不会去图片资源加载出来,真正加载图片的是CustomRelativeLayout了,这样就会看到一个图层叠加的效果
CircleButon.java的关键代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/** 边界 */
canvas.drawCircle(mWidth/2,getRadius(),getRadius(),borderPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
CustomPagerIndicator parent = (CustomPagerIndicator)getParent();
SlidingViewPager slidingViewPager = (SlidingViewPager)getAttachView();
int a = event.getAction();
if(parent.isTranslateOrRippleInProgress()&&a==MotionEvent.ACTION_UP){
Log.e(TAG,"在动画进行中");
// setClickable(false);
}
switch (a){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
// Log.e(TAG,"onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
int pageIndex = slidingViewPager.getCurrentPageIndex();
int childIndex = parent.indexOfChild(this);
Log.i(TAG,"onTouchEvent ACTION_UP "+pageIndex+" moveTo "+childIndex);
slidingViewPager.moveTo(pageIndex,childIndex);
break;
}
return super.onTouchEvent(event);
}
CustomRelativeLayout的关键代码:
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
drawLayer(canvas);
}
private void drawLayer(Canvas canvas){
customPagerIndicator = (CustomPagerIndicator) getChildAt(0);
//用于绘制icon...
for(int i = 0; i< customPagerIndicator.getChildCount(); i++){
CircleButton circleButton = (CircleButton) customPagerIndicator.getChildAt(i);
int top = circleButton.getTop();
int bottom = circleButton.getBottom();
int left = circleButton.getLeft();
int right = circleButton.getRight();
Drawable drawable = circleButton.getDrawable();
Bitmap bitmap = drawableToBitmap(drawable);
canvas.drawBitmap(bitmap,left+circleButton.getRadius()-bitmap.getWidth()/2,top+circleButton.getRadius()-bitmap.getHeight()/2,p);
}
}
贴上
项目地址 : https://github.com/gaoqionglou/CustomView
其实还有很多想法,比如实现颜色渐变啦,上方ViewPager能牵引下方来运动啦(卧槽,好难-,-投降)等等
其实图层那种,是不是可以用canvas的交集来做呢…..
遇到问题需要不断LogLogLog….然后再重现整理思路找到问题出现的原因.Then Fix them.
写完反观整个项目代码,主要是为了熟悉使用自定义View的知识和理解事件分发原理的目的。