luncrzs 发布的文章

LoadingCircleView 会转圈圈的圆弧的实现

这个转圈圈,不是单纯的转圈圈,而是最近在安卓上很常见的那个很难用语言描述的不知道是谁发明出来的一段圆上的弧在旋转然后忽长忽短的那个。我期初看了好久也没太看懂他的规律,根据canvas的drawArc方法,我原来是想用startAngle和endAngle来描述圆弧,后来发现太复杂,灵光一闪发现用drawArc里面的startAngle和sweepAngle就能做到,而且逻辑相对不复杂。

public class LoadingCircleView extends View {

    private int strokeColor = Color.RED; // TODO: use a default from R.color...
    private int mCircleLineStrokeWidth = 8;
    private boolean drawCircle = true;
    private int circleBgColor = Color.WHITE;

    private RectF mRectF;
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private int startAngle = -90;
    private int sweepAngle = 0;
    private Timer timer;
    private RedrawTimerTask timerTask;
    private boolean isRotating = false;
    private MyHandler handler = new MyHandler(this);

    public LoadingCircleView(Context context) {
        super(context);
        mContext = context;
        init(null, 0);
    }

    public LoadingCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init(attrs, 0);
    }

    public LoadingCircleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mContext = context;
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        if(attrs != null){
            final TypedArray a = getContext().obtainStyledAttributes(
                    attrs, R.styleable.LoadingCircleView, defStyle, 0);

            strokeColor = a.getColor(
                    R.styleable.LoadingCircleView_strokeColor,
                    getResources().getColor(R.color.theme_dark_blue));
            drawCircle = a.getBoolean(R.styleable.LoadingCircleView_drawCircle,true);
            circleBgColor = a.getColor(R.styleable.LoadingCircleView_circleBgColor,circleBgColor);
            a.recycle();
        }else{
            strokeColor = getResources().getColor(R.color.theme_dark_blue);
        }

        mPaint = new Paint();
        mRectF = new RectF();
        mPaint.setAntiAlias(true);

        addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                mWidth = getWidth()-getPaddingLeft()-getPaddingRight();
                mHeight = getHeight()-getPaddingTop()-getPaddingBottom();
                if (mWidth != mHeight) {
                    int min = Math.min(mWidth, mHeight);
                    mWidth = min;
                    mHeight = min;
                }
                mCircleLineStrokeWidth = mWidth/15;
                if(mCircleLineStrokeWidth==0){
                    mCircleLineStrokeWidth = 1;
                }

                if(drawCircle){
                    mRectF.left = getPaddingLeft() + (int)(mHeight*0.2) + mCircleLineStrokeWidth / 2; 
                    mRectF.top = getPaddingTop() + (int)(mHeight*0.2) + mCircleLineStrokeWidth / 2; 
                    mRectF.right = getPaddingLeft() + (int)(mHeight*0.8) - mCircleLineStrokeWidth / 2; 
                    mRectF.bottom = getPaddingTop() + (int)(mHeight*0.8) - mCircleLineStrokeWidth / 2;
                }else{
                    mWidth -= mCircleLineStrokeWidth*2;
                    mHeight -= mCircleLineStrokeWidth*2;
                    mRectF.left = getPaddingLeft() + mCircleLineStrokeWidth / 2; // 左上角x
                    mRectF.top = getPaddingTop() + mCircleLineStrokeWidth / 2; // 左上角y
                    mRectF.right = getPaddingLeft() + mWidth + mCircleLineStrokeWidth / 2; // 左下角x
                    mRectF.bottom = getPaddingTop() + mHeight + mCircleLineStrokeWidth / 2; // 右下角y
                }
            }
        });
    }

    private void invalidateTextPaintAndMeasurements() {
        
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawColor(Color.TRANSPARENT);

        if(drawCircle){
            mPaint.setColor(circleBgColor);
            mPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(getPaddingLeft()+((float)mWidth)/2,getPaddingTop()+((float)mWidth)/2,((float)mWidth)/2,mPaint);
        }

        mPaint.setStrokeWidth(mCircleLineStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);

        /*mPaint.setColor(Color.rgb(0xe9, 0xe9, 0xe9));
        canvas.drawArc(mRectF, -90, 360, false, mPaint);*/

        mPaint.setColor(strokeColor);
        canvas.drawArc(mRectF, startAngle, sweepAngle, false, mPaint);
    }

    public void startRotation(){
        if(isRotating){
            return;
        }
        isRotating = true;
        if(timer!=null){
            timer.cancel();
        }
        startAngle = -90;
        sweepAngle = 30;
        timer = new Timer();
        timerTask = new RedrawTimerTask(this);
        timer.schedule(timerTask,0,15);
    }

    public void stopRotation(){
        if(timer!=null){
            timer.cancel();
        }
        startAngle = -90;
        sweepAngle = 0;
        isRotating = false;
    }

    public void setProgress(float progress){
        if(progress<0 || progress>1){
            return;
        }
        if(isRotating){
            stopRotation();
        }
        startAngle = -90;
        sweepAngle = (int)(progress * 360);
        invalidate();
    }

    private class RedrawTimerTask extends TimerTask{

        private WeakReference<LoadingCircleView> v;

        private int direction = 0;
        private int startAngle = -90;
        private int sweepAngle = 30;

        public RedrawTimerTask(LoadingCircleView view){
            v = new WeakReference<LoadingCircleView>(view);
        }

        @Override
        public void run(){
            LoadingCircleView view = v.get();
            if(view == null){
                cancel();
                return;
            }
            startAngle = startAngle + 3 + direction * 6;
            sweepAngle = sweepAngle + 6 - direction * 12;

            if(sweepAngle == 30 && direction == 1){
                startAngle = startAngle%360;
                direction = 0;
                sweepAngle = -30;
            }else if(sweepAngle == 360){
                startAngle = startAngle%360;
                direction = 1;
                sweepAngle = 330;
            }

            int finalSweepAngle = sweepAngle;
            if(finalSweepAngle < 30){
                finalSweepAngle = 30;
            }else if(finalSweepAngle > 330){
                finalSweepAngle = 330;
            }
            view.handler.removeMessages(0);
            view.handler.sendMessage(Message.obtain(view.handler,0,startAngle,finalSweepAngle));
        }
    }

    private static class MyHandler extends Handler {

        private WeakReference<LoadingCircleView> v;

        MyHandler(LoadingCircleView view){
            v = new WeakReference<LoadingCircleView>(view);
        }

        @Override
        public void handleMessage(Message msg){
            LoadingCircleView view = v.get();
            if(view == null){
                return;
            }
            view.startAngle = msg.arg1;
            view.sweepAngle = msg.arg2;
            view.invalidate();
        }
    }
}

