前言:
看著自己簡(jiǎn)書創(chuàng)建時(shí)間2016年,CSDN2015年。自己在這最好的年華居然連技術(shù)文章或者論文都沒(méi)發(fā)表過(guò),怎么對(duì)的起我看過(guò)前輩們的心血和付出。在此我還是決定了今后的總結(jié)方向不在是單一的筆記和書本,還是為IT大軍做一份貢獻(xiàn)。
正文:
寫這篇文章主要是為了當(dāng)前日益增多三方庫(kù)和開(kāi)發(fā)中的一些日常造輪子,加自己的經(jīng)驗(yàn)總結(jié)
效果圖:

庫(kù):
SmartRefreshLayout
Lottie
LottieFiles
布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/sfl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.xxx.lottie.DesginLottieHeadRefresh
android:id="@+id/headRefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:gravity="center"
android:text="測(cè)試代碼"
android:layout_height="wrap_content" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>
簡(jiǎn)單的布局和自定義的HeadRefresh
DesginLottieHeadRefresh:
public class DesginLottieHeadRefresh extends ViewGroup implements RefreshHeader {
private LottieAnimationView lav;
private String asset_loading_json = "desgin/newAnimation.json";
//中心點(diǎn)
private int mCircleDiameter;
@VisibleForTesting
private static final int CIRCLE_DIAMETER = 160;
private RefreshState mState;
public DesginLottieHeadRefresh(Context context) {
this(context, null);
}
public DesginLottieHeadRefresh(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() == 0) {
return;
}
final int width = getMeasuredWidth();
int lottieWidth = lav.getMeasuredWidth();
int lottieHeight = lav.getMeasuredHeight();
int leftLav = width / 2 - lottieWidth / 2;
int topLav = 0;
lav.layout(leftLav, topLav, leftLav + lottieWidth, topLav + lottieHeight / 2);
}
private void initView(Context context) {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
lav = new LottieAnimationView(context);
lav.setAnimation(asset_loading_json);
lav.loop(true);
addView(lav);
}
@NonNull
@Override
public View getView() {
return this;
}
@NonNull
@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.MatchLayout;
}
@Override
public void setPrimaryColors(int... colors) {
}
@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int extendHeight) {
}
@Override
public void onMoving(boolean isDragging, float percent, int offset, int height, int extendHeight) {
}
@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int extendHeight) {
}
@Override
public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int extendHeight) {
lav.playAnimation();
}
@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
// lav.clearAnimation();
if (lav != null) {
lav.cancelAnimation();
lav.clearAnimation();
}
return 0;
}
@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {
}
@Override
public boolean isSupportHorizontalDrag() {
return false;
}
@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
mState = newState;
switch (newState) {
case None:
lav.setFrame(0);
lav.setProgress(0);
break;
case PullDownToRefresh:
lav.setVisibility(View.VISIBLE);
break;
case PullDownCanceled:
break;
case ReleaseToRefresh:
lav.setVisibility(View.VISIBLE);
break;
case Refreshing:
break;
case RefreshFinish:
lav.setVisibility(View.GONE);
break;
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// canvas.save();
// lav.draw(canvas);
// canvas.restore();
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
lav.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus) {
}
}
實(shí)現(xiàn)步驟:
1.implements RefreshHead View或者ViewGroup
2.實(shí)現(xiàn) onMeasure-onLayout 或者實(shí)現(xiàn)onDraw(Canvas canvas)
3.Lottiview 加載assets目錄下面 .json動(dòng)畫完成初步顯示
4.根據(jù)Lottie和SmartRefreshLayout api 完成動(dòng)畫連貫和狀態(tài)更新
RefreshHead > RefreshInternal
/**
* 獲取實(shí)體視圖
* @return 實(shí)體視圖
*/
@NonNull
View getView();
/**
* 獲取變換方式 {@link SpinnerStyle} 必須返回 非空
* @return 變換方式
*/
@NonNull
SpinnerStyle getSpinnerStyle();
/**
* 設(shè)置主題顏色
* @param colors 對(duì)應(yīng)Xml中配置的 srlPrimaryColor srlAccentColor
*/
void setPrimaryColors(@ColorInt int... colors);
/**
* 尺寸定義完成 (如果高度不改變(代碼修改:setHeader),只調(diào)用一次, 在RefreshLayout#onMeasure中調(diào)用)
* @param kernel RefreshKernel
* @param height HeaderHeight or FooterHeight
* @param extendHeight extendHeaderHeight or extendFooterHeight
*/
void onInitialized(@NonNull RefreshKernel kernel, int height, int extendHeight);
/**
* 手指拖動(dòng)下拉(會(huì)連續(xù)多次調(diào)用)
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+extendHeight)
* @param height 高度 HeaderHeight or FooterHeight
* @param extendHeight 擴(kuò)展高度 extendHeaderHeight or extendFooterHeight
*/
void onPulling(float percent, int offset, int height, int extendHeight);
/**
* 手指釋放之后的持續(xù)動(dòng)畫(會(huì)連續(xù)多次調(diào)用)
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+extendHeight)
* @param height 高度 HeaderHeight or FooterHeight
* @param extendHeight 擴(kuò)展高度 extendHeaderHeight or extendFooterHeight
*/
void onReleasing(float percent, int offset, int height, int extendHeight);
/**
* 釋放時(shí)刻(調(diào)用一次,將會(huì)觸發(fā)加載)
* @param refreshLayout RefreshLayout
* @param height 高度 HeaderHeight or FooterHeight
* @param extendHeight 擴(kuò)展高度 extendHeaderHeight or extendFooterHeight
*/
void onReleased(RefreshLayout refreshLayout, int height, int extendHeight);
/**
* 開(kāi)始動(dòng)畫
* @param refreshLayout RefreshLayout
* @param height HeaderHeight or FooterHeight
* @param extendHeight extendHeaderHeight or extendFooterHeight
*/
void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int extendHeight);
/**
* 動(dòng)畫結(jié)束
* @param refreshLayout RefreshLayout
* @param success 數(shù)據(jù)是否成功刷新或加載
* @return 完成動(dòng)畫所需時(shí)間 如果返回 Integer.MAX_VALUE 將取消本次完成事件,繼續(xù)保持原有狀態(tài)
*/
int onFinish(@NonNull RefreshLayout refreshLayout, boolean success);
/**
* 水平方向的拖動(dòng)
* @param percentX 下拉時(shí),手指水平坐標(biāo)對(duì)屏幕的占比(0 - percentX - 1)
* @param offsetX 下拉時(shí),手指水平坐標(biāo)對(duì)屏幕的偏移(0 - offsetX - LayoutWidth)
* @param offsetMax 最大的偏移量
*/
void onHorizontalDrag(float percentX, int offsetX, int offsetMax);
/**
* 是否支持水平方向的拖動(dòng)(將會(huì)影響到onHorizontalDrag的調(diào)用)
* @return 水平拖動(dòng)需要消耗更多的時(shí)間和資源,所以如果不支持請(qǐng)返回false
*/
boolean isSupportHorizontalDrag();
代碼很簡(jiǎn)單,源碼中有中文注解就不一一說(shuō)明
LottileAnimation
結(jié)合官網(wǎng)和部分源碼很好實(shí)現(xiàn)Lottie在RefreshHead 中的實(shí)現(xiàn) ?。?!
重點(diǎn):
View,ViewGroup的生命周期和Wind上面的渲染過(guò)程,剛開(kāi)始的時(shí)候去繼承View拿到當(dāng)前.json動(dòng)畫的寬高和在onMeasure中一直是0,0后來(lái)改為ViewGroup 子類重新自測(cè)measure寬和高得到的也是0,0。這下搞的我翻了波筆記本
筆記本mark入口
后來(lái)才決定改為CIRCLE_DIAMETER 和mCircleDiameter根據(jù)自己的分辨率和中心點(diǎn)來(lái)繪制動(dòng)畫.json的大小
(主要為了適配動(dòng)畫在不同分辨率手機(jī)里面的效果)
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
小伙伴也可以使用AT_MOST來(lái)根據(jù)父布局的指定獲取當(dāng)前自定義View的寬和高
OK到這里基本完善了Lottie動(dòng)畫能在Header里面指定的位置跳動(dòng)了,接下run了一次發(fā)現(xiàn)動(dòng)畫確實(shí)是在Head里面跳動(dòng),但是因?yàn)樵O(shè)置了looper(true)的屬性本身是不和Refresh onRefreshing 時(shí)間沖突,但是后面再次下拉刷新的時(shí)候出現(xiàn)了動(dòng)畫的幀數(shù)不是原來(lái)第一幀,這下糾結(jié)了,我一般不喜歡手動(dòng)導(dǎo)入三方源碼修改別人的源碼主要以前被(XXX)坑哭過(guò),升級(jí)一次,我基本要上重構(gòu)一次我的項(xiàng)目。好在快速瀏覽了一遍L(zhǎng)ottieAnimationView的源碼,好在和我猜測(cè)的一樣 LottieDraw 和LottieAnimator 果然是根據(jù)Frame幀來(lái)實(shí)現(xiàn)動(dòng)畫的過(guò)程

