本篇文章已受權微信公衆號 hongyangAndroid (鴻洋)獨家發佈 javascript
轉載請標明出處:
gold.xitu.io/post/581c51…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/P…java一 概述
本來只是想模仿一下我魂牽夢縈的StoreHouse效果,沒想到意外擼出來一個工具庫。android
最簡單用法,給我一個path(能夠有多段),我還你一個動畫。git
I have a path.I have a view. (Oh~),Path(Anim)View.github
<com.mcxtzhang.pathanimlib.PathAnimView
android:id="@+id/pathAnimView1"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@color/blue"
android:padding="5dp"/>複製代碼
Path sPath = new Path();
sPath.moveTo(0, 0);
sPath.addCircle(40, 40, 30, Path.Direction.CW);
pathAnimView1.setSourcePath(sPath);複製代碼
先看效果圖:(真機效果更棒哦,我本身的手機是去年某款599的手機,算是低端的了,6個View一塊兒動畫,不會卡,查看GPU呈現模式,95%時間都處於16ms線如下。性能還能夠的)canvas
目前可配參數:
1 繪製方面,支持繪製Path的前景 背景色。api
//設置顏色
fillView2.setColorBg(Color.WHITE).setColorFg(Color.BLACK);複製代碼
2 動畫方面,目前支持設置動畫的時長,是否無限循環等。微信
//設置了動畫總時長,只執行一次的動畫
fillView2.setAnimTime(3000).setAnimInfinite(false).startAnim();複製代碼
3 仿StoreHouse風格的View,還支持設置殘影的長度。架構
//設動畫時長,設置了stoneHouse殘影長度
storeView3.setPathMaxLength(1200).setAnimTime(20000).startAnim();複製代碼
4 固然你能夠拿到Paint本身搞事情:ide
//固然你能夠本身拿到Paint,而後搞事情,我這裏設置線條寬度
pathAnimView1.getPaint().setStrokeWidth(10);複製代碼
PathAnimView的數據源是Path。(給我一個Path,還你一個動畫View)
因此內置了幾種將別的資源->Path的方法。
1 直接傳string。 StoreHouse風格支持的A-Z,0-9 "." "- " " "(源自百萬大神的庫文末也有鳴謝,)
//根據String 轉化成Path
setSourcePath(PathParserUtils.getPathFromArrayFloatList(StoreHousePath.getPath("ZhangXuTong", 1.1f, 16)));複製代碼
2 定義在R.array.xxx裏
//動態設置 從StringArray裏取
storeView2.setSourcePath(PathParserUtils.getPathFromStringArray(this, R.array.storehouse, 3));複製代碼
3 簡單的SVG(半成品)
之前從gayHub上找了一個SVG-PATH的轉換類:SvgPathParser,如今派上了用場,簡單的SVG-PATH,能夠,複雜的還有問題,還須要繼續尋找更加方案。
//SVG轉-》path
//還在完善中,我從github上找了以下工具類,發現簡單的SVG能夠轉path,複雜點的 就亂了
/* SvgPathParser svgPathParser = new SvgPathParser(); try { Path path = svgPathParser.parsePath("M1,1 L1,50 L50,50 L50,50 L50,1 Z"); storeView3.setSourcePath(path); } catch (ParseException e) { e.printStackTrace(); }*/複製代碼
普通PathAnimView
效果如圖1 3。動畫是 進度填充直到滿的效果。
<com.mcxtzhang.pathanimlib.PathAnimView
android:id="@+id/pathAnimView1"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@color/blue"
android:padding="5dp"/>複製代碼
高仿StoreHouse風格AnimView:
這種View顯示出來的效果如圖2 4 6 。動畫是 殘影流動的效果。
<com.mcxtzhang.pathanimlib.StoreHouseAnimView
android:id="@+id/storeView3"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@android:color/black"
android:padding="5dp"/>複製代碼
fillView1.startAnim();複製代碼
fillView1.stopAnim();複製代碼
fillView1.clearAnim();複製代碼
看到這裏細心的朋友可能會發現,上一節,我沒有提第5個圖View是怎麼定義的, 並且第五個View的效果,貌似和其餘的不同,仔細看動畫是否是像Android L+的系統自帶進度條ProgressBar的效果?
那說明它的動畫效果和我先前提到的兩種不同,是的,一開始我擼是照着StoreHouse那種效果擼的,這是我次日才擴展的。
高級的用法,就是本控件動畫的擴展性。
你徹底能夠經過繼承PathAnimHelper類
,重寫onPathAnimCallback()
方法,擴展動畫,圖5就是這麼來的。
先講用法預覽,稍後章節會詳解。
用法:
對任意一個普通的PathAnimView,設置一個自定義的PathAnimHelper類便可:
//代碼示例 動態對path加工,經過Helper
pathAnimView1.setPathAnimHelper(new CstSysLoadAnimHelper(pathAnimView1, pathAnimView1.getSourcePath(), pathAnimView1.getAnimPath()));複製代碼
自定義的PathAnimHelper類:
/** * 介紹:自定義的PathAnimHelper,實現相似Android L+ 進度條效果 * 做者:zhangxutong * 郵箱:zhangxutong@imcoming.com * 時間: 2016/11/3. */
public class CstSysLoadAnimHelper extends PathAnimHelper {
public CstSysLoadAnimHelper(View view, Path sourcePath, Path animPath) {
super(view, sourcePath, animPath);
}
public CstSysLoadAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
super(view, sourcePath, animPath, animTime, isInfinite);
}
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//獲取一個段落
float end = pathMeasure.getLength() * value;
float begin = (float) (end - ((0.5 - Math.abs(value - 0.5)) * pathMeasure.getLength()));
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.getSegment(begin, end, animPath, true);
}
}複製代碼
伸手黨看到這裏若是感興趣,就能夠直接一步gayhub了
(github.com/mcxtzhang/P…)
後文比較長,須要自帶耐心觀看。
這裏我簡單畫了一下本文介紹的幾個類的類圖:
對於重要方法和屬性標註了一下。
PathAnimView
繼承自View,是一個自定義View。
PathAnimHelper
,專一作
Path動畫。它默認的實現是
逐漸填充 的動畫效果。
通常狀況下只須要更換PathAnimHelper
,PathAnimView
便可作出不一樣的動畫。(圖1第5個View)
可是若是須要擴充一些動畫屬性供用戶設置,例如仿StoreHouse風格的動畫View,想暴露 殘影長度 屬性供設置。
我這裏採用的是:繼承自PathAnimView
,並增長屬性get、set 方法,並重寫getInitAnimHeper()
方法,返回自定義的PathAnimHelper
。
如StoreHouseAnimView
繼承自PathAnimView
,增長了殘影長度的get、set方法。並重寫getInitAnimHeper()
方法,返回StoreHouseAnimHelper
對象。 StoreHouseAnimHelper
類繼承的是PathAnimHelper
。
基礎類是PathAnimView
和PathAnimHelper
。
先看PathAnimView
:
這裏我將一些不重要的get、set方法和構造方法剔除,留下比較重要的方法。
一個作路徑動畫的View
代碼自己不難,註釋也比較詳細,核心的話,就是onDraw()
方法咯:
我這裏用平移作的paddingLeft、paddingTop。
先利用源Path(mSourcePath)繪製底邊的樣子。
再利用變化的animPath(mAnimPath)繪製前景,這樣animPath不斷變化,而且重繪View->onDraw(),前景就會不斷變化,造成動畫效果。
那麼核心就是animPath的的變化了,animPath的變化交由 mPathAnimHelper去作。
核心源碼以下:
public class PathAnimView extends View {
protected Path mSourcePath;//須要作動畫的源Path
protected Path mAnimPath;//用於繪製動畫的Path
protected Paint mPaint;
protected int mColorBg = Color.GRAY;//背景色
protected int mColorFg = Color.WHITE;//前景色 填充色
protected PathAnimHelper mPathAnimHelper;//Path動畫工具類
protected int mPaddingLeft, mPaddingTop;
public PathAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/** * 這個方法可能會常常用到,用於設置源Path * * @param sourcePath * @return */
public PathAnimView setSourcePath(Path sourcePath) {
mSourcePath = sourcePath;
initAnimHelper();
return this;
}
/** * INIT FUNC **/
protected void init() {
//Paint
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
//動畫路徑只要初始化便可
mAnimPath = new Path();
//初始化動畫幫助類
initAnimHelper();
}
/** * 初始化動畫幫助類 */
protected void initAnimHelper() {
mPathAnimHelper = getInitAnimHeper();
//mPathAnimHelper = new PathAnimHelper(this, mSourcePath, mAnimPath, 1500, true);
}
/** * 子類可經過重寫這個方法,返回自定義的AnimHelper * * @return */
protected PathAnimHelper getInitAnimHeper() {
return new PathAnimHelper(this, mSourcePath, mAnimPath);
}
/** * draw FUNC **/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//平移
canvas.translate(mPaddingLeft, mPaddingTop);
//先繪製底,
mPaint.setColor(mColorBg);
canvas.drawPath(mSourcePath, mPaint);
//再繪製前景,mAnimPath不斷變化,不斷重繪View的話,就會有動畫效果。
mPaint.setColor(mColorFg);
canvas.drawPath(mAnimPath, mPaint);
}
/** * 設置動畫 循環 */
public PathAnimView setAnimInfinite(boolean infinite) {
mPathAnimHelper.setInfinite(infinite);
return this;
}
/** * 設置動畫 總時長 */
public PathAnimView setAnimTime(long animTime) {
mPathAnimHelper.setAnimTime(animTime);
return this;
}
/** * 執行循環動畫 */
public void startAnim() {
mPathAnimHelper.startAnim();
}
/** * 中止動畫 */
public void stopAnim() {
mPathAnimHelper.stopAnim();
}
/** * 清除並中止動畫 */
public void clearAnim() {
stopAnim();
mAnimPath.reset();
mAnimPath.lineTo(0, 0);
invalidate();
}
}複製代碼
看看最基礎的PathAnimHelper
類是怎麼作的,同樣省略一些代碼:
它是一個PathAnimView的Path動畫的工具類
值得一提的是,這裏的動畫時間,是指循環取出SourcePath裏的N段Path的總時間。
startAnim()
方法是入口,這個方法會在PathAnimView裏被調用。
在startAnim()
方法裏,先初始化一個PathMeasure
,以及重置animPath
。
而後利用PathMeasure.nextContour()
方法,循環一遍SourcePath的Path段數count,
利用這個count求出每段小Path應該執行的動畫時間:totalDuaration / count
。
而後便調用loopAnim()
方法,循環取出每一段path ,並執行動畫。
loopAnim()
方法裏,定義一個無限循環的屬性動畫mAnimator
,
爲其設置AnimatorUpdateListener
和onAnimationRepeat
,監聽動畫的更新和重複。
重點就在這兩個監聽器裏:
public void onAnimationUpdate(ValueAnimator animation) {
//增長一個callback 便於子類重寫搞事情
onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//通知View刷新本身
view.invalidate();
}複製代碼
動畫每次Update的時候,回調onPathAnimCallback()
方法,在裏面對animPath作處理。
對AnimPath處理之後,就可讓View繪製新animPath造成動畫了:
而後就是讓View重繪,這樣就會重走onDraw()方法,就是上一節提到的內容。
onPathAnimCallback()
方法也很簡單,按動畫進度值,取出當前這一小段的path的部分路徑,賦值給animPath。
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//獲取一個段落
pathMeasure.getSegment(0, pathMeasure.getLength() * value, animPath, true);
}複製代碼
在Repeat監聽器裏:
public void onAnimationRepeat(Animator animation) {
//繪製完一條Path以後,再繪製下一條
pathMeasure.nextContour();
//長度爲0 說明一次循環結束
if (pathMeasure.getLength() == 0) {
if (isInfinite) {//若是須要循環動畫
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
} else {//不須要就中止(由於repeat是無限 須要手動中止)
animation.end();
}
}
}複製代碼
由於SourcePath裏是可能含有1+段Path的,這裏是合適的時機,利用pathMeasure.nextContour();
循環取出下一段Path, 判斷一下新Path的長度,若是爲0,說明這一次大循環結束,即用戶視覺上的一次動畫進度100%了。
這裏判斷咱們設置的isInfinite
屬性,
若是是true,說明是循環動畫,那麼作初始化工做:
清空咱們的animPath,初始化pathMeasure。(和startAnim()
方法裏的初始化工做一致)。
若是是false,說明動畫須要中止,那麼手動調用animation.end()
中止動畫。(圖1,第三個動畫)
核心源碼以下:
public class PathAnimHelper {
protected static final long mDefaultAnimTime = 1500;//默認動畫總時間
protected View mView;//執行動畫的View
protected Path mSourcePath;//源Path
protected Path mAnimPath;//用於繪製動畫的Path
protected long mAnimTime;//動畫一共的時間
protected boolean mIsInfinite;//是否無限循環
protected ValueAnimator mAnimator;//動畫對象
public PathAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
if (view == null || sourcePath == null || animPath == null) {
Log.e(TAG, "PathAnimHelper init error: view 、sourcePath、animPath can not be null");
return;
}
mView = view;
mSourcePath = sourcePath;
mAnimPath = animPath;
mAnimTime = animTime;
mIsInfinite = isInfinite;
}
/** * 執行動畫 */
public void startAnim() {
startAnim(mView, mSourcePath, mAnimPath, mAnimTime, mIsInfinite);
}
/** * 一個SourcePath 內含多段Path,循環取出每段Path,並作一個動畫 * 自定義動畫的總時間 * 和是否循環 * * @param view 須要作動畫的自定義View * @param sourcePath 源Path * @param animPath 自定義View用這個Path作動畫 * @param totalDuaration 動畫一共的時間 * @param isInfinite 是否無限循環 */
protected void startAnim(View view, Path sourcePath, Path animPath, long totalDuaration, boolean isInfinite) {
if (view == null || sourcePath == null || animPath == null) {
return;
}
PathMeasure pathMeasure = new PathMeasure();
//先重置一下須要顯示動畫的path
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
//這裏僅僅是爲了 計算一下每一段的duration
int count = 0;
while (pathMeasure.getLength() != 0) {
pathMeasure.nextContour();
count++;
}
//通過上面這段計算duration代碼的折騰 須要從新初始化pathMeasure
pathMeasure.setPath(sourcePath, false);
loopAnim(view, sourcePath, animPath, totalDuaration, pathMeasure, totalDuaration / count, isInfinite);
}
/** * 循環取出每一段path ,並執行動畫 * * @param animPath 自定義View用這個Path作動畫 * @param pathMeasure 用於測量的PathMeasure */
protected void loopAnim(final View view, final Path sourcePath, final Path animPath, final long totalDuaration, final PathMeasure pathMeasure, final long duration, final boolean isInfinite) {
//動畫正在運行的話,先stop吧。萬一有人要使用新動畫呢,(正經用戶不會這麼用。)
stopAnim();
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setDuration(duration);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//增長一個callback 便於子類重寫搞事情
onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//通知View刷新本身
view.invalidate();
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animation) {
//每段path走完後,要補一下 某些狀況會出現 animPath不滿的狀況
pathMeasure.getSegment(0, pathMeasure.getLength(), animPath, true);
//繪製完一條Path以後,再繪製下一條
pathMeasure.nextContour();
//長度爲0 說明一次循環結束
if (pathMeasure.getLength() == 0) {
if (isInfinite) {//若是須要循環動畫
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
} else {//不須要就中止(由於repeat是無限 須要手動中止)
animation.end();
}
}
}
});
mAnimator.start();
}
/** * 中止動畫 */
public void stopAnim() {
if (null != mAnimator && mAnimator.isRunning()) {
mAnimator.end();
}
}
/** * 用於子類繼承搞事情,對animPath進行再次操做的函數 * * @param view * @param sourcePath * @param animPath * @param pathMeasure */
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//獲取一個段落
pathMeasure.getSegment(0, pathMeasure.getLength() * value, animPath, true);
}
}複製代碼
至此兩個最基礎的類就講完了,如此簡單就可實現圖1第一、3個動畫效果。
咱們前面提過,擴展動畫,核心是繼承PathAnimHelper 重寫onPathAnimCallback()
方法便可,因此實現StoreHouse風格,核心類就是StoreHouseAnimHelper
。
核心代碼以下:
public class StoreHouseAnimHelper extends PathAnimHelper {
private final static long MAX_LENGTH = 400;
private long mPathMaxLength;//殘影路徑最大長度
Path mStonePath;//暫存一下路徑,最終要複製給animPath的
PathMeasure mPm;
private ArrayList<Float> mPathLengthArray;//路徑長度array
private SparseArray<Boolean> mPathNeedAddArray;//路徑是否須要被所有Add的Array
private int partIndex;//殘缺的index
private float partLength;//殘缺部分的長度
public StoreHouseAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
super(view, sourcePath, animPath, animTime, isInfinite);
mPathMaxLength = MAX_LENGTH;
mStonePath = new Path();
mPm = new PathMeasure();
mPathLengthArray = new ArrayList<>();//順序存放path的length
mPathNeedAddArray = new SparseArray<>();
}
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
super.onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//仿StoneHouse效果 ,如今的作法很挫
//重置變量
mStonePath.reset();
mStonePath.lineTo(0, 0);
mPathLengthArray.clear();
//循環一遍AnimPath,記錄裏面每一段小Path的length。
mPm.setPath(animPath, false);
while (mPm.getLength() != 0) {
mPathLengthArray.add(mPm.getLength());
mPm.nextContour();
}
//逆序遍歷AnimPath,記錄哪些子Path是須要add的,而且記錄那段須要部分add的path的下標
mPathNeedAddArray.clear();
float totalLength = 0;
partIndex = 0;
partLength = 0;
for (int i = mPathLengthArray.size() - 1; i >= 0; i--) {
if (totalLength + mPathLengthArray.get(i) <= mPathMaxLength) {//加上了也沒滿
mPathNeedAddArray.put(i, true);
totalLength = totalLength + mPathLengthArray.get(i);
} else if (totalLength < mPathMaxLength) {//加上了滿了,可是不加就沒滿
partIndex = i;
partLength = mPathMaxLength - totalLength;
totalLength = totalLength + mPathLengthArray.get(i);
}
}
//循環Path,並獲得最終要顯示的AnimPath
mPm.setPath(animPath, false);
int i = 0;
while (mPm.getLength() != 0) {
if (mPathNeedAddArray.get(i, false)) {
mPm.getSegment(0, mPm.getLength(), mStonePath, true);
} else if (i == partIndex) {
mPm.getSegment(mPm.getLength() - partLength, mPm.getLength(), mStonePath, true);
}
mPm.nextContour();
i++;
}
animPath.set(mStonePath);
}
}複製代碼
直接上碼了,得益於咱們的設計,很簡單:
重寫getInitAnimHeper()
返回咱們的StoreHouseAnimHelper
,並增長殘影長度的get、set方法。
public class StoreHouseAnimView extends PathAnimView {
public StoreHouseAnimView(Context context) {
this(context, null);
}
public StoreHouseAnimView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StoreHouseAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/** * GET SET FUNC **/
public long getPathMaxLength() {
return ((StoreHouseAnimHelper) mPathAnimHelper).getPathMaxLength();
}
/** * 設置殘影最大長度 * * @param pathMaxLength * @return */
public StoreHouseAnimView setPathMaxLength(long pathMaxLength) {
((StoreHouseAnimHelper) mPathAnimHelper).setPathMaxLength(pathMaxLength);
return this;
}
@Override
protected PathAnimHelper getInitAnimHeper() {
return new StoreHouseAnimHelper(this, mSourcePath, mAnimPath);
}
}複製代碼
前面提過,如圖1第五個動畫的效果,就是後期我加入擴展的,分析一下這種效果,它和普通的PathAnimView
的效果只有動畫不一樣,也不須要額外引入屬性暴露出去供設置,因此這種場景,咱們只須要重寫一個PathAnimHelper
類,set給PathAnimView
便可。
代碼第一章節也提過,
一點注意的地方就是,這裏沒有同第四章節那樣調用super.onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
。
由於第四章仿StoreHouse的效果,是在第三章的效果基礎之上加工而成的。因此須要PathAnimHeper
先處理一下。
而咱們這裏實現的仿系統ProgressBar的效果,則是徹底重寫的。
核心方法以下重寫,很簡單,再也不贅述:
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//獲取一個段落
float end = pathMeasure.getLength() * value;
float begin = (float) (end - ((0.5 - Math.abs(value - 0.5)) * pathMeasure.getLength()));
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.getSegment(begin, end, animPath, true);
}複製代碼
總結起來就是 I have a path.I have a view. (Oh~),Path(Anim)View.
利用這條褲子,只要傳一個Path進去,就能夠實現多姿多彩的酷炫Path動畫,若是對動畫不滿意,還能夠本身動態擴展。
目前最急需完善的:
SVG->Android PATH的轉換,
但願有知道的兄弟能夠告知一下,多謝。
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/P…
StoreHouse風格點陣資源引用處,也是我第一次看見StoreHouse動畫:百萬大神的庫: github.com/liaohuqiu/a…
一開始我看到這種動畫,我仍是個小菜雞,也不知道怎麼實現的,可是一直在我腦海裏揮之不去。
後來忽然有一天想到能夠用Path、PathMeasure作自定義View動畫來實現,就開始動筆寫了起來。
發現Path的路徑不太好獲取,因而翻看百萬大神的庫,發現他並非使用的Path動畫,可是的的確確是利用點陣設置數據源的,因而我就藉助了這些最原始的點陣資源,擼出了這麼一個Path動畫。
最初只是想實現這麼一個效果,了卻個人心願,沒想到還有意外收穫。有了這個Path動畫工具類庫。說實話寫這麼一個東西,我不知不覺也提高了,之前可能不太會把層級分的這麼開,利用繼承組合的方式去擴展功能。之前大多仍是C V 一份代碼改一改,像圖上的效果,我可能會分開自定義三個View去作,複製一些重複代碼也不在意,看來堅持會有收穫。但願咱們都一塊兒進步。