這幾天不少歐洲球隊來中國進行熱身賽,不知道喜歡足球的各位小夥伴們有沒有看球。喜歡足球的朋友可能知道懂球帝APP,鄙人也常用這個應用,裏面有一個我是教練的功能挺好玩,就是能夠模擬教練員的身份,排兵佈陣;本着好奇心簡單模仿了一下,在這裏和你們分享。java
老規矩,先上效果圖看看模仿的像不。android
玩過我是教練這個功能的小夥伴能夠對比一下。git
總的來講,這樣的一個效果,其實很簡單,就是一個view隨着手指在屏幕上移動的效果,外加一個圖片替換的動畫。但就是這些看似簡單的效果,在實現的過程當中也是遇到了不少坑,漲了許多新姿式。好了,廢話不說,代碼走起(。◕ˇ∀ˇ◕)。github
整個內容中最核心的就是一個自定義View-BallGameView,就是屏幕中綠色背景,有氣泡和球員圖片的整個view。canvas
說到自定義View,老生常談,你們一直都在學習,卻永遠都以爲本身沒有學會,可是自定義View的知識原本就不少呀,想要熟練掌握,必須假以時日。數組
既然是自定View就從你們最關心的兩個方法 onMeasure和onDraw 兩個方法提及。這裏因爲是純粹繼承自View,就不考慮onLayout的實現了。bash
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int viewW = screenW;
int viewH = (int) (screenW * 1.3);
setMeasuredDimension(viewW, viewH);
}複製代碼
這裏onMeasure()方法的實現很簡單,簡單的用屏幕的寬度規定了整個View 的寬高;至於1.3這個倍數,徹底一個估算值,沒必要深究。app
onDraw()方法是整個View中最核心的方法。ide
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪製背景
canvas.drawBitmap(backgroundBitmap, bitmapRect, mViewRect, mPaint);
//繪製提示文字透明背景
canvas.drawRoundRect(mRoundRect, 8, 8, mRectPaint);
//繪製底部提示文字 ( TextPiant 文字垂直居中實現 http://blog.csdn.net/hursing/article/details/18703599)
Paint.FontMetricsInt fontMetrics = mTipPaint.getFontMetricsInt();
float baseY=(mRoundRect.bottom+mRoundRect.top)/2-(fontMetrics.top+fontMetrics.bottom)/2;
canvas.drawText(tips, screenW / 2, baseY, mTipPaint);
//繪製初始的11個氣泡
for (int i = 0; i < players.length; i++) {
//繪製當前選中的球員
if (i == currentPos) {
if (players[i].isSetReal()) {
//繪製球員頭像
canvas.drawBitmap(players[i].getBitmap(), positions[i].x - playW / 2,
positions[i].y - playW / 2, mPaint);
//繪製選中球員金色底座
canvas.drawBitmap(playSelectedBitmap, positions[i].x - goldW / 2,
positions[i].y - goldH / 2, mPaint);
//繪製球員姓名
canvas.drawText(players[i].getName(), positions[i].x,
positions[i].y + playW, mTextPaint);
} else {
canvas.drawBitmap(selectedBitmap, positions[i].x - playW / 2,
positions[i].y - playW / 2, mPaint);
}
} else {
canvas.drawBitmap(players[i].getBitmap(), positions[i].x - playW / 2,
positions[i].y - playW / 2, mPaint);
if (players[i].isSetReal()) {
//繪製球員姓名
canvas.drawText(players[i].getName(), positions[i].x,
positions[i].y + playW, mTextPaint);
//繪製已設置正常圖片球員背景
canvas.drawBitmap(playeBgBitmap, positions[i].x - grayW / 2,
positions[i].y + 200, mPaint);
}
}
}
}複製代碼
能夠看到,在onDraw方法裏,咱們主要使用了canvas.drawBitmap 方法,繪製了不少圖片。下面就簡單瞭解一下canvas.drawBitmap 裏的兩個重載方法。佈局
/** * Draw the specified bitmap, scaling/translating automatically to fill * the destination rectangle. If the source rectangle is not null, it * specifies the subset of the bitmap to draw. * * * @param bitmap The bitmap to be drawn * @param src May be null. The subset of the bitmap to be drawn * @param dst The rectangle that the bitmap will be scaled/translated * to fit into * @param paint May be null. The paint used to draw the bitmap */
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst, @Nullable Paint paint) {
}複製代碼
drawBitmap(Bitmap bitmap,Rect src,Rect dst,Paint paint),這個重載方法主要是經過兩個Rectangle 決定了bitmap以怎樣的形式繪製出來。簡單來講,src 這個長方形決定了「截取」bitmap的大小,dst 決定了最終繪製出來時Bitmap應該佔有的大小。。就拿上面的代碼來講
backgroundBitmap = BitmapFactory.decodeResource(res, R.drawable.battle_bg);
//確保整張背景圖,都能完整的顯示出來
bitmapRect = new Rect(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
//目標區域,在整個視圖的大小中,繪製Bitmap
mViewRect = new Rect(0, 0, viewW, viewH);
//繪製背景
canvas.drawBitmap(backgroundBitmap, bitmapRect, mViewRect, mPaint);複製代碼
bitmapRect 是整個backgroundBitmap的大小,mViewRect也就是咱們在onMeasure裏規定的整個視圖的大小,這樣至關於把battle_bg這張圖片,以scaleType="fitXY"的形式畫在了視圖大小的區域內。這樣,你應該理解這個重載方法的含義了。
/**
* Draw the specified bitmap, with its top/left corner at (x,y), using
* the specified paint, transformed by the current matrix.
*
*
* @param bitmap The bitmap to be drawn
* @param left The position of the left side of the bitmap being drawn
* @param top The position of the top side of the bitmap being drawn
* @param paint The paint used to draw the bitmap (may be null)
*/
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {
}複製代碼
這個重載方法應該很容易理解了,left,top 規定了繪製Bitmap的左上角的座標,而後按照其大小正常繪製便可。
這裏咱們全部的氣泡(球員位置)都是使用這個方法繪製的。足球場上有11個球員,所以咱們經過數組預先定義了11個氣泡的初始位置,而後經過其座標位置,繪製他們。爲了繪製精確,須要減去每張圖片自身的寬高,這應該是很傳統的作法了。
同時,在以後的觸摸反饋機制中,咱們會根據手指的滑動,修改這些座標值,這樣就能夠隨意移動球員在場上的位置了;具體實現,結合代碼中的註釋應該很容易理解了,就再也不贅述;能夠查看完整源碼BallGameView。
這裏再說一個在繪製過程當中遇到一個小問題,能夠看到在整個視圖底部,繪製了一個半透明的圓角矩形,並在他上面繪製了一行黃色的文字,這行文字在水平和垂直方向都是居中的;使用TextPaint 繪製文字實現水平居中是很容易的事情,只須要設置mTipPaint.setTextAlign(Paint.Align.CENTER)便可,可是在垂直方向實現居中,就沒那麼簡單了,這裏須要考慮一個文本繪製時基線的問題,具體細節能夠參考這篇文章,分析的很詳細。
咱們在這裏爲了使文字在圓角矩形中居中,以下實現。
canvas.drawRoundRect(mRoundRect, 8, 8, mRectPaint);
Paint.FontMetricsInt fontMetrics = mTipPaint.getFontMetricsInt();
float baseY = (mRoundRect.bottom + mRoundRect.top) / 2 - (fontMetrics.top + fontMetrics.bottom) / 2;
canvas.drawText(tips, screenW / 2, baseY, mTipPaint);複製代碼
圓角矩形的垂直中心點的基礎上,再一次作修正,確保實現真正的垂直居中。
好了,結合扔物線大神所總結的自定義View關鍵步驟,以上兩點算是完成了繪製和佈局的工做,下面就看看觸摸反饋的實現。
這裏觸摸反饋機制,使用到了GestureDetector這個類;這個類能夠用來進行手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。內部提供了OnGestureListener、OnDoubleTapListener和OnContextClickListener三個接口,並提供了一系列的方法,好比常見的
能夠看到,使用這個類能夠更加精確的處理手勢操做。
這裏引入GestureDetector的緣由是這樣的,單獨在onTouchEvent處理全部事件時,在手指點擊屏幕的瞬間,很容易觸發MotionEvent.ACTION_MOVE事件,致使每次觸碰氣泡,被點擊氣泡的位置都會稍微顫抖一下,位置發生輕微的偏移,體驗十分糟糕。採用GestureDetector對手指滑動的處理,對點擊和滑動的檢測顯得更加精確
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mValueAnimator != null) {
if (mValueAnimator.isRunning()) {
return false;
}
}
m_gestureDetector.onTouchEvent(event);
int lastX = (int) event.getX();
int lastY = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
for (int i = 0; i < positions.length; i++) {
int deltaX = positions[i].x - lastX;
int deltaY = positions[i].y - lastY;
// 手指 -- ACTION_DOWN 時,落在了某一個氣泡上時,刷新選中氣泡(球員)的bitmap
if (Math.abs(deltaX) < playW / 2 && Math.abs(deltaY) < playW / 2) {
position = i;
currentPos = i;
invalidate();
moveEnable = true;
Log.e(TAG, "onTouchEvent: position= " + position);
return true;
}
}
//沒有點擊中任意一個氣泡,點擊在外部是,重置氣泡(球員)狀態
resetBubbleView();
moveEnable = false;
return false;
}
return super.onTouchEvent(event);
}複製代碼
這裏m_gestureDetector.onTouchEvent(event),這樣就可讓GestureDetector在他本身的回調方法OnGestureListener裏,處理觸摸事件。
上面的邏輯很簡單,動畫正在進行是,直接返回。MotionEvent.ACTION_DOWN事件發生時的處理邏輯,經過註釋很容易理解,就再也不贅述。
當咱們點擊到某個氣泡時,就獲取到了當前選中位置currentPos;下面看看GestureDetector的回調方法,是怎樣處理滑動事件的。
GestureDetector.OnGestureListener onGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (moveEnable) {
positions[position].x -= distanceX;
positions[position].y -= distanceY;
//滑動時,考慮一下上下邊界的問題,不要把球員移除場外
// 橫向就不考慮了,由於底圖是3D 擺放的,上窄下寬,沒法計算
// 主要限制一下,縱向滑動值
if (positions[position].y < minY) {
positions[position].y = minY;
} else if (positions[position].y > maxY) {
positions[position].y = maxY;
}
Log.e(TAG, "onScroll: y=" + positions[position].y);
//跟隨手指,移動氣泡(球員)
invalidate();;
}
return true;
}
};複製代碼
SimpleOnGestureListener 默認實現了OnGestureListener,OnDoubleTapListener, OnContextClickListener這三個接口中全部的方法,所以很是方便咱們使用GestureDetector進行特定手勢的處理。
這裏的處理很簡單,當氣泡被選中時moveEnable=true,經過onScroll回調方法返回的距離,不斷更新當前位置的座標,同時記得限制一下手勢滑動的邊界,總不能把球員移動到場地外面吧o(╯□╰)o,最後的postInvalidate()是關鍵,觸發onDraw方法,實現從新繪製。
這裏有一個細節,不知你發現沒有,咱們在更新座標的時候,每次都是在當前座標的位置,減去了滑動距離(distanceX/distanceY)。這是爲何(⊙o⊙)?,爲何不是加呢?
咱們能夠看看這個回調方法的定義
/** * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the * current move {@link MotionEvent}. The distance in x and y is also supplied for * convenience. * * @param e1 The first down motion event that started the scrolling. * @param e2 The move motion event that triggered the current onScroll. * @param distanceX The distance along the X axis that has been scrolled since the last * call to onScroll. This is NOT the distance between {@code e1} * and {@code e2}. * @param distanceY The distance along the Y axis that has been scrolled since the last * call to onScroll. This is NOT the distance between {@code e1} * and {@code e2}. * @return true if the event is consumed, else false */
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);複製代碼
能夠看到,這裏特定強調了This is NOT the distance between {@code e1}and {@code e2},就是說這個距離並非兩次事件e1和e2 之間的距離。那麼這個距離又是什麼呢?那咱們就找一找究竟是在哪裏觸發了這個回調方法.
最終在GestureDetector類的onTouchEvent()方法裏找到了觸發這個方法發生的地方:
public boolean onTouchEvent(MotionEvent ev) {
.....
final boolean pointerUp =
(action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = ev.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i) continue;
sumX += ev.getX(i);
sumY += ev.getY(i);
}
final int div = pointerUp ? count - 1 : count;
final float focusX = sumX / div;
final float focusY = sumY / div;
boolean handled = false;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
if (mInLongPress || mInContextClick) {
break;
}
final float scrollX = mLastFocusX - focusX;
final float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
// Give the move events of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mAlwaysInTapRegion) {
final int deltaX = (int) (focusX - mDownFocusX);
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion = false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
if (distance > mDoubleTapTouchSlopSquare) {
mAlwaysInBiggerTapRegion = false;
}
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
break;
return handled;
}複製代碼
這裏還涉及到多指觸控的考慮,狀況較爲複雜;簡單說一下結論,在ACTION_MOVE時,會從上一次手指離開的距離,減去這次手指觸碰的位置;這樣當scrollX>0時,就是在向右滑動,反之向左;scrollY > 0 時,是在向上滑動,反之向下;所以,這兩個距離和咱們習覺得常的方向剛好都是相反的,所以,在更新座標時,須要作相反的處理。
有興趣的同窗,能夠把上面的「-」改爲「+」,嘗試運行一下代碼,就會明白其中的道理了。
好了,到了這裏按照繪製,佈局,觸摸反饋的順序咱們已經完成了BallGameView這個自定義View本身的內容了,可是咱們還看到在點擊下面的球員頭像時,還有一個簡單的動畫,下面就看看動畫是如何實現的。
首先說明一下,底部球員列表是一個橫向的RecyclerView,這樣一個橫向滑動的雙列展現的RecyclerView 應該很簡單了,這裏就再也不詳述。文末有源碼,最後能夠查看。
這裏看一下每個RecyclerView中item的點擊事件
@Override
public void onRVItemClick(ViewGroup parent, View itemView, int position) {
if (mPlayerBeanList.get(position).isSelected()) {
Toast.makeText(mContext, "球員已被選擇!", Toast.LENGTH_SHORT).show();
} else {
View avatar = itemView.findViewById(R.id.img);
int width = avatar.getWidth();
int height = avatar.getHeight();
Bitmap bitmap = Tools.View2Bitmap(avatar, width, height);
int[] location = new int[2];
itemView.getLocationOnScreen(location);
if (bitmap != null) {
mGameView.updatePlayer(bitmap, mPlayerBeanList.get(position).getName(), location, content);
}
}
}複製代碼
這裏能夠看到調用了GameView的updatePlayer方法:
/** * 在下方球員區域,選中球員後,根據位置執行動畫,將球員放置在選中的氣泡中 * * @param bitmap 被選中球員bitmap * @param name 被選中球員名字 * @param location 被選中球員在屏幕中位置 * @param contentView 根視圖(方便實現動畫) */
public void updatePlayer(final Bitmap bitmap, final String name, int[] location, final ViewGroup contentView) {
Path mPath = new Path();
mPath.moveTo(location[0] + bitmap.getWidth() / 2, location[1] - bitmap.getHeight() / 2);
mPath.lineTo(positions[currentPos].x - playW / 2, positions[currentPos].y - playW / 2);
final ImageView animImage = new ImageView(getContext());
animImage.setImageBitmap(bitmap);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(120, 120);
contentView.addView(animImage, params);
final float[] animPositions = new float[2];
final PathMeasure mPathMeasure = new PathMeasure(mPath, false);
mValueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
mPathMeasure.getPosTan(value, animPositions, null);
animImage.setTranslationX(animPositions[0]);
animImage.setTranslationY(animPositions[1]);
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
contentView.removeView(animImage);
players[currentPos].setBitmap(bitmap);
players[currentPos].setSetReal(true);
players[currentPos].setName(name);
invalidate();
}
});
mValueAnimator.setDuration(500);
mValueAnimator.setInterpolator(new AccelerateInterpolator());
mValueAnimator.start();
}複製代碼
這個動畫,簡單來講就是一個一階貝塞爾曲線。根據RecyclerView中item在屏幕中的位置,構造一個如出一轍的ImageView添加到根視圖中,而後經過一個屬性動畫,在屬性值不斷更新時,在回調方法中不斷調用setTranslation方法,改變這個ImageView的位置,呈現出動畫的效果。動畫結束後,將這個ImageView從視圖移除,同時氣泡中的數據便可,最後再次invalidate致使整個視圖從新繪製,這樣動畫完成時,氣泡就被替換爲真實的頭像了。
到這裏,基本上全部功能,都實現了。最後就是把本身排出來的陣型,保存爲圖片分享給小夥伴了。這裏主要說一下保存圖片的實現;分享功能,就不做爲重點討論了。
private class SavePicTask extends AsyncTask<Bitmap, Void, String> {
@Override
protected String doInBackground(Bitmap... params) {
Bitmap mBitmap = params[0];
String filePath = "";
Calendar now = new GregorianCalendar();
SimpleDateFormat simpleDate = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
String fileName = simpleDate.format(now.getTime());
//保存在應用內目錄,免去申請讀取權限的麻煩
File mFile = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName + ".jpg");
try {
OutputStream mOutputStream = new FileOutputStream(mFile);
mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, mOutputStream);
mOutputStream.flush();
mOutputStream.close();
filePath = mFile.getAbsolutePath();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return filePath;
}
}複製代碼
mGameView.setDrawingCacheEnabled(true);
Bitmap mBitmap = mGameView.getDrawingCache();
if (mBitmap != null) {
new SavePicTask().execute(mBitmap);
} else {
Toast.makeText(mContext, "fail", Toast.LENGTH_SHORT).show();
}複製代碼
一個典型的AsyncTask實現,文件流的輸出,沒什麼多說的。主要是存儲目錄的選擇,這裏有個技巧,若是沒有特殊限制,平時咱們作開發的時候,能夠 把一些存儲路徑作以下定義
對於mContext.getExternalFilesDir還可定義爲Environment.DIRECTORY_DOWNLOADS,Environment.DIRECTORY_DOCUMENTS等目錄,對應的文件夾名稱也會變化。
這個目錄中的內容會隨着用戶卸載應用,一併刪除。最重要的是,讀寫這個目錄是不須要權限的,所以省去了每次作權限判斷的麻煩,並且也避免了沒有權限時的窘境。
到這裏,模仿功能,所有都實現了。下面稍微來一點額外的擴展。
咱們但願圖片保存後能夠在通知欄提示用戶,點擊通知欄後能夠經過手機相冊查看保存的圖片。
private void SaveAndNotify() {
if (!TextUtils.isEmpty(picUrl)) {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext);
mBuilder.setWhen(System.currentTimeMillis())
.setTicker("下載圖片成功")
.setContentTitle("點擊查看")
.setSmallIcon(R.mipmap.app_start)
.setContentText("圖片保存在:" + picUrl)
.setAutoCancel(true)
.setOngoing(false);
//通知默認的聲音 震動 呼吸燈
mBuilder.setDefaults(NotificationCompat.DEFAULT_ALL);
Intent mIntent = new Intent();
mIntent.setAction(Intent.ACTION_VIEW);
Uri contentUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 將文件轉換成content://Uri的形式
contentUri = FileProvider.getUriForFile(mContext, getPackageName() + ".provider", new File(picUrl));
// 申請臨時訪問權限
mIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
contentUri = Uri.fromFile(new File(picUrl));
}
mIntent.setDataAndType(contentUri, "image/*");
PendingIntent mPendingIntent = PendingIntent.getActivity(mContext
, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(mPendingIntent);
Notification mNotification = mBuilder.build();
mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mManager.notify(0, mNotification);
} else {
T.showSToast(mContext, "圖片保存失敗");
}
}複製代碼
Android 系統中的通知欄,隨着版本的升級,已經造成了固定了寫法,在Builder模式的基礎上,經過鏈式寫法,能夠很是方便的設置各類屬性。這裏重點說一下PendingIntent的用法,咱們知道這個PendingIntent 顧名思義,就是處於Pending狀態,當咱們點擊通知欄,就會觸發他所包含的Intent。
嚴格來講,經過本身的應用想用手機自帶相冊打開一張圖片是沒法實現的,由於沒法保證每一種手機上面相冊的包名是同樣的,所以這裏咱們建立ACTION=Intent.ACTION_VIEW的 Intent,去匹配系統全部符合這個Action 的Activity,系統相冊必定是其中之一。
到這裏,還有必定須要注意,Android 7.0 開始,沒法以file://xxxx 形式向外部應用提供內容了,所以須要考慮使用FileProvider。固然,對這個問題,Google官方提供了完整的使用實例,實現起來都是套路,沒有什麼特別之處。
重點記住下面的對應關係便可:
<root-path/> 表明設備的根目錄new File("/");
<files-path/> 表明context.getFilesDir()
<cache-path/> 表明context.getCacheDir()
<external-path/> 表明Environment.getExternalStorageDirectory()
<external-files-path>表明context.getExternalFilesDirs()
<external-cache-path>表明getExternalCacheDirs()複製代碼
按照上面,咱們存儲圖片的目錄,咱們在file_path.xml 作以下定義便可:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="root" path=""/>
</paths>複製代碼
在AndroidManifest中完成以下配置 :
<!-- Android 7.0 FileUriExposedException -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path"/>
</provider>複製代碼
這樣,當Build.VERSION.SDK_INT大於等於24及Android7.0時,能夠安心的使用FileProvider來和外部應用共享文件了。
好了,從一個簡單的自定義View 出發,又牽出了一大堆周邊的內容。好在,總算完整的說完了。
特別申明
以上代碼中所用到的圖片資源,所有源自懂球帝APP內;此處對應用解包,只是本着學習的目的,沒有其餘任何用意。
源碼地址: Github-AndroidAnimationExercise。
有興趣的同窗歡迎 star & fork。