那么現(xiàn)在來(lái)了不是給我機(jī)會(huì)為所欲為最大幀和最小幀 整個(gè)圖片繪制過(guò)程Progress等
switch (newState) {
case None:
lav.setFrame(0);
lav.setProgress(0);
break;
case PullDownToRefresh:
lav.setVisibility(View.VISIBLE);
break;
case PullDownCanceled:
break;
case ReleaseToRefresh:
lav.setVisibility(View.VISIBLE);
break;
case Refreshing:
break;
case RefreshFinish:
lav.setVisibility(View.GONE);
break;
}
配合上層接口對(duì)代碼做了最后的處理
喜歡效果小伙伴可以在去關(guān)注SmartRefreshLayout refresh-heads 和fresh-foot代碼的實(shí)現(xiàn),其中Vector向量和對(duì)View,ViewGroup,Drawable繪制 是很不錯(cuò)的學(xué)習(xí)源碼。
OK 效果做出來(lái)了
經(jīng)驗(yàn)分享:
記得幾年前做Android開(kāi)發(fā)的時(shí)拿到第三方庫(kù)或者框架很是頭疼和煩躁,后面接觸多了能心平氣和的寫代碼,反而覺(jué)得開(kāi)發(fā)過(guò)程在別人車輪下面還是相對(duì)容易的,api 知識(shí)體系清楚的情況下,功能實(shí)現(xiàn)反而很輕松! 時(shí)代在進(jìn)步,人也在進(jìn)步,學(xué)習(xí)是IT的必經(jīng)之路,找準(zhǔn)自己愛(ài)好堅(jiān)持下去就行。
以后博主每周五分享一篇博客(Kotilin React-native Android Flutter Java)