博主是愛奇藝員工愉阎,以上幾個都是從愛 奇藝泡泡客戶端中截取的绞蹦。
本文中一共舉出了四個栗子:內(nèi)容由簡到難,但是分析方法和基本原理都是相似的榜旦。
本文四個控件的代碼都是筆者自己手寫的幽七。希望可以給自己留下些筆記,也給后來者一些啟發(fā)溅呢。
一. 下拉回彈控件 + 收起
功能點分析
- 下拉手勢判定 + View位移
- 松手之后 + View位移
View位移推薦使用translationY, 建議在做位移操作時不要直接調(diào)用View.setTranslationY()
而是應(yīng)該封裝一個統(tǒng)一的方法
public float getCurrentOffset(){
return getTranslationY();
}
public void setOffset(float targetScrollX){
//標(biāo)準(zhǔn)坐標(biāo)軸 右下為正
//進行左右平移時澡屡,需要保證平移的scrollX 范圍是 0 - mRefreshView.width()
targetScrollX = checkOffsetX(targetScrollX);
// scrollTo(0,(int)targetScrollX);
setTranslationY(targetScrollX);
}
private float checkOffsetX(float target) {
if(target > getMaxOffset() /*|| Math.abs(target - getMaxOffset()) < 10*/){
target = getMaxOffset();
}else if(target < 0){
target = 0;
}
return target;
}
這樣的好處是:如果希望修改一種位移方式(例如使用ScrollTo)時,所做的修改量很小咐旧。
核心的事件處理部分:
/*相關(guān)變量*/
private float mTouchSlop;//最小位移
/*上一次的點擊位置*/
private float mXDown;
private float mYDown;
private float mYLastMove;//上一次move事件的Y坐標(biāo)
private float mYMove;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mHandler!=null && mHandler.shouldForbidden() || ev.getPointerCount() > 1){
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mYDown = ev.getRawY();
mYLastMove = mYDown;
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
mYMove = ev.getRawY();
float diffX = (mXMove - mXDown);
float diffY = (mYMove - mYDown);
if(Math.abs(diffY) < mTouchSlop || Math.abs(diffY * 0.5) < Math.abs(diffX)){// 過濾掉水平方向的手勢
break;
}
mYLastMove = mYMove;
return true;
}
return super.onInterceptTouchEvent(ev);
}
public float getMaxOffset(){
return mTargetView.getHeight();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mYMove = event.getRawY();
float deltaY = 1.2f * (mYMove - mYLastMove);//正規(guī)坐標(biāo)軸下的偏移
setOffset(getCurrentOffset() + deltaY);
mYLastMove = mYMove;
break;
case MotionEvent.ACTION_UP:
// 當(dāng)手指抬起時驶鹉,根據(jù)當(dāng)前的滾動值來判定應(yīng)該滾動到哪個子控件的界面
onRelease();
break;
}
return true;
}
public float getCurrentOffset(){
return getTranslationY();
}
整體思路還是按照View的動作攔截機制完成的。
在onInterceptTouchEvent進行動作判別铣墨、攔截室埋。
在onTouchEvnet中完成偏移量計算、View的位移伊约、以及回彈動畫的播放姚淆。
回彈動畫
public void onRelease(){
final boolean hasGotPoint = Math.abs(getCurrentOffset()) >= mTriggerPoint;
mAnimator = ValueAnimator.ofFloat(getCurrentOffset(), hasGotPoint? getMaxOffset() : 0).setDuration(ANIMATOR_DURATION);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float)animation.getAnimatedValue();
setOffset(animatedValue);
}
});
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//todo 進入詳情頁
if(mListener!=null && hasGotPoint) {
mListener.onTriggered();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mAnimator.start();
}
二. 視頻縮放 + View動畫
這個效果看起來稍微復(fù)雜,但是基本實現(xiàn)思路是類似的
1.找到合適的動作觸發(fā)時機
2.對View進行操作
除此之外還有幾個點需要注意:
1.從上圖可以看到視頻的主要形態(tài)有三種屡律,100%腌逢,80%以及隱藏。狀態(tài)的跳轉(zhuǎn)需要記錄超埋。
由于這個view的動畫基本上是只要觸發(fā)就會進行下去的搏讶。
- 內(nèi)部還有個ListView佳鳖。需要處理好和ListView的沖突。
3.另外媒惕,由于動作幾乎是立即觸發(fā)并且不可逆的(施加動作之后就會執(zhí)行形變)
所以系吩,我們只在onInterceptTouchEvnet中就可以完成主要邏輯了。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (listView == null || videoLayout == null) {//子控件還未初始化
return super.onInterceptTouchEvent(ev);
}
if (!enable) {//禁用開關(guān)
return super.onInterceptTouchEvent(ev);
}
//操作區(qū)域在listView以上,即視頻區(qū)域內(nèi)
int y = (int) ev.getRawY();
int x = (int) ev.getRawX();
int[] location = new int[2];
listView.getLocationOnScreen(location);
if (y < location[1]) {
return super.onInterceptTouchEvent(ev);
}
if (isAnimationPlaying) {
return true;//動畫播放期間禁止操作
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 發(fā)生down事件時,記錄y坐標(biāo)
mLastMotionY = y;
mLastMotionX = x;
break;
case MotionEvent.ACTION_MOVE:
deltaY = y - mLastMotionY;
if (Math.abs(deltaY) < 20) {
break;
}
if (!isVideoStop() && isListViewTopping()) {
//非暫停態(tài)
if (deltaY < 0 && videoState == VIDEO_LAYOUT_NORMAL_SIZE) {
zoomInVideoLayout(VIDEO_LAYOUT_HALF_SIZE);
return true;
} else if (deltaY > 0 && videoState != VIDEO_LAYOUT_NORMAL_SIZE) {
zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
return true;
}
}
if (isVideoStop()) {
if (deltaY < 0 && videoState != VIDEO_LAYOUT_ZERO_SIZE) {
zoomInVideoLayout(VIDEO_LAYOUT_ZERO_SIZE);
return true;
} else if (deltaY > 0 && isListViewTopping()) {
if (videoState == VIDEO_LAYOUT_ZERO_SIZE) {
zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
return true;
}
}
}
break;
}
return super.onInterceptTouchEvent(ev);
}
三. 左拉刷新
從原理上來講吓笙,這個控件其實和常見的下拉刷新控件是一樣的淑玫。只是方向變?yōu)榱讼蜃蠡瑒印?/p>
完全從零做起的,實現(xiàn)一個這個小控件也是挺有意思的面睛。
主要思路是絮蒿,在視覺區(qū)域以外的地方添加一個新View(indicate 刷新狀態(tài))
主要動作是對整個View做位移動畫。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mTargetView.layout(l,t,r,b);//在此栗子中是圖片
mRefreshView.layout(r,t,r + mRefreshView.getMeasuredWidth(),b);//左拉提示叁鉴,旋轉(zhuǎn)指示等
}
而動作判別又是我們熟悉的那一套代碼啦
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mHandler!=null && mHandler.shouldForbidden()){
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mYDown = ev.getRawY();
mXLastMove = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
mYMove = ev.getRawY();
float diffX = (mXMove - mXDown);
float diffY = (mYMove - mYDown);
if(Math.abs(diffX * 0.5) < Math.abs(diffY)){
break;
}
mXLastMove = mXMove;
// 當(dāng)手指拖動值大于TouchSlop值時土涝,認(rèn)為應(yīng)該進行滾動,攔截子控件的事件
//向左滑動
if (diffX < 0 && Math.abs(diffX) > mTouchSlop && !canTargetScrollLeft()) {
return true;
}else if(diffX > 0 && Math.abs(diffX) > mTouchSlop && isRefreshViewDisplayed()){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
float diffX = 1.6f * (mXMove - mXLastMove);//正規(guī)坐標(biāo)軸下的偏移
diffX = diffX * (1.2f - (getCurrentOffset()/getMaxOffset()));//阻尼修正
float target = checkOffsetX(getCurrentOffset()- diffX);
if(getMaxOffset() * mPercentFactor < target){
mRefreshView.setExplodeState(true);//爆炸特效 + 提示轉(zhuǎn)換
}else{
mRefreshView.setExplodeState(false);
}
setOffset(target);
mXLastMove = mXMove;
break;
case MotionEvent.ACTION_UP:
// 當(dāng)手指抬起時幌墓,根據(jù)當(dāng)前的滾動值來判定應(yīng)該滾動到哪個子控件的界面
//todo 進入詳情頁
if(mListener!=null && mRefreshView.isHasExploded()) {
mListener.onTriggered();
}
postDelayed(new Runnable() {
@Override
public void run() {
onRelease();
}
}, mRefreshView.isHasExploded() ? 500 :0);
break;
}
return super.onTouchEvent(event);
}
主要動作核心代碼:
public void setOffset(float targetScrollX){
//標(biāo)準(zhǔn)坐標(biāo)軸 右下為正
//進行左右平移時但壮,需要保證平移的scrollX 范圍是 0 - mRefreshView.width()
targetScrollX = checkOffsetX(targetScrollX);
float percent = (targetScrollX / getMaxOffset())/ mPercentFactor;
percent = Math.min(percent,1);
mRefreshView.updatePullPercent(percent);
scrollTo((int)targetScrollX,0);
}
private float checkOffsetX(float targetScrollX) {
if(targetScrollX > mRefreshView.getWidth() /*|| Math.abs(targetScrollX - getMaxOffset()) < 10*/){
targetScrollX = mRefreshView.getWidth();
}else if(targetScrollX < 0){
targetScrollX = 0;
}
return targetScrollX;
}
被刷新的View被抽象出來作為mRefreshView,相對比較簡單常侣,只要實現(xiàn)了
void updatePullPercent(float percent);
void setExploedState(boolean explored);
這里除了問題提示之外蜡饵,還有一個
旋轉(zhuǎn)的箭頭以及漸變的綠色背景。
箭頭是現(xiàn)成的UI圖胳施,綠色背景稍微麻煩一些溯祸,需要使用顏色漸變來完成。
下面的RotateArrowView 實現(xiàn)了這個功能舞肆,順便將箭頭也add了進來焦辅。
//只包括了這個類的核心代碼
public class RotateArrowView extends FrameLayout {
private ArgbEvaluator argbEvaluator = new ArgbEvaluator();
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int x = getMeasuredWidth()/2;
int y = getMeasuredHeight()/2;
int radius = getWidth()/2;
canvas.drawCircle(x,y,radius,mPaint);
}
public void updatePercent(float percent){
int evaluateColor = (int)argbEvaluator.evaluate(percent, startColor, endColor);
mPaint.setColor(evaluateColor);
arrow.setRotation(180* percent);//箭頭的角度需要旋轉(zhuǎn)
postInvalidate();
}
}
ArgbEvaluator 是谷歌提供的一個方便的顏色漸變計算器。
之前對ViewGroup在直覺上有個誤解椿胯,就是復(fù)寫父view的onDraw要考慮和子View z-index上的層級關(guān)系筷登。
實際上ViewGroup的onDraw復(fù)寫之后,并不會影響到其子View(只是默默地在最后面畫了一個背景)哩盲。
其實思考一下也是前方,父View以及子View的z-index層級關(guān)系是在layout時就已經(jīng)確定好的。如果需要在onDraw再去費心考慮种冬,對于api使用者而言是一個災(zāi)難镣丑。