【Android開源專案解析】仿支付寶付款成功及"天女散花"效果實現——看PathMeasure大展身手...
話說,在前面兩篇文章中,我們學習了BitmapShader、Path的基本使用,那麼這一篇文章,我們們接著來學習一下PathMeasure的用法。什麼,你沒聽說過PathMeasure?那你就要OUT咯~
專案效果圖
廢話不多說,在開始講解之前,先看下最終實現的效果。
效果一:
仿支付寶支付成功效果
效果二:
這兩個專案都是使用Path和PathMeature配合完成的,由其他專案改造而來
專案一是七叔寫的,我對程式碼進行了大量改造。
專案二是不小心搜到的,然後進行了改造,原文請戳這裡
本文程式碼請到這裡下載
PathMeasure介紹
PathMeasure這個類確實是不太常見的,關於這個類的介紹也是甚少,那麼這個類是用來幹嘛的呢?主要其實是配合Path,來計算Path裡面點的座標的,或者是給一個範圍,來擷取Path其中的一部分的。
這麼說,你肯定也迷糊,我們們先簡單看一下有哪些方法,然後根據案例來進行講解更好一些。
構造方法有兩個,很好理解,不多解釋。
PathMeasure()
PathMeasure(Path path, boolean forceClosed)
重點看下常用方法:
- float getLength() 返回當前contour(解釋為輪廓不太恰當,我覺得更像是筆畫)的長度,也就是這一個Path有多長
- boolean getPosTan(float distance, float[] pos, float[] tan) 傳入一個距離distance(0<=distance<=getLength()),然後會計算當前距離的座標點和切線,注意,pos會自動填充上座標,這個方法很重要
- boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 傳入一個開始和結束距離,然後會返回介於這之間的Path,在這裡就是dst,他會被填充上內容,這個方法很重要
- boolean nextContour() 移動到下一個筆畫,如果你的Path是由多個筆畫組成的話,那麼就可以使用這個方法
- void setPath(Path path, boolean forceClosed)這個方法也比較重要,用來設定新的Path物件的,算是對第一個建構函式的一個補充
仿支付寶實現原理解析
下面,我將介紹一下如何實現下面的這個效果
首先分析需求:
- 需要有三種狀態:載入中,成功,失敗
- 載入中時,需要不斷更換顏色
- 載入中狀態時,圓弧要不斷的變換長度和位置
- 成功狀態和失敗狀態,需要把√和×一筆一劃的畫出來
OK,基本就是這些需求,那麼對應著需求,我們們看一下解決方案
- 有三種狀態好說,用靜態常量或者是列舉型別進行區分
- 不斷變換顏色也好說,只要改變Paint的顏色就可以啦
- 不斷的變化長度和位置,從效果圖上可以看出來,我們需要畫一段圓弧,那就要用下面的drawArc(),需要知道範圍,起始角度和繪製角度,由於需要不斷的變化長度,因此就需要用Animator,具體實現一會詳談
Canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint)
- 需要畫出來形狀,其實就是一些線段,那麼就需要用Path了,但是如何能一筆一劃的效果呢?那就要靠PathMeasure啦
下面開始講解程式碼實現,最好參照著原始碼看下面的文章。
首先看怎麼用ConfirmView呢?很簡單,只需要呼叫animatedWithState()然後傳入一個列舉型別即可
confirmView.animatedWithState(ConfirmView.State.Progressing);
這個列舉型別在類的內部,代表三種狀態
public enum State {
Success, Fail, Progressing
}
再看建構函式,很簡單,只是進行了變數的初始化,這些變數的具體作用,我將在下面用到的時候重點介紹
public ConfirmView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mSuccessPath = new Path();
mPathMeasure = new PathMeasure(mSuccessPath, false);
mRenderPaths = new ArrayList<>();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xFF0099CC);
mPaint.setStrokeWidth(STROKEN_WIDTH);
mPaint.setStrokeCap(Paint.Cap.ROUND);
oval = new RectF();
}
那麼呼叫了animatedWithState()之後,進行了什麼操作呢?
public void animatedWithState(State state) {
if (mCurrentState != state) {
mCurrentState = state;
if (mPhareAnimator != null && mPhareAnimator.isRunning()) {
stopPhareAnimation();
}
switch (state) {
case Fail:
case Success:
updatePath();
if (mCircleAnimator != null && mCircleAnimator.isRunning()) {
mCircleAngle = (Float) mCircleAnimator.getAnimatedValue();
mCircleAnimator.end();
}
if ((mStartAngleAnimator == null || !mStartAngleAnimator.isRunning() || !mStartAngleAnimator.isStarted()) &&
(mEndAngleAnimator == null || !mEndAngleAnimator.isRunning() || !mEndAngleAnimator.isStarted())) {
mStartAngle = 360;
mEndAngle = 0;
startPhareAnimation();
}
break;
case Progressing:
mCircleAngle = 0;
startCircleAnimation();
break;
}
}
}
結合著上面的程式碼,我簡單解釋一下。
首先進行重複性的判斷,如果當前所處的狀態與要改變的狀態相同則不進行操作。
接下來,對動畫狀態進行了判斷,mPhareAnimator是用來實現√和×的動畫繪製效果的,如果正在執行,則停掉。
再往下的一個switch則是開始真正的操作了,updatePath()是更新Path,一會重點看下,mCircleAnimator這個則是實現外部弧形的偏移量的控制的,現在看不明白也沒事,重點看下下面的程式碼,當mStartAngleAnimator和mEndAngleAnimator都不在執行狀態的時候(這兩個Animator是為了控制外部弧形的起點和終點的),會進入下面的程式碼,
mStartAngle = 360;
mEndAngle = 0;
startPhareAnimation();
mStartAngle和mEndAngle分別代表起點轉過的角度和終點轉過的角度,然後就startPhareAnimation(),這個時候,真正的繪製√和×的動畫才開始執行。
如果是Progressing呢,則執行下面的程式碼,重置mCircleAngle,startCircleAnimation()這個方法是繪製外部的弧形的動畫
mCircleAngle = 0;
startCircleAnimation();
至此,我們們知道了傳入不同狀態的列舉型別會進行什麼操作,下面,開始看真正的操作。
我們先看一個簡單的,就是startCircleAnimation()到底做了什麼。
前面說過,這個方法是為了繪製載入中狀態時,外部不斷變化的彩色弧形的,下面是程式碼實現
public void startCircleAnimation() {
if (mCircleAnimator == null || mStartAngleAnimator == null || mEndAngleAnimator == null) {
initAngleAnimation();
}
mStartAngleAnimator.setDuration(NORMAL_ANGLE_ANIMATION_DURATION);
mEndAngleAnimator.setDuration(NORMAL_ANGLE_ANIMATION_DURATION);
mCircleAnimator.setDuration(NORMAL_CIRCLE_ANIMATION_DURATION);
mStartAngleAnimator.start();
mEndAngleAnimator.start();
mCircleAnimator.start();
}
首先前面的if語句是為空判斷,從而進行初始化的操作,後面則是簡單的設定動畫的持續時間和開啟動畫。這裡一共出現了三個動畫,完成外部弧形的效果控制
- mStartAngleAnimator 控制圓弧起點
- mEndAngleAnimator 控制圓弧終點
- mCircleAnimator 控制圓弧的整體偏移量
這麼說,你可能還是不很明白,沒關係,我們們一點點的看程式碼,首先,我們們看在初始化的時候,到底做了什麼操作,也就是initAngleAnimation()。
private void initAngleAnimation() {
mStartAngleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
mEndAngleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
mCircleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
mStartAngleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
setStartAngle(value);
}
});
mEndAngleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
setEndAngle(value);
}
});
mStartAngleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
if (mCurrentState == State.Progressing) {
if (mEndAngleAnimator != null) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mEndAngleAnimator.start();
}
}, 400L);
}
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (mCurrentState != State.Progressing && mEndAngleAnimator != null && !mEndAngleAnimator.isRunning() && !mEndAngleAnimator.isStarted()) {
startPhareAnimation();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mEndAngleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mStartAngleAnimator != null) {
if (mCurrentState != State.Progressing) {
mStartAngleAnimator.setDuration(NORMAL_ANIMATION_DURATION);
}
colorCursor++;
if (colorCursor >= colors.length) colorCursor = 0;
mPaint.setColor(colors[colorCursor]);
mStartAngleAnimator.start();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mCircleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
setCircleAngle(value);
}
});
mStartAngleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mEndAngleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mCircleAnimator.setInterpolator(new LinearInterpolator());
mCircleAnimator.setRepeatCount(-1);
}
這段程式碼雖然長,但是也沒有太大的難度,無非就是進行了初始化操作,ValueAnimator的範圍是0-1,這個在後面將用於計算角度。在值不斷的更新的過程中,分別呼叫了下面這三個方法,更新一些值
setStartAngle(value);
setEndAngle(value);
setCircleAngle(value);
在這三個方法裡面,都對成員變數進行了更新,並且!呼叫了invalidate()!看到這裡是不是激動了,改變一次就重繪一次,這三個值肯定和弧形的動畫效果有關啊!
private void setStartAngle(float startAngle) {
this.mStartAngle = startAngle;
invalidate();
}
private void setEndAngle(float endAngle) {
this.mEndAngle = endAngle;
invalidate();
}
private void setCircleAngle(float circleAngle) {
this.mCircleAngle = circleAngle;
invalidate();
}
我們知道了這個,先不著急去看onDraw(),仔細看下動畫的執行順序。
在mStartAngleAnimator執行之後,呼叫了下面的方法,這當然很簡單,就是說,mStartAngleAnimator執行了400毫秒之後,mEndAngleAnimator才會執行,而且插值器設定的是AccelerateDecelerateInterpolator,為啥呢?很簡單,因為只有這樣,才能做出弧形長度先長後短的效果呀~
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mEndAngleAnimator.start();
}
}, 400L);
而在mEndAngleAnimator執行結束之後,會呼叫下面的程式碼
if (mStartAngleAnimator != null) {
if (mCurrentState != State.Progressing) {
mStartAngleAnimator.setDuration(NORMAL_ANIMATION_DURATION);
}
colorCursor++;
if (colorCursor >= colors.length) colorCursor = 0;
mPaint.setColor(colors[colorCursor]);
mStartAngleAnimator.start();
}
在這個設定mStartAngleAnimator的動畫時間,是為了畫√或者是×的時候快一些效果更流暢。下面的程式碼很簡單了吧,改變畫筆顏色,然後mStartAngleAnimator又開啟啦!這就是為啥一直轉啊轉的原因。
但是說到這裡,我們們還沒看onDraw()做了什麼呢!
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (mCurrentState) {
case Fail:
for (int i = 0; i < PATH_SIZE_TWO; i++) {
Path p = mRenderPaths.get(i);
if (p != null) {
canvas.drawPath(p, mPaint);
}
}
drawCircle(canvas);
break;
case Success:
Path p = mRenderPaths.get(0);
if (p != null) {
canvas.drawPath(p, mPaint);
}
drawCircle(canvas);
break;
case Progressing:
drawCircle(canvas);
break;
}
}
我們先看Progressing分支裡面的drawCircle(canvas),其他的先不要管
private void drawCircle(Canvas canvas) {
float offsetAngle = mCircleAngle * 360;
float startAngle = mEndAngle * 360;
float sweepAngle = mStartAngle * 360;
if (startAngle == 360)
startAngle = 0;
sweepAngle = sweepAngle - startAngle;
startAngle += offsetAngle;
if (sweepAngle < 0)
sweepAngle = 1;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
}
是的,上面這段程式碼就是繪製不斷變幻的環的程式碼咯
float startAngle = mEndAngle * 360;是計算終點的位置,有人會感到奇怪,為啥終點的位置叫startAngle啊!因為終點的位置就是開始繪製的位置,所以不要奇怪了。
sweepAngle = sweepAngle - startAngle;則是計算要畫多少角度的弧線,因為起點先跑到前面的,所以減去終點的位置,就是旋轉角度。
startAngle += offsetAngle;那麼這句是幹嘛的?這個就是所謂的偏移量,為了要實現更隨性的從非固定點開始結束的效果。沒聽懂?我給你去掉你看下效果!
private void drawCircle(Canvas canvas) {
float offsetAngle = mCircleAngle * 360;
float startAngle = mEndAngle * 360;
float sweepAngle = mStartAngle * 360;
if (startAngle == 360)
startAngle = 0;
sweepAngle = sweepAngle - startAngle;
// startAngle += offsetAngle;
if (sweepAngle < 0)
sweepAngle = 1;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
}
這下子明白了吧,去掉漂移量效果就沒有之前那麼隨性了~
ok,關於弧線的問題就說這麼多,下面就要說我們們今天的主角PathMeasure了。
在前面的程式碼中,我們提到,成功和失敗狀態會執行updatePath()和startPhareAnimation(),那麼到底做了些什麼呢?
private void updatePath() {
int offset = (int) (mSignRadius * 0.15F);
mRenderPaths.clear();
switch (mCurrentState) {
case Success:
mSuccessPath.reset();
mSuccessPath.moveTo(mCenterX - mSignRadius, mCenterY + offset);
mSuccessPath.lineTo(mCenterX - offset, mCenterY + mSignRadius - offset);
mSuccessPath.lineTo(mCenterX + mSignRadius, mCenterY - mSignRadius + offset);
mRenderPaths.add(new Path());
break;
case Fail:
mSuccessPath.reset();
float failRadius = mSignRadius * 0.8F;
mSuccessPath.moveTo(mCenterX - failRadius, mCenterY - failRadius);
mSuccessPath.lineTo(mCenterX + failRadius, mCenterY + failRadius);
mSuccessPath.moveTo(mCenterX + failRadius, mCenterY - failRadius);
mSuccessPath.lineTo(mCenterX - failRadius, mCenterY + failRadius);
for (int i = 0; i < PATH_SIZE_TWO; i++) {
mRenderPaths.add(new Path());
}
break;
default:
mSuccessPath.reset();
}
mPathMeasure.setPath(mSuccessPath, false);
}
在updatePath()我們可以很清楚的看到,在這裡初始化了mSuccessPath,通過moveTo()和lineTo()�首先勾勒除了√和×的形狀,至於這個座標是怎麼確定的,這個可以自己想法來,我就不介紹了。還要需要注意的是,Success中最後在mRenderPaths中新增了一個Path物件,而在Fail則新增了兩個物件,這個其實是和要繪製的圖形的筆畫數有關的,×是兩筆,所以是兩個,這裡新增的Path議會將用來紀錄每一筆畫的形狀。
最後,我們們的主角終於現身了
mPathMeasure.setPath(mSuccessPath, false);
呼叫完這個方法,會馬上呼叫下面的方法
public void startPhareAnimation() {
if (mPhareAnimator == null) {
mPhareAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
mPhareAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
setPhare(value);
}
});
mPhareAnimator.setDuration(NORMAL_ANIMATION_DURATION);
mPhareAnimator.setInterpolator(new LinearInterpolator());
}
mPhare = 0;
mPhareAnimator.start();
}
其實也很簡單,初始化了mPhareAnimator,然後開啟動畫,不斷呼叫setPhare(value),
private void setPhare(float phare) {
mPhare = phare;
updatePhare();
invalidate();
}
在這裡updatePhare(),然後重繪介面,那麼玄機應該都在updatePhare()了吧!
private void updatePhare() {
if (mSuccessPath != null) {
switch (mCurrentState) {
case Success: {
if (mPathMeasure.getSegment(0, mPhare * mPathMeasure.getLength(), mRenderPaths.get(0), true)) {
mRenderPaths.get(0).rLineTo(0, 0);
}
}
break;
case Fail: {
//i = 0,畫一半,i=1,畫另一半
float seg = 1.0F / PATH_SIZE_TWO;
for (int i = 0; i < PATH_SIZE_TWO; i++) {
float offset = mPhare - seg * i;
offset = offset < 0 ? 0 : offset;
offset *= PATH_SIZE_TWO;
Log.d("i:" + i + ",seg:" + seg, "offset:" + offset + ", mPhare:" + mPhare + ", size:" + PATH_SIZE_TWO);
boolean success = mPathMeasure.getSegment(0, offset * mPathMeasure.getLength(), mRenderPaths.get(i), true);
if (success) {
mRenderPaths.get(i).rLineTo(0, 0);
}
mPathMeasure.nextContour();
}
mPathMeasure.setPath(mSuccessPath, false);
}
break;
}
}
}
在這裡,一個很重要的方法呼叫了,那就是mPathMeasure.getSegment()
當Success的時候,會執行下面的程式碼。mPhare就是動畫的百分比,從0到1,那麼,下面的這段程式碼就很好理解了,這是為了根據動畫的百分比,獲取畫出√的整個Path的一部分,然後把這部分,填充到了mRenderPaths.get(0)裡面,這裡面存放的就是在上面方法中新增進去的一個Path物件。mPhare不斷的變化,我們就能獲取到畫整個√形狀所需的所有Path物件,還記得這個方法之後是什麼嗎?invalidate()!所以,現在在onDraw()裡面肯定用這Path物件,畫出√的一部分,不斷的更新從mPhare,不斷繪製,從無到有,而出現了動畫效果。
mRenderPaths.get(0).rLineTo(0, 0);這個程式碼則是為了在4.4以下不能繪製出圖形BUG的解決方法,不要在意。
if (mPathMeasure.getSegment(0, mPhare * mPathMeasure.getLength(), mRenderPaths.get(0), true)) {
mRenderPaths.get(0).rLineTo(0, 0);
}
不信我們們看下onDraw(),是不是!那麼現在你應該知道×是怎麼畫出來的吧?
case Success:
Path p = mRenderPaths.get(0);
if (p != null) {
canvas.drawPath(p, mPaint);
}
drawCircle(canvas);
break;
來來來,我們們看下程式碼!
case Fail: {
//i = 0,畫一半,i=1,畫另一半
float seg = 1.0F / PATH_SIZE_TWO;
for (int i = 0; i < PATH_SIZE_TWO; i++) {
float offset = mPhare - seg * i;
offset = offset < 0 ? 0 : offset;
offset *= PATH_SIZE_TWO;
Log.d("i:" + i + ",seg:" + seg, "offset:" + offset + ", mPhare:" + mPhare + ", size:" + PATH_SIZE_TWO);
boolean success = mPathMeasure.getSegment(0, offset * mPathMeasure.getLength(), mRenderPaths.get(i), true);
if (success) {
mRenderPaths.get(i).rLineTo(0, 0);
}
mPathMeasure.nextContour();
}
mPathMeasure.setPath(mSuccessPath, false);
}
break;
與繪製√相比,因為×是兩筆,所以有些小複雜,但是也不難,offset *= PATH_SIZE_TWO;是為了保證在mPhare從0-0.5過程中控制第一筆畫,0.5-1則控制第二條筆畫,你仔細看下程式碼,這樣可以實現offset從0-1兩次。由於×是兩筆畫,所以在i=0取到第一筆畫的Path部分,儲存在mRenderPaths的第一個Path之後,呼叫了mPathMeasure.nextContour();切換到下一筆畫,再次完成相同的操作。
而由於PathMeasure只能往下找Contour,所以最後 mPathMeasure.setPath(mSuccessPath, false);回覆到最初狀態,然後我們看下onDraw()
for (int i = 0; i < PATH_SIZE_TWO; i++) {
Path p = mRenderPaths.get(i);
if (p != null) {
canvas.drawPath(p, mPaint);
}
}
drawCircle(canvas);
其實和Success差不多的,只不過是兩個Path,畫出兩筆。
OK,到這裡,這個效果就算是全部實現了,累死我了
"天女散花"實現效果解析
其實這個我並不打算詳細講,因為一通百通,多說無益,更多的東西需要你自己研究程式碼吸收,我們們就重點看下PathMeasure的用法。
其實這種效果實現的真相是這樣滴
YES!就是一些Bitmap物件沿著Path路徑移動!
那麼和PathMeasure有啥關係呢?
看下onDraw()!
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFllower(canvas, fllowers1);
drawFllower(canvas, fllowers2);
drawFllower(canvas, fllowers3);
}
OK,再看下drawFllower()
private void drawFllower(Canvas canvas, List<Fllower> fllowers) {
for (Fllower fllower : fllowers) {
float[] pos = new float[2];
canvas.drawPath(fllower.getPath(), mPaint);
pathMeasure.setPath(fllower.getPath(), false);
pathMeasure.getPosTan(height * fllower.getValue(), pos, null);
canvas.drawBitmap(mBitmap, pos[0], pos[1] - top, null);
}
}
首先,遍歷一個Fllower集合,然後把每個Fllower所屬的Path畫出來,就是上面藍色的曲線,然後很眼熟了吧,給PathMeasure設定Path物件,然後呢,就是重點啦!height是螢幕的高度,fllower.getValue()也是一個百分比,從0-1,和前面的Animator作用相同,這句程式碼就是說,我要距離為height * fllower.getValue()處的點的座標,給我放在pos裡面!
好了,點的座標都有了,剩下的還需要說麼...
不行了,再不回家,就真回不去了,拜拜,同學們
更多參考資料
尊重原創,轉載請註明:From 凱子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵權必究!
相關文章
- ReactNative仿支付寶付款密碼輸入框React密碼
- Android使用Path仿支付寶支付成功失敗動畫Android動畫
- 【Android】開源專案UniversalImageLoader及開源框架ImageLoaderAndroid框架
- Android開源mvp專案,實現玩Android客戶端AndroidMVP客戶端
- vue全家桶 仿小紅書開源專案Vue
- Android 開源專案原始碼解析 -->PhotoView 原始碼解析(七)Android原始碼View
- 專案常用效果!Flutter仿頭條頂部tab切換實現!Flutter
- Android 開源專案原始碼解析 -->Volley 原始碼解析(十五)Android原始碼
- Android 開源專案原始碼解析 -->Dagger 原始碼解析(十三)Android原始碼
- Android 開源專案Android
- android開源專案Android
- android開源專案【專案篇】Android
- 專案需求討論-仿ios底部彈框實現及分析iOS
- Android 開源專案原始碼解析 -->CircularFloatingActionMenu 原始碼解析(八)Android原始碼
- Alipay (支付寶支付) 如何實現一個專案配置多個商戶資訊付款給對應商戶
- android開源專案集合Android
- 看JDon論壇隨感一篇——中國成功的開源專案在哪裡?
- 使用Vitamio開源專案在Android實現播放網路視訊Android
- springboot+vue前後端分離專案-專案搭建13-支付寶付款Spring BootVue後端
- 預研canvas實現Excel開源專案CanvasExcel
- 設計模式Java實現開源專案設計模式Java
- Android實現仿360手機衛士懸浮窗效果Android
- Android 實現GridView的橫向滾動,實現仿京東秒殺效果AndroidView
- Android專案實戰之高仿網易雲音樂專案介紹Android
- android開源專案和框架Android框架
- Android開源專案彙總Android
- android開源專案【developer篇】AndroidDeveloper
- Android專案篇(二):開源庫及工具的封裝Android封裝
- 白嫖GitHub Action實現開源專案CICDGithub
- 神了!兩個開源的高仿外賣專案!
- Android專案實戰之高仿網易雲音樂建立專案和配置Android
- 【Android】Android開源專案精選(一)Android
- 最新Android開源庫、工具、開源專案整理分享Android
- Android 開源專案原始碼解析 -->Android Universal Image Loader 原始碼分析(十四)Android原始碼
- Android開源專案第四篇:開發及測試工具篇Android
- Android實時監控專案第一篇:專案分析及AVD模擬效果圖Android
- Android開源專案釋出jCenterAndroid
- Android開源專案庫彙總Android