需要定义styleable

<declare-styleable name="LoadingCircleView">
    <attr name="strokeColor" format="color" /><!--圆弧颜色-->
    <attr name="drawCircle" format="boolean" /><!--是否在底层画一个圆-->
    <attr name="circleBgColor" format="color" /><!--是否圆的颜色-->
</declare-styleable>

Handle uncaught exception in Android app

  1. Handle uncaughtException in your Application subclass.
  2. After catching an exception, start a new activity to ask the user to send a log.
  3. Extract the log info from logcat's files and write to your own file.
  4. Start an email app, providing your file as an attachment.
  5. Manifest: filter your activity to be recognized by your exception handler.
  6. Optionally, setup Proguard to strip out Log.d() and Log.v().
public class MyApplication extends Application
{
  public void onCreate ()
  {
    // Setup handler for uncaught exceptions.
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  public void handleUncaughtException (Thread thread, Throwable e)
  {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
  }
}
private String extractLogToFile()
{
  PackageManager manager = this.getPackageManager();
  PackageInfo info = null;
  try {
    info = manager.getPackageInfo (this.getPackageName(), 0);
  } catch (NameNotFoundException e2) {
  }
  String model = Build.MODEL;
  if (!model.startsWith(Build.MANUFACTURER))
    model = Build.MANUFACTURER + " " + model;

  // Make file name - file must be saved to external storage or it wont be readable by
  // the email app.
  String path = Environment.getExternalStorageDirectory() + "/" + "MyApp/";
  String fullName = path + <some name>;

  // Extract to file.
  File file = new File (fullName);
  InputStreamReader reader = null;
  FileWriter writer = null;
  try
  {
    // For Android 4.0 and earlier, you will get all app's log output, so filter it to
    // mostly limit it to your app's output.  In later versions, the filtering isn't needed.
    String cmd = (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) ?
                  "logcat -d -v time MyApp:v dalvikvm:v System.err:v *:s" :
                  "logcat -d -v time";

    // get input stream
    Process process = Runtime.getRuntime().exec(cmd);
    reader = new InputStreamReader (process.getInputStream());

    // write output stream
    writer = new FileWriter (file);
    writer.write ("Android version: " +  Build.VERSION.SDK_INT + "\n");
    writer.write ("Device: " + model + "\n");
    writer.write ("App version: " + (info == null ? "(null)" : info.versionCode) + "\n");

    char[] buffer = new char[10000];
    do 
    {
      int n = reader.read (buffer, 0, buffer.length);
      if (n == -1)
        break;
      writer.write (buffer, 0, n);
    } while (true);

    reader.close();
    writer.close();
  }
  catch (IOException e)
  {
    if (writer != null)
      try {
        writer.close();
      } catch (IOException e1) {
      }
    if (reader != null)
      try {
        reader.close();
      } catch (IOException e1) {
      }

    // You might want to write a failure message to the log here.
    return null;
  }

  return fullName;
}
public class SendLog extends Activity implements OnClickListener
{
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    requestWindowFeature (Window.FEATURE_NO_TITLE); // make a dialog without a titlebar
    setFinishOnTouchOutside (false); // prevent users from dismissing the dialog by tapping outside
    setContentView (R.layout.send_log);
  }
}
<activity
    android:name="com.mydomain.SendLog"
    android:theme="@android:style/Theme.Dialog"
    android:textAppearance="@android:style/TextAppearance.Large"
    android:windowSoftInputMode="stateHidden">
    <intent-filter>
      <action android:name="com.mydomain.SEND_LOG" />
      <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int d(...);
}

