自定義View高仿懂球帝我是教練效果

前言

這幾天不少歐洲球隊來中國進行熱身賽,不知道喜歡足球的各位小夥伴們有沒有看球。喜歡足球的朋友可能知道懂球帝APP,鄙人也常用這個應用,裏面有一個我是教練的功能挺好玩,就是能夠模擬教練員的身份,排兵佈陣;本着好奇心簡單模仿了一下,在這裏和你們分享。java

效果圖

老規矩,先上效果圖看看模仿的像不。android

add_player
add_player

move_player
move_player

玩過我是教練這個功能的小夥伴能夠對比一下。git

總的來講,這樣的一個效果,其實很簡單,就是一個view隨着手指在屏幕上移動的效果,外加一個圖片替換的動畫。但就是這些看似簡單的效果,在實現的過程當中也是遇到了不少坑,漲了許多新姿式。好了,廢話不說,代碼走起(。◕ˇ∀ˇ◕)。github

自定義View-BallGameView

整個內容中最核心的就是一個自定義View-BallGameView,就是屏幕中綠色背景,有氣泡和球員圖片的整個view。canvas

說到自定義View,老生常談,你們一直都在學習,卻永遠都以爲本身沒有學會,可是自定義View的知識原本就不少呀,想要熟練掌握,必須假以時日數組

既然是自定View就從你們最關心的兩個方法 onMeasure和onDraw 兩個方法提及。這裏因爲是純粹繼承自View,就不考慮onLayout的實現了。bash

測量-onMeasure

@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

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 裏的兩個重載方法。佈局

  • drawBitmap(Bitmap bitmap,Rect src,Rect dst,Paint paint)
/** * 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"的形式畫在了視圖大小的區域內。這樣,你應該理解這個重載方法的含義了。

  • drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
/**
     * 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關鍵步驟,以上兩點算是完成了繪製和佈局的工做,下面就看看觸摸反饋的實現。

觸摸反饋-onTouchEvent

這裏觸摸反饋機制,使用到了GestureDetector這個類;這個類能夠用來進行手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。內部提供了OnGestureListener、OnDoubleTapListener和OnContextClickListener三個接口,並提供了一系列的方法,好比常見的

  • onSingleTapUp : 手指輕觸屏幕離開
  • onScroll : 滑動
  • onLongPress: 長按
  • onFling: 按下後,快速滑動鬆開(相似切水果的手勢)
  • onDoubleTap : 雙擊

能夠看到,使用這個類能夠更加精確的處理手勢操做。

這裏引入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致使整個視圖從新繪製,這樣動畫完成時,氣泡就被替換爲真實的頭像了。

到這裏,基本上全部功能,都實現了。最後就是把本身排出來的陣型,保存爲圖片分享給小夥伴了。這裏主要說一下保存圖片的實現;分享功能,就不做爲重點討論了。

自定義View保存爲Bitmap

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_PICTURES):表明/storage/emulated/0/Android/data/{packagname}/files/Pictures
  • mContext.getExternalCacheDir() 表明 /storage/emulated/0/Android/data/{packagname}/cache

對於mContext.getExternalFilesDir還可定義爲Environment.DIRECTORY_DOWNLOADS,Environment.DIRECTORY_DOCUMENTS等目錄,對應的文件夾名稱也會變化。

這個目錄中的內容會隨着用戶卸載應用,一併刪除。最重要的是,讀寫這個目錄是不須要權限的,所以省去了每次作權限判斷的麻煩,並且也避免了沒有權限時的窘境

到這裏,模仿功能,所有都實現了。下面稍微來一點額外的擴展。

咱們但願圖片保存後能夠在通知欄提示用戶,點擊通知欄後能夠經過手機相冊查看保存的圖片。

擴展-Android Notification & FileProvider 的使用

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。

相關文章
相關標籤/搜索