編輯:關於Android編程
前一段時間,跟一個項目,忙的焦頭爛額,所以博客斷更了。最近處於學習階段,細細總結了上個項目的不足之處,最突出的一點就是UI做的很中規中矩,Android版本已經來到6.0了,在UI style上面已經可以與IOS界面相比較了,我們的界面怎麼只能滿足於用基本的控件和基本的動畫類來渲染呢?老版本的補間動畫(Tween Animation)和逐幀動畫(Frame Animation orDrawable Animation )已經滿足不了日益增長的用戶體驗了,3.0以後引進了屬性動畫,通過它,我們可以演繹出很多炫目的動態效果,不過也僅僅局限在API層,其實我們可以自己通過一定的算法、高等數學配合圖形學,做出一些更加賞心悅目的動畫效果。實際上,Android還可以使用SVG矢量圖打造酷炫動效。
1.SVG 可被非常多的工具讀取和修改(比如記事本),由於使用xml格式定義,所以可以直接被當作文本文件打開,看裡面的數據;
2.SVG 與 JPEG 和 GIF 圖像比起來,尺寸更小,且可壓縮性更強,SVG 圖就相當於保存了關鍵的數據點,比如要顯示一個圓,需要知道圓心和半徑,那麼SVG 就只保存圓心坐標和半徑數據,而平常我們用的位圖都是以像素點的形式根據圖片大小保存對應個數的像素點,因而SVG尺寸更小;
3.SVG 是可伸縮的,平常使用的位圖拉伸會發虛,壓縮會變形,而SVG格式圖片保存數據進行運算展示,不管多大多少,可以不失真顯示;
4.SVG 圖像可在任何的分辨率下被高質量地打印;
5.SVG 可在圖像質量不下降的情況下被放大;
6.SVG 圖像中的文本是可選的,同時也是可搜索的(很適合制作地圖);
7.SVG 可以與 Java 技術一起運行;
8.SVG 是開放的標准;
9.SVG 文件是純粹的 XML;
既然SVG是公認的xml文件格式定義的,那麼我們則可以通過解析xml文件拿到對應SVG圖的所有數據;拿到數據之後,我們需要用"path"元素產生SVG一個路徑,5.0以前,api並未提供矢量圖資源使用支持,我們需要自定義一個工具類,對數據進行解析,封裝成我們要的Path:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.DrawFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.Log;
import com.caverock.androidsvg.PreserveAspectRatio;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.util.ArrayList;
import java.util.List;
/**
* Util class to init and get paths from svg.
*/
public class SvgUtils {
/**
* It is for logging purposes.
*/
private static final String LOG_TAG = "SVGUtils";
/**
* All the paths with their attributes from the svg.
*/
private final List mPaths = new ArrayList<>();
/**
* The paint provided from the view.
*/
private final Paint mSourcePaint;
/**
* The init svg.
*/
private SVG mSvg;
/**
* Init the SVGUtils with a paint for coloring.
*
* @param sourcePaint - the paint for the coloring.
*/
public SvgUtils(final Paint sourcePaint) {
mSourcePaint = sourcePaint;
}
/**
* Loading the svg from the resources.
*
* @param context Context object to get the resources.
* @param svgResource int resource id of the svg.
*/
public void load(Context context, int svgResource) {
if (mSvg != null)
return;
try {
mSvg = SVG.getFromResource(context, svgResource);
mSvg.setDocumentPreserveAspectRatio(PreserveAspectRatio.UNSCALED);
} catch (SVGParseException e) {
Log.e(LOG_TAG, "Could not load specified SVG resource", e);
}
}
/**
* Draw the svg to the canvas.
*
* @param canvas The canvas to be drawn.
* @param width The width of the canvas.
* @param height The height of the canvas.
*/
public void drawSvgAfter(final Canvas canvas, final int width, final int height) {
final float strokeWidth = mSourcePaint.getStrokeWidth();
rescaleCanvas(width, height, strokeWidth, canvas);
}
/**
* Render the svg to canvas and catch all the paths while rendering.
*
* @param width - the width to scale down the view to,
* @param height - the height to scale down the view to,
* @return All the paths from the svg.
*/
public List getPathsForViewport(final int width, final int height) {
final float strokeWidth = mSourcePaint.getStrokeWidth();
Canvas canvas = new Canvas() {
private final Matrix mMatrix = new Matrix();
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public void drawPath(Path path, Paint paint) {
Path dst = new Path();
//noinspection deprecation
getMatrix(mMatrix);
path.transform(mMatrix, dst);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(strokeWidth);
mPaths.add(new SvgPath(dst, paint));
}
};
rescaleCanvas(width, height, strokeWidth, canvas);
return mPaths;
}
/**
* Rescale the canvas with specific width and height.
*
* @param width The width of the canvas.
* @param height The height of the canvas.
* @param strokeWidth Width of the path to add to scaling.
* @param canvas The canvas to be drawn.
*/
private void rescaleCanvas(int width, int height, float strokeWidth, Canvas canvas) {
if (mSvg == null)
return;
final RectF viewBox = mSvg.getDocumentViewBox();
final float scale = Math.min(width
/ (viewBox.width() + strokeWidth),
height / (viewBox.height() + strokeWidth));
canvas.translate((width - viewBox.width() * scale) / 2.0f,
(height - viewBox.height() * scale) / 2.0f);
canvas.scale(scale, scale);
mSvg.renderToCanvas(canvas);
}
/**
* Path with bounds for scalling , length and paint.
*/
public static class SvgPath {
/**
* Region of the path.
*/
private static final Region REGION = new Region();
/**
* This is done for clipping the bounds of the path.
*/
private static final Region MAX_CLIP =
new Region(Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE);
/**
* The path itself.
*/
final Path path;
/**
* The paint to be drawn later.
*/
final Paint paint;
/**
* The length of the path.
*/
float length;
/**
* Listener to notify that an animation step has happened.
*/
AnimationStepListener animationStepListener;
/**
* The bounds of the path.
*/
final Rect bounds;
/**
* The measure of the path, we can use it later to get segment of it.
*/
final PathMeasure measure;
/**
* Constructor to add the path and the paint.
*
* @param path The path that comes from the rendered svg.
* @param paint The result paint.
*/
SvgPath(Path path, Paint paint) {
this.path = path;
this.paint = paint;
measure = new PathMeasure(path, false);
this.length = measure.getLength();
REGION.setPath(path, MAX_CLIP);
bounds = REGION.getBounds();
}
/**
* Sets the animation step listener.
*
* @param animationStepListener AnimationStepListener.
*/
public void setAnimationStepListener(AnimationStepListener animationStepListener) {
this.animationStepListener = animationStepListener;
}
/**
* Sets the length of the path.
*
* @param length The length to be set.
*/
public void setLength(float length) {
path.reset();
measure.getSegment(0.0f, length, path, true);
path.rLineTo(0.0f, 0.0f);
if (animationStepListener != null) {
animationStepListener.onAnimationStep();
}
}
/**
* @return The length of the path.
*/
public float getLength() {
return length;
}
}
public interface AnimationStepListener {
/**
* Called when an animation step happens.
*/
void onAnimationStep();
}
}
package com.eftimoff.androipathview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Interpolator;
import com.eftimoff.mylibrary.R;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.AnimatorSet;
import java.util.ArrayList;
import java.util.List;
/**
* PathView is a View that animates paths.
*/
@SuppressWarnings("unused")
public class PathView extends View implements SvgUtils.AnimationStepListener {
/**
* Logging tag.
*/
public static final String LOG_TAG = "PathView";
/**
* The paint for the path.
*/
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* Utils to catch the paths from the svg.
*/
private final SvgUtils svgUtils = new SvgUtils(paint);
/**
* All the paths provided to the view. Both from Path and Svg.
*/
private List paths = new ArrayList<>();
/**
* This is a lock before the view is redrawn
* or resided it must be synchronized with this object.
*/
private final Object mSvgLock = new Object();
/**
* Thread for working with the object above.
*/
private Thread mLoader;
/**
* The svg image from the raw directory.
*/
private int svgResourceId;
/**
* Object that builds the animation for the path.
*/
private AnimatorBuilder animatorBuilder;
/**
* Object that builds the animation set for the path.
*/
private AnimatorSetBuilder animatorSetBuilder;
/**
* The progress of the drawing.
*/
private float progress = 0f;
/**
* If the used colors are from the svg or from the set color.
*/
private boolean naturalColors;
/**
* If the view is filled with its natural colors after path drawing.
*/
private boolean fillAfter;
/**
* The view will be filled and showed as default without any animation.
*/
private boolean fill;
/**
* The solid color used for filling svg when fill is true
*/
private int fillColor;
/**
* The width of the view.
*/
private int width;
/**
* The height of the view.
*/
private int height;
/**
* Will be used as a temporary surface in each onDraw call for more control over content are
* drawing.
*/
private Bitmap mTempBitmap;
/**
* Will be used as a temporary Canvas for mTempBitmap for drawing content on it.
*/
private Canvas mTempCanvas;
/**
* Default constructor.
*
* @param context The Context of the application.
*/
public PathView(Context context) {
this(context, null);
}
/**
* Default constructor.
*
* @param context The Context of the application.
* @param attrs attributes provided from the resources.
*/
public PathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Default constructor.
*
* @param context The Context of the application.
* @param attrs attributes provided from the resources.
* @param defStyle Default style.
*/
public PathView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint.setStyle(Paint.Style.STROKE);
getFromAttributes(context, attrs);
}
/**
* Get all the fields from the attributes .
*
* @param context The Context of the application.
* @param attrs attributes provided from the resources.
*/
private void getFromAttributes(Context context, AttributeSet attrs) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PathView);
try {
if (a != null) {
paint.setColor(a.getColor(R.styleable.PathView_pathColor, 0xff00ff00));
paint.setStrokeWidth(a.getDimensionPixelSize(R.styleable.PathView_pathWidth, 8));
svgResourceId = a.getResourceId(R.styleable.PathView_svg, 0);
naturalColors = a.getBoolean(R.styleable.PathView_naturalColors, false);
fill = a.getBoolean(R.styleable.PathView_fill,false);
fillColor = a.getColor(R.styleable.PathView_fillColor,Color.argb(0,0,0,0));
}
} finally {
if (a != null) {
a.recycle();
}
//to draw the svg in first show , if we set fill to true
invalidate();
}
}
/**
* Set paths to be drawn and animated.
*
* @param paths - Paths that can be drawn.
*/
public void setPaths(final List paths) {
for (Path path : paths) {
this.paths.add(new SvgUtils.SvgPath(path, paint));
}
synchronized (mSvgLock) {
updatePathsPhaseLocked();
}
}
/**
* Set path to be drawn and animated.
*
* @param path - Paths that can be drawn.
*/
public void setPath(final Path path) {
paths.add(new SvgUtils.SvgPath(path, paint));
synchronized (mSvgLock) {
updatePathsPhaseLocked();
}
}
/**
* Animate this property. It is the percentage of the path that is drawn.
* It must be [0,1].
*
* @param percentage float the percentage of the path.
*/
public void setPercentage(float percentage) {
if (percentage < 0.0f || percentage > 1.0f) {
throw new IllegalArgumentException("setPercentage not between 0.0f and 1.0f");
}
progress = percentage;
synchronized (mSvgLock) {
updatePathsPhaseLocked();
}
invalidate();
}
/**
* This refreshes the paths before draw and resize.
*/
private void updatePathsPhaseLocked() {
final int count = paths.size();
for (int i = 0; i < count; i++) {
SvgUtils.SvgPath svgPath = paths.get(i);
svgPath.path.reset();
svgPath.measure.getSegment(0.0f, svgPath.length * progress, svgPath.path, true);
// Required only for Android 4.4 and earlier
svgPath.path.rLineTo(0.0f, 0.0f);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mTempBitmap==null || (mTempBitmap.getWidth()!=canvas.getWidth()||mTempBitmap.getHeight()!=canvas.getHeight()) )
{
mTempBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
mTempCanvas = new Canvas(mTempBitmap);
}
mTempBitmap.eraseColor(0);
synchronized (mSvgLock) {
mTempCanvas.save();
mTempCanvas.translate(getPaddingLeft(), getPaddingTop());
fill(mTempCanvas);
final int count = paths.size();
for (int i = 0; i < count; i++) {
final SvgUtils.SvgPath svgPath = paths.get(i);
final Path path = svgPath.path;
final Paint paint1 = naturalColors ? svgPath.paint : paint;
mTempCanvas.drawPath(path, paint1);
}
fillAfter(mTempCanvas);
mTempCanvas.restore();
applySolidColor(mTempBitmap);
canvas.drawBitmap(mTempBitmap,0,0,null);
}
}
/**
* If there is svg , the user called setFillAfter(true) and the progress is finished.
*
* @param canvas Draw to this canvas.
*/
private void fillAfter(final Canvas canvas) {
if (svgResourceId != 0 && fillAfter && Math.abs(progress - 1f) < 0.00000001) {
svgUtils.drawSvgAfter(canvas, width, height);
}
}
/**
* If there is svg , the user called setFill(true).
*
* @param canvas Draw to this canvas.
*/
private void fill(final Canvas canvas) {
if (svgResourceId != 0 && fill) {
svgUtils.drawSvgAfter(canvas, width, height);
}
}
/**
* If fillColor had value before then we replace untransparent pixels of bitmap by solid color
*
* @param bitmap Draw to this canvas.
*/
private void applySolidColor(final Bitmap bitmap) {
if(fill && fillColor!=Color.argb(0,0,0,0) )
if (bitmap != null) {
for(int x=0;x animators = new ArrayList<>();
/**
* Listener called before the animation.
*/
private AnimatorBuilder.ListenerStart listenerStart;
/**
* Listener after the animation.
*/
private AnimatorBuilder.ListenerEnd animationEnd;
/**
* Animation listener.
*/
private AnimatorSetBuilder.PathViewAnimatorListener pathViewAnimatorListener;
/**
* The animator that can animate paths sequentially
*/
private AnimatorSet animatorSet = new AnimatorSet();
/**
* The list of paths to be animated.
*/
private List paths;
/**
* Default constructor.
*
* @param pathView The view that must be animated.
*/
public AnimatorSetBuilder(final PathView pathView) {
paths = pathView.paths;
for (SvgUtils.SvgPath path : paths) {
path.setAnimationStepListener(pathView);
ObjectAnimator animation = ObjectAnimator.ofFloat(path, "length", 0.0f, path.getLength());
animators.add(animation);
}
animatorSet.playSequentially(animators);
}
/**
* Sets the duration of the animation. Since the AnimatorSet sets the duration for each
* Animator, we have to divide it by the number of paths.
*
* @param duration - The duration of the animation.
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder duration(final int duration) {
this.duration = duration / paths.size();
return this;
}
/**
* Set the Interpolator.
*
* @param interpolator - Interpolator.
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder interpolator(final Interpolator interpolator) {
this.interpolator = interpolator;
return this;
}
/**
* The delay before the animation.
*
* @param delay - int the delay
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder delay(final int delay) {
this.delay = delay;
return this;
}
/**
* Set a listener before the start of the animation.
*
* @param listenerStart an interface called before the animation
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder listenerStart(final AnimatorBuilder.ListenerStart listenerStart) {
this.listenerStart = listenerStart;
if (pathViewAnimatorListener == null) {
pathViewAnimatorListener = new PathViewAnimatorListener();
animatorSet.addListener(pathViewAnimatorListener);
}
return this;
}
/**
* Set a listener after of the animation.
*
* @param animationEnd an interface called after the animation
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder listenerEnd(final AnimatorBuilder.ListenerEnd animationEnd) {
this.animationEnd = animationEnd;
if (pathViewAnimatorListener == null) {
pathViewAnimatorListener = new PathViewAnimatorListener();
animatorSet.addListener(pathViewAnimatorListener);
}
return this;
}
/**
* Starts the animation.
*/
public void start() {
resetAllPaths();
animatorSet.cancel();
animatorSet.setDuration(duration);
animatorSet.setInterpolator(interpolator);
animatorSet.setStartDelay(delay);
animatorSet.start();
}
/**
* Sets the length of all the paths to 0.
*/
private void resetAllPaths() {
for (SvgUtils.SvgPath path : paths) {
path.setLength(0);
}
}
/**
* Animation listener to be able to provide callbacks for the caller.
*/
private class PathViewAnimatorListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animation) {
if (listenerStart != null)
listenerStart.onAnimationStart();
}
@Override
public void onAnimationEnd(Animator animation) {
if (animationEnd != null)
animationEnd.onAnimationEnd();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
}
}
VectorDrawable,直接可以使用SVG類型的資源。似乎有點跑題了,下面正式進入本篇要呈現給讀者的東西,關於Android進度加載動畫。
然後在xml定義ProgressBar,引用此drawable就可以了
1.2 自定義控件實現: CircleProgress.java
package com.john.circleprogress;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AnimationUtils;
public class CircleProgress extends View
{
private static final int RED = 0xFFE5282C;
private static final int YELLOW = 0xFF1F909A;
private static final int BLUE = 0xFFFC9E12;
private static final int COLOR_NUM = 3;
private int[] COLORS;
private TimeInterpolator mInterpolator = new EaseInOutCubicInterpolator();
private final double DEGREE = Math.PI / 180;
private Paint mPaint;
private int mViewSize;
private int mPointRadius;
private long mStartTime;
private long mPlayTime;
private boolean mStartAnim = false;
private Point mCenter = new Point();
private ArcPoint[] mArcPoint;
private static final int POINT_NUM = 15;
private static final int DELTA_ANGLE = 360 / POINT_NUM;
private long mDuration = 3600;
public CircleProgress(Context context)
{
super(context);
init(null, 0);
}
public CircleProgress(Context context, AttributeSet attrs)
{
super(context, attrs);
init(attrs, 0);
}
public CircleProgress(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle)
{
mArcPoint = new ArcPoint[POINT_NUM];
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircleProgress, defStyle, 0);
int color1 = a.getColor(R.styleable.CircleProgress_color1, RED);
int color2 = a.getColor(R.styleable.CircleProgress_color2, YELLOW);
int color3 = a.getColor(R.styleable.CircleProgress_color3, BLUE);
a.recycle();
COLORS = new int[] { color1, color2, color3 };
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int defaultSize = getResources().getDimensionPixelSize(R.dimen.default_circle_view_size);
int width = getDefaultSize(defaultSize, widthMeasureSpec);
int height = getDefaultSize(defaultSize, heightMeasureSpec);
mViewSize = Math.min(width, height);
setMeasuredDimension(mViewSize, mViewSize);
mCenter.set(mViewSize / 2, mViewSize / 2);
calPoints(1.0f);
}
@Override
protected void onDraw(Canvas canvas)
{
canvas.save();
canvas.translate(mCenter.x, mCenter.y);
float factor = getFactor();
canvas.rotate(36 * factor);
float x, y;
for (int i = 0; i < POINT_NUM; ++i)
{
mPaint.setColor(mArcPoint[i].color);
float itemFactor = getItemFactor(i, factor);
x = mArcPoint[i].x - 2 * mArcPoint[i].x * itemFactor;
y = mArcPoint[i].y - 2 * mArcPoint[i].y * itemFactor;
canvas.drawCircle(x, y, mPointRadius, mPaint);
}
canvas.restore();
if (mStartAnim)
{
postInvalidate();
}
}
private void calPoints(float factor)
{
int radius = (int) (mViewSize / 3 * factor);
mPointRadius = radius / 12;
for (int i = 0; i < POINT_NUM; ++i)
{
float x = radius * -(float) Math.sin(DEGREE * DELTA_ANGLE * i);
float y = radius * -(float) Math.cos(DEGREE * DELTA_ANGLE * i);
ArcPoint point = new ArcPoint(x, y, COLORS[i % COLOR_NUM]);
mArcPoint[i] = point;
}
}
private float getFactor()
{
if (mStartAnim)
{
mPlayTime = AnimationUtils.currentAnimationTimeMillis() - mStartTime;
}
float factor = mPlayTime / (float) mDuration;
return factor % 1f;
}
private float getItemFactor(int index, float factor)
{
float itemFactor = (factor - 0.66f / POINT_NUM * index) * 3;
if (itemFactor < 0f)
{
itemFactor = 0f;
}
else if (itemFactor > 1f)
{
itemFactor = 1f;
}
return mInterpolator.getInterpolation(itemFactor);
}
public void startAnim()
{
mPlayTime = mPlayTime % mDuration;
mStartTime = AnimationUtils.currentAnimationTimeMillis() - mPlayTime;
mStartAnim = true;
postInvalidate();
}
public void reset()
{
stopAnim();
mPlayTime = 0;
postInvalidate();
}
public void stopAnim()
{
mStartAnim = false;
}
public void setInterpolator(TimeInterpolator interpolator)
{
mInterpolator = interpolator;
}
public void setDuration(long duration)
{
mDuration = duration;
}
public void setRadius(float factor)
{
stopAnim();
calPoints(factor);
startAnim();
}
static class ArcPoint
{
float x;
float y;
int color;
ArcPoint(float x, float y, int color)
{
this.x = x;
this.y = y;
this.color = color;
}
}
}
動畫插值器實現類EaseInOutCubicInterpolator.java
package com.john.circleprogress;
import android.animation.TimeInterpolator;
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 fichardu
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
public class EaseInOutCubicInterpolator implements TimeInterpolator
{
@Override
public float getInterpolation(float input)
{
if ((input *= 2) < 1.0f)
{
return 0.5f * input * input * input;
}
input -= 2;
return 0.5f * input * input * input + 1;
}
}
主類測試兩種圓形進度條效果:MainActivity.java
package com.john.circleprogress;
import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
/**
* test
*
* @author john
* @created 2016-6-30
*/
public class MainActivity extends Activity implements View.OnClickListener
{
private CircleProgress mProgressView;
private ProgressBar progressBar;
private View mBtn0;
private View mBtn1;
private LinearLayout mLinearLayout;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
mProgressView = (CircleProgress) findViewById(R.id.progress);
progressBar = (ProgressBar) findViewById(R.id.seekBar);
mLinearLayout = (LinearLayout) findViewById(R.id.btn_layout);
mBtn0 = findViewById(R.id.circle_progress);
mBtn0.setOnClickListener(this);
mBtn1 = findViewById(R.id.rotate_progress);
mBtn1.setOnClickListener(this);
}
@Override
public void onClick(View v)
{
// TODO Auto-generated method stub
mLinearLayout.setVisibility(View.GONE);
if (v == mBtn0)
{
mProgressView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
mProgressView.startAnim();
}
else if (v == mBtn1)
{
mProgressView.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
// TODO Auto-generated method stub
if (event.getAction() == KeyEvent.ACTION_DOWN)
{
switch (keyCode)
{
case KeyEvent.KEYCODE_BACK:
if (mProgressView.isShown() || progressBar.isShown())
{
mLinearLayout.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
mProgressView.setVisibility(View.GONE);
return true;
}
break;
default:
break;
}
}
return super.onKeyDown(keyCode, event);
}
}
圓形進度條測試效果:
稍後補上。。。
2、一個柔和飽滿的Loading UI
效果稍後補上。。。
實現這樣一個效果主要有三點:
1、葉子隨機產生並作無規律移動旋轉;
2、隨著進度往前繪制的進度條;
3、葉子與進度條有一個融合交互過程
代碼結構:
一個動畫工具類AnimationUtils.java,這裡主要是用來實現風扇的旋轉效果。
package com.john.loading.animation;
import android.content.Context;
import android.graphics.drawable.AnimationDrawable;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
public class AnimationUtils
{
public static RotateAnimation initRotateAnimation(long duration, int fromAngle, int toAngle, boolean isFillAfter, int repeatCount)
{
RotateAnimation mLoadingRotateAnimation = new RotateAnimation(fromAngle, toAngle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
LinearInterpolator lirInterpolator = new LinearInterpolator();
mLoadingRotateAnimation.setInterpolator(lirInterpolator);
mLoadingRotateAnimation.setDuration(duration);
mLoadingRotateAnimation.setFillAfter(isFillAfter);
mLoadingRotateAnimation.setRepeatCount(repeatCount);
mLoadingRotateAnimation.setRepeatMode(Animation.RESTART);
return mLoadingRotateAnimation;
}
public static RotateAnimation initRotateAnimation(boolean isClockWise, long duration, boolean isFillAfter, int repeatCount)
{
int endAngle;
if (isClockWise)
{
endAngle = 360;
}
else
{
endAngle = -360;
}
RotateAnimation mLoadingRotateAnimation = new RotateAnimation(0, endAngle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
LinearInterpolator lirInterpolator = new LinearInterpolator();
mLoadingRotateAnimation.setInterpolator(lirInterpolator);
mLoadingRotateAnimation.setDuration(duration);
mLoadingRotateAnimation.setFillAfter(isFillAfter);
mLoadingRotateAnimation.setRepeatCount(repeatCount);
mLoadingRotateAnimation.setRepeatMode(Animation.RESTART);
return mLoadingRotateAnimation;
}
public static AnimationDrawable initAnimationDrawable(Context context, int[] drawableIds, int durationTime, boolean isOneShot)
{
AnimationDrawable mAnimationDrawable = new AnimationDrawable();
for (int i = 0; i < drawableIds.length; i++)
{
int id = drawableIds[i];
mAnimationDrawable.addFrame(context.getResources().getDrawable(id), durationTime);
}
mAnimationDrawable.setOneShot(isOneShot);
return mAnimationDrawable;
}
public static Animation initAlphaAnimtion(Context context, float fromAlpha, float toAlpha, long duration)
{
Animation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha);
alphaAnimation.setDuration(duration);
return alphaAnimation;
}
}
package com.john.loading.animation;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
public class UiUtils
{
static public int getScreenWidthPixels(Context context)
{
DisplayMetrics dm = new DisplayMetrics();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm);
return dm.widthPixels;
}
static public int dipToPx(Context context, int dip)
{
return (int) (dip * getScreenDensity(context) + 0.5f);
}
static public float getScreenDensity(Context context)
{
try
{
DisplayMetrics dm = new DisplayMetrics();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm);
return dm.density;
}
catch (Exception e)
{
return DisplayMetrics.DENSITY_DEFAULT;
}
}
}
繪制葉子和進度條,將其封裝成一個自定義控件,LeafLoadingView.java
package com.john.loading.animation;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class LeafLoadingView extends View
{
private static final String TAG = "LeafLoadingView";
// 淡白色
private static final int WHITE_COLOR = 0xfffde399;
// 橙色
private static final int ORANGE_COLOR = 0xffffa800;
// 中等振幅大小
private static final int MIDDLE_AMPLITUDE = 13;
// 不同類型之間的振幅差距
private static final int AMPLITUDE_DISPARITY = 5;
// 總進度
private static final int TOTAL_PROGRESS = 100;
// 葉子飄動一個周期所花的時間
private static final long LEAF_FLOAT_TIME = 3000;
// 葉子旋轉一周需要的時間
private static final long LEAF_ROTATE_TIME = 2000;
// 用於控制繪制的進度條距離左/上/下的距離
private static final int LEFT_MARGIN = 9;
// 用於控制繪制的進度條距離右的距離
private static final int RIGHT_MARGIN = 25;
private int mLeftMargin, mRightMargin;
// 中等振幅大小
private int mMiddleAmplitude = MIDDLE_AMPLITUDE;
// 振幅差
private int mAmplitudeDisparity = AMPLITUDE_DISPARITY;
// 葉子飄動一個周期所花的時間
private long mLeafFloatTime = LEAF_FLOAT_TIME;
// 葉子旋轉一周需要的時間
private long mLeafRotateTime = LEAF_ROTATE_TIME;
private Resources mResources;
private Bitmap mLeafBitmap;
private int mLeafWidth, mLeafHeight;
private Bitmap mOuterBitmap;
private Rect mOuterSrcRect, mOuterDestRect;
private int mOuterWidth, mOuterHeight;
private int mTotalWidth, mTotalHeight;
private Paint mBitmapPaint, mWhitePaint, mOrangePaint;
private RectF mWhiteRectF, mOrangeRectF, mArcRectF;
// 當前進度
private int mProgress;
//最大進度
private int mMaxProgress=TOTAL_PROGRESS;
// 所繪制的進度條部分的寬度
private int mProgressWidth;
// 當前所在的繪制的進度條的位置
private int mCurrentProgressPosition;
// 弧形的半徑
private int mArcRadius;
// arc的右上角的x坐標,也是矩形x坐標的起始點
private int mArcRightLocation;
// 用於產生葉子信息
private LeafFactory mLeafFactory;
// 產生出的葉子信息
private List mLeafInfos;
// 用於控制隨機增加的時間不抱團
private int mAddTime;
public LeafLoadingView(Context context, AttributeSet attrs)
{
super(context, attrs);
mResources = getResources();
mLeftMargin = UiUtils.dipToPx(context, LEFT_MARGIN);
mRightMargin = UiUtils.dipToPx(context, RIGHT_MARGIN);
mLeafFloatTime = LEAF_FLOAT_TIME;
mLeafRotateTime = LEAF_ROTATE_TIME;
initBitmap();
initPaint();
mLeafFactory = new LeafFactory();
mLeafInfos = mLeafFactory.generateLeafs();
}
private void initPaint()
{
mBitmapPaint = new Paint();
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setDither(true);
mBitmapPaint.setFilterBitmap(true);
mWhitePaint = new Paint();
mWhitePaint.setAntiAlias(true);
mWhitePaint.setColor(WHITE_COLOR);
mOrangePaint = new Paint();
mOrangePaint.setAntiAlias(true);
mOrangePaint.setColor(ORANGE_COLOR);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
// 繪制進度條和葉子
// 之所以把葉子放在進度條裡繪制,主要是層級原因
drawProgressAndLeafs(canvas);
canvas.drawBitmap(mOuterBitmap, mOuterSrcRect, mOuterDestRect, mBitmapPaint);
postInvalidate();
}
private void drawProgressAndLeafs(Canvas canvas)
{
if (mProgress >= TOTAL_PROGRESS)
{
mProgress = 0;
}
// mProgressWidth為進度條的寬度,根據當前進度算出進度條的位置
mCurrentProgressPosition = mProgressWidth * mProgress / TOTAL_PROGRESS;
// 即當前位置在圖中所示1范圍內
if (mCurrentProgressPosition < mArcRadius)
{
Log.i(TAG, "mProgress = " + mProgress + "---mCurrentProgressPosition = " + mCurrentProgressPosition + "--mArcProgressWidth" + mArcRadius);
// 1.繪制白色ARC,繪制orange ARC
// 2.繪制白色矩形
// 1.繪制白色ARC
canvas.drawArc(mArcRectF, 90, 180, false, mWhitePaint);
// 2.繪制白色矩形
mWhiteRectF.left = mArcRightLocation;
canvas.drawRect(mWhiteRectF, mWhitePaint);
// 繪制葉子
drawLeafs(canvas);
// 3.繪制棕色 ARC
// 單邊角度
int angle = (int) Math.toDegrees(Math.acos((mArcRadius - mCurrentProgressPosition) / (float) mArcRadius));
// 起始的位置
int startAngle = 180 - angle;
// 掃過的角度
int sweepAngle = 2 * angle;
Log.i(TAG, "startAngle = " + startAngle);
canvas.drawArc(mArcRectF, startAngle, sweepAngle, false, mOrangePaint);
}
else
{
Log.i(TAG, "mProgress = " + mProgress + "---transfer-----mCurrentProgressPosition = " + mCurrentProgressPosition + "--mArcProgressWidth" + mArcRadius);
// 1.繪制white RECT
// 2.繪制Orange ARC
// 3.繪制orange RECT
// 這個層級進行繪制能讓葉子感覺是融入棕色進度條中
// 1.繪制white RECT
mWhiteRectF.left = mCurrentProgressPosition;
canvas.drawRect(mWhiteRectF, mWhitePaint);
// 繪制葉子
drawLeafs(canvas);
// 2.繪制Orange ARC
canvas.drawArc(mArcRectF, 90, 180, false, mOrangePaint);
// 3.繪制orange RECT
mOrangeRectF.left = mArcRightLocation;
mOrangeRectF.right = mCurrentProgressPosition;
canvas.drawRect(mOrangeRectF, mOrangePaint);
}
}
/**
* 繪制葉子
*
* @param canvas
*/
private void drawLeafs(Canvas canvas)
{
mLeafRotateTime = mLeafRotateTime <= 0 ? LEAF_ROTATE_TIME : mLeafRotateTime;
long currentTime = System.currentTimeMillis();
for (int i = 0; i < mLeafInfos.size(); i++)
{
Leaf leaf = mLeafInfos.get(i);
if (currentTime > leaf.startTime && leaf.startTime != 0)
{
// 繪制葉子--根據葉子的類型和當前時間得出葉子的(x,y)
getLeafLocation(leaf, currentTime);
// 根據時間計算旋轉角度
canvas.save();
// 通過Matrix控制葉子旋轉
Matrix matrix = new Matrix();
float transX = mLeftMargin + leaf.x;
float transY = mLeftMargin + leaf.y;
Log.i(TAG, "left.x = " + leaf.x + "--leaf.y=" + leaf.y);
matrix.postTranslate(transX, transY);
// 通過時間關聯旋轉角度,則可以直接通過修改LEAF_ROTATE_TIME調節葉子旋轉快慢
float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime) / (float) mLeafRotateTime;
int angle = (int) (rotateFraction * 360);
// 根據葉子旋轉方向確定葉子旋轉角度
int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle + leaf.rotateAngle;
matrix.postRotate(rotate, transX + mLeafWidth / 2, transY + mLeafHeight / 2);
canvas.drawBitmap(mLeafBitmap, matrix, mBitmapPaint);
canvas.restore();
}
else
{
continue;
}
}
}
private void getLeafLocation(Leaf leaf, long currentTime)
{
long intervalTime = currentTime - leaf.startTime;
mLeafFloatTime = mLeafFloatTime <= 0 ? LEAF_FLOAT_TIME : mLeafFloatTime;
if (intervalTime < 0)
{
return;
}
else if (intervalTime > mLeafFloatTime)
{
leaf.startTime = System.currentTimeMillis() + new Random().nextInt((int) mLeafFloatTime);
}
float fraction = (float) intervalTime / mLeafFloatTime;
leaf.x = (int) (mProgressWidth - mProgressWidth * fraction);
leaf.y = getLocationY(leaf);
}
// 通過葉子信息獲取當前葉子的Y值
private int getLocationY(Leaf leaf)
{
// y = A(wx+Q)+h
float w = (float) ((float) 2 * Math.PI / mProgressWidth);
float a = mMiddleAmplitude;
switch (leaf.type)
{
case LITTLE:
// 小振幅 = 中等振幅 - 振幅差
a = mMiddleAmplitude - mAmplitudeDisparity;
break;
case MIDDLE:
a = mMiddleAmplitude;
break;
case BIG:
// 小振幅 = 中等振幅 + 振幅差
a = mMiddleAmplitude + mAmplitudeDisparity;
break;
default:
break;
}
Log.i(TAG, "---a = " + a + "---w = " + w + "--leaf.x = " + leaf.x);
return (int) (a * Math.sin(w * leaf.x)) + mArcRadius * 2 / 3;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void initBitmap()
{
mLeafBitmap = ((BitmapDrawable) mResources.getDrawable(R.drawable.leaf)).getBitmap();
mLeafWidth = mLeafBitmap.getWidth();
mLeafHeight = mLeafBitmap.getHeight();
mOuterBitmap = ((BitmapDrawable) mResources.getDrawable(R.drawable.leaf_kuang)).getBitmap();
mOuterWidth = mOuterBitmap.getWidth();
mOuterHeight = mOuterBitmap.getHeight();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mTotalWidth = w;
mTotalHeight = h;
mProgressWidth = mTotalWidth - mLeftMargin - mRightMargin;
mArcRadius = (mTotalHeight - 2 * mLeftMargin) / 2;
mOuterSrcRect = new Rect(0, 0, mOuterWidth, mOuterHeight);
mOuterDestRect = new Rect(0, 0, mTotalWidth, mTotalHeight);
mWhiteRectF = new RectF(mLeftMargin + mCurrentProgressPosition, mLeftMargin, mTotalWidth - mRightMargin, mTotalHeight - mLeftMargin);
mOrangeRectF = new RectF(mLeftMargin + mArcRadius, mLeftMargin, mCurrentProgressPosition, mTotalHeight - mLeftMargin);
mArcRectF = new RectF(mLeftMargin, mLeftMargin, mLeftMargin + 2 * mArcRadius, mTotalHeight - mLeftMargin);
mArcRightLocation = mLeftMargin + mArcRadius;
}
private enum StartType
{
LITTLE, MIDDLE, BIG
}
/**
* 葉子對象,用來記錄葉子主要數據
*
* @author Ajian_Studio
*/
private class Leaf
{
// 在繪制部分的位置
float x, y;
// 控制葉子飄動的幅度
StartType type;
// 旋轉角度
int rotateAngle;
// 旋轉方向--0代表順時針,1代表逆時針
int rotateDirection;
// 起始時間(ms)
long startTime;
}
private class LeafFactory
{
private static final int MAX_LEAFS = 8;
Random random = new Random();
// 生成一個葉子信息
public Leaf generateLeaf()
{
Leaf leaf = new Leaf();
int randomType = random.nextInt(3);
// 隨時類型- 隨機振幅
StartType type = StartType.MIDDLE;
switch (randomType)
{
case 0:
break;
case 1:
type = StartType.LITTLE;
break;
case 2:
type = StartType.BIG;
break;
default:
break;
}
leaf.type = type;
// 隨機起始的旋轉角度
leaf.rotateAngle = random.nextInt(360);
// 隨機旋轉方向(順時針或逆時針)
leaf.rotateDirection = random.nextInt(2);
// 為了產生交錯的感覺,讓開始的時間有一定的隨機性
mLeafFloatTime = mLeafFloatTime <= 0 ? LEAF_FLOAT_TIME : mLeafFloatTime;
mAddTime += random.nextInt((int) (mLeafFloatTime * 2));
leaf.startTime = System.currentTimeMillis() + mAddTime;
return leaf;
}
// 根據最大葉子數產生葉子信息
public List generateLeafs()
{
return generateLeafs(MAX_LEAFS);
}
// 根據傳入的葉子數量產生葉子信息
public List generateLeafs(int leafSize)
{
List leafs = new LinkedList();
for (int i = 0; i < leafSize; i++)
{
leafs.add(generateLeaf());
}
return leafs;
}
}
/**
* 設置中等振幅
*
* @param amplitude
*/
public void setMiddleAmplitude(int amplitude)
{
this.mMiddleAmplitude = amplitude;
}
/**
* 設置振幅差
*
* @param disparity
*/
public void setMplitudeDisparity(int disparity)
{
this.mAmplitudeDisparity = disparity;
}
/**
* 獲取中等振幅
*
* @param amplitude
*/
public int getMiddleAmplitude()
{
return mMiddleAmplitude;
}
/**
* 獲取振幅差
*
* @param disparity
*/
public int getMplitudeDisparity()
{
return mAmplitudeDisparity;
}
/**
* 設置進度
*
* @param progress
*/
public void setProgress(int progress)
{
this.mProgress = progress;
postInvalidate();
}
/**
* 設置葉子飄完一個周期所花的時間
*
* @param time
*/
public void setLeafFloatTime(long time)
{
this.mLeafFloatTime = time;
}
/**
* 設置葉子旋轉一周所花的時間
*
* @param time
*/
public void setLeafRotateTime(long time)
{
this.mLeafRotateTime = time;
}
/**
* 獲取葉子飄完一個周期所花的時間
*/
public long getLeafFloatTime()
{
mLeafFloatTime = mLeafFloatTime == 0 ? LEAF_FLOAT_TIME : mLeafFloatTime;
return mLeafFloatTime;
}
/**
* 獲取葉子旋轉一周所花的時間
*/
public long getLeafRotateTime()
{
mLeafRotateTime = mLeafRotateTime == 0 ? LEAF_ROTATE_TIME : mLeafRotateTime;
return mLeafRotateTime;
}
public int getMaxProgress()
{
return mMaxProgress;
}
public void setMaxProgress(int maxProgress)
{
mMaxProgress = maxProgress;
}
}
創建一個Activity,專門用於顯示進度加載效果。
package com.john.loading.animation;
import java.util.Random;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
public class LeafLoadingActivity extends Activity
{
Handler mHandler = new Handler()
{
public void handleMessage(Message msg)
{
switch (msg.what)
{
case REFRESH_PROGRESS:
if (mProgress < 40)
{
mProgress += 1;
// 隨機800ms以內刷新一次
mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, new Random().nextInt(800));
mLeafLoadingView.setProgress(mProgress);
}
else
{
mProgress += 1;
// 隨機1200ms以內刷新一次
mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, new Random().nextInt(1200));
mLeafLoadingView.setProgress(mProgress);
}
if (mProgress == mLeafLoadingView.getMaxProgress())
{
mHandler.sendEmptyMessage(LOADING_FINISHED);
}
break;
case LOADING_FINISHED:
{
finish();
// go to other activity
}
break;
default:
break;
}
};
};
private static final int REFRESH_PROGRESS = 0x10;
private static final int LOADING_FINISHED = 1;
private LeafLoadingView mLeafLoadingView;
private View mFanView;
private int mProgress = 0;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.leaf_loading_layout);
initViews();
mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, 3000);
}
private void initViews()
{
mFanView = findViewById(R.id.fan_pic);
RotateAnimation rotateAnimation = AnimationUtils.initRotateAnimation(false, 1500, true, Animation.INFINITE);
mFanView.startAnimation(rotateAnimation);
mLeafLoadingView = (LeafLoadingView) findViewById(R.id.leaf_loading);
}
}
下一篇:Android酷炫動畫效果之3D星體旋轉效果
Android - ScrollView添加提示Arrow(箭頭)
ScrollView添加提示Arrow(箭頭) 在ScrollView的滑動功能中,需要給用戶提示,可以滑動,可以添加兩個箭頭。
從源碼分析Android的Glide庫的圖片加載流程及特點
0.基礎知識Glide中有一部分單詞,我不知道用什麼中文可以確切的表達出含義,用英文單詞可能在行文中更加合適,還有一些詞在Glide中有特別的含義,我理解的可能也不深入,
Android中操作SQLite數據庫快速入門教程
SQLite是Android平台軟件開發中會經常用到的數據庫產品,作為一款輕型數據庫,SQLite的設計目標就是是嵌入式的,而且目前已經在很多嵌入式產品中使用了它,它占用
com.android.phone已停止運行怎麼解決
在安卓手機上,不少用戶都會遇過com.android.phone已停止的彈窗,尤其經常刷機的最明顯。導致的原因實在太多,有刷機步驟不對的,亂改系統文件的,這