Debug Release采用不同资源

From Steffen Funke

Just to give a working example to my comment above:

declare a resValue in your defaultConfig which will become the Application's name. (Attention: if you choose to name it app_name, be sure to delete the original app_name property from your strings.xml file, or it will clash.)

defaultConfig {
    // applicationId, versionCode, etc.

    (...)

    // define your base Applications name here
    resValue 'string', 'app_name', 'MyApp'
}

set your productFlavors as you did already. You can leave them empty if it is ok for you to concat your App's name with the flavor's name only, or provide an explicit resValue, which will override the value from defaultConfig.

productFlavors {
    dev {
        // resValue 'string', 'app_name', 'MyAppDevFlavor'
    }

    prod {
        // resValue 'string', 'app_name', 'MyAppProdFlavor'
    }
}

configure the resValue's name at gradle configuration time

android.applicationVariants.all { variant ->
    // get app_name field from defaultConfig
    def appName = variant.mergedFlavor.resValues.get('app_name').getValue()

    // concat new App name with each flavor's name
    appName = "${appName}"
    variant.productFlavors.each { flavor ->
        appName += " ${flavor.name}"
    }

    // optionally add buildType name
    appName += " ${variant.buildType.name}"

    // your requirement: if buildType == debug, add DEV to App name
    if(variant.buildType.name == "debug") {
        appName += " DEV"
    }

    // set new resVale
    variant.resValue 'string', 'app_name', appName
}

In your AndroidManifest, set the app_name field:

    <application
    ...
    android:label="@string/app_name"
    ...
    >

As I mentioned above, be sure to delete the default app_name property from strings.xml