Android 拍照或相册中选择图片编辑功能(仿微信拍照或相册选择照片编辑功能)

最终效果图:
这里写图片描述

实现方案:自定义一个父容器RelativeLayout将ImageView放入父容器中并初始化一个和ImageView相同大小的DrawingView来做涂鸦层最后将ImageView和DrawingView重叠部分生成Bitmap。

父容器代码PhotoEditorView:

public class PhotoEditorView extends RelativeLayout {

    private ImageView mImgSource;
    private BrushDrawingView mBrushDrawingView;
    private static final int imgSrcId = 1, brushSrcId = 2;

    public PhotoEditorView(Context context) {
        super(context);
        init();
    }

    public PhotoEditorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PhotoEditorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public PhotoEditorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        //Setup image attributes
        mImgSource = new ImageView(getContext());
        mImgSource.setId(imgSrcId);
        mImgSource.setAdjustViewBounds(true);
        LayoutParams imgSrcParam = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        imgSrcParam.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
        //Setup brush view
        mBrushDrawingView = new BrushDrawingView(getContext());
        mBrushDrawingView.setVisibility(GONE);
        mBrushDrawingView.setId(brushSrcId);
        //Align brush to the size of image view
        LayoutParams brushParam = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        brushParam.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
        brushParam.addRule(RelativeLayout.ALIGN_TOP, imgSrcId);
        brushParam.addRule(RelativeLayout.ALIGN_BOTTOM, imgSrcId);


        //Add image source
        addView(mImgSource, imgSrcParam);
        //Add brush view
        addView(mBrushDrawingView, brushParam);
    }

    /**
     * Source image which you want to edit
     *
     * @return source ImageView
     */
    public ImageView getImageView() {
        return mImgSource;
    }

    BrushDrawingView getBrushDrawingView() {
        return mBrushDrawingView;
    }

    /**
     * 返回最终Bitmap
     *
     * @return
     */
    public Bitmap getResultBitmap() {
        BitmapDrawable drawable = (BitmapDrawable) mImgSource.getDrawable();
        Bitmap imageViewBitmap = drawable.getBitmap();
        RectF clipRect = new RectF();
        clipRect.top = mImgSource.getY();
        clipRect.left = mImgSource.getX();
        clipRect.bottom = mImgSource.getHeight();
        clipRect.right = mImgSource.getWidth();
        PointF srcSize = new PointF();
        srcSize.x = imageViewBitmap.getWidth();
        srcSize.y = imageViewBitmap.getHeight();
        Bitmap bitmap = mBrushDrawingView.getBrushResultImage(clipRect, srcSize);
        Bitmap resultBitmap = Bitmap.createBitmap(imageViewBitmap.getWidth(), imageViewBitmap.getHeight(), Bitmap.Config
                .ARGB_4444);
        Canvas canvas = new Canvas(resultBitmap);
        canvas.drawBitmap(imageViewBitmap, 0, 0, null);
        canvas.drawBitmap(bitmap, 0, 0, null);
        return resultBitmap;
    }
}

自定义涂鸦path View代码:

public class BrushDrawingView extends View {

    private float mBrushSize = 15;
    private int mOpacity = 255;

    private List<LinePath> mLinePaths = new ArrayList<>();
    private Paint mDrawPaint;

    private Canvas mDrawCanvas;
    private boolean mBrushDrawMode;

    private Path mPath;
    private float mTouchX, mTouchY;
    private static final float TOUCH_TOLERANCE = 4;


    public BrushDrawingView(Context context) {
        this(context, null);
    }

    public BrushDrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupBrushDrawing();
    }

    public BrushDrawingView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupBrushDrawing();
    }

    void setupBrushDrawing() {
        //Caution: This line is to disable hardware acceleration to make eraser feature work properly
        setLayerType(LAYER_TYPE_HARDWARE, null);
        mDrawPaint = new Paint();
        mPath = new Path();
        mDrawPaint.setAntiAlias(true);
        mDrawPaint.setDither(true);
        mDrawPaint.setColor(Color.RED);
        mDrawPaint.setStyle(Paint.Style.STROKE);
        mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
        mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
        mDrawPaint.setStrokeWidth(mBrushSize);
        mDrawPaint.setAlpha(mOpacity);
        mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
        this.setVisibility(View.GONE);
    }

    private void refreshBrushDrawing() {
        mBrushDrawMode = true;
        mPath = new Path();
        mDrawPaint.setAntiAlias(true);
        mDrawPaint.setDither(true);
        mDrawPaint.setStyle(Paint.Style.STROKE);
        mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
        mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
        mDrawPaint.setStrokeWidth(mBrushSize);
        mDrawPaint.setAlpha(mOpacity);
        mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
    }

    void setBrushDrawingMode(boolean brushDrawMode) {
        this.mBrushDrawMode = brushDrawMode;
        if (brushDrawMode) {
            this.setVisibility(View.VISIBLE);
            refreshBrushDrawing();
        }
    }

    void setOpacity(@IntRange(from = 0, to = 255) int opacity) {
        this.mOpacity = opacity;
        setBrushDrawingMode(true);
    }

    boolean getBrushDrawingMode() {
        return mBrushDrawMode;
    }

    boolean isCacheEmpty() {
        return mLinePaths.isEmpty();
    }

    void setBrushSize(float size) {
        mBrushSize = size;
        setBrushDrawingMode(true);
    }

    void setBrushColor(@ColorInt int color) {
        mDrawPaint.setColor(color);
        setBrushDrawingMode(true);
    }

    float getBrushSize() {
        return mBrushSize;
    }

    int getBrushColor() {
        return mDrawPaint.getColor();
    }

    void clearAll() {
        mLinePaths.clear();
        if (mDrawCanvas != null) {
            mDrawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        invalidate();
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0 && h > 0) {
            Bitmap canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
            mDrawCanvas = new Canvas(canvasBitmap);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (LinePath linePath : mLinePaths) {
            canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
        }
        canvas.drawPath(mPath, mDrawPaint);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (mBrushDrawMode) {
            float touchX = event.getX();
            float touchY = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    touchStart(touchX, touchY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    touchMove(touchX, touchY);
                    break;
                case MotionEvent.ACTION_UP:
                    touchUp();
                    break;
            }
            invalidate();
            return true;
        } else {
            return false;
        }
    }

    public Bitmap getBrushResultImage(RectF clipRect, PointF srcSize) {
        Bitmap drawBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
        Canvas canvas = new Canvas(drawBitmap);
        canvas.drawColor(Color.TRANSPARENT);
        for (int i = 0; i < mLinePaths.size(); i++) {
            LinePath linePath = mLinePaths.get(i);
            canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
        }
        Bitmap clipBitmap = Bitmap.createBitmap(drawBitmap, 0, 0, (int) clipRect.right, (int) clipRect.bottom, null,
                false);
        Bitmap resultBitmap = Bitmap.createScaledBitmap(clipBitmap, (int) srcSize.x, (int) srcSize.y, true);
        drawBitmap.recycle();
        clipBitmap.recycle();
        return resultBitmap;
    }


    private class LinePath {
        private Paint mDrawPaint;
        private Path mDrawPath;

        LinePath(Path drawPath, Paint drawPaints) {
            mDrawPaint = new Paint(drawPaints);
            mDrawPath = new Path(drawPath);
        }

        Paint getDrawPaint() {
            return mDrawPaint;
        }

        Path getDrawPath() {
            return mDrawPath;
        }
    }

    boolean undo() {
        if (mLinePaths.size() > 0) {
            mLinePaths.remove(mLinePaths.size() - 1);
            invalidate();
        }
        return mLinePaths.size() != 0;
    }


    private void touchStart(float x, float y) {
        mPath.reset();
        mPath.moveTo(x, y);
        mTouchX = x;
        mTouchY = y;
    }

    private void touchMove(float x, float y) {
        float dx = Math.abs(x - mTouchX);
        float dy = Math.abs(y - mTouchY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mTouchX, mTouchY, (x + mTouchX) / 2, (y + mTouchY) / 2);
            mTouchX = x;
            mTouchY = y;
        }
    }

    private void touchUp() {
        mPath.lineTo(mTouchX, mTouchY);
        // Commit the path to our offscreen
        mDrawCanvas.drawPath(mPath, mDrawPaint);
        // kill this so we don't double draw
        mLinePaths.add(new LinePath(mPath, mDrawPaint));
        mPath = new Path();
    }
}

为了方便使用实现部分方法封装构造器PhotoEditor:

public class PhotoEditor {

    private static final String TAG = PhotoEditor.class.getSimpleName();
    private PhotoEditorView parentView;
    private BrushDrawingView brushDrawingView;
    private Context context;

    private PhotoEditor(Builder builder) {
        this.context = builder.context;
        this.parentView = builder.parentView;
        this.brushDrawingView = builder.brushDrawingView;
    }

    public void setBrushDrawingMode(boolean brushDrawingMode) {
        if (brushDrawingView != null) {
            brushDrawingView.setBrushDrawingMode(brushDrawingMode);
        }
    }

    public Boolean getBrushDrawableMode() {
        return brushDrawingView != null && brushDrawingView.getBrushDrawingMode();
    }

    public void setBrushSize(float size) {
        if (brushDrawingView != null) {
            brushDrawingView.setBrushSize(size);
        }
    }

    public void setOpacity(@IntRange(from = 0, to = 100) int opacity) {
        if (brushDrawingView != null) {
            opacity = (int) ((opacity / 100.0) * 255.0);
            brushDrawingView.setOpacity(opacity);
        }
    }


    public void setPaintColor(@ColorInt int color) {
        if (brushDrawingView != null) {
            brushDrawingView.setBrushColor(color);
        }
    }

    public boolean undo() {
        return brushDrawingView != null && brushDrawingView.undo();
    }

    public void clearBrushAllViews() {
        if (brushDrawingView != null) {
            brushDrawingView.clearAll();
        }
    }

    public interface OnSaveListener {
        void onStart();

        void onSuccess(String imagePath);

        void onFailure(Boolean success);
    }

    @SuppressLint("StaticFieldLeak")
    @RequiresPermission(allOf = {Manifest.permission.WRITE_EXTERNAL_STORAGE})
    public void saveImage(@NonNull final String imagePath, @NonNull final OnSaveListener onSaveListener) {
        new AsyncTask<String, String, Boolean>() {

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                onSaveListener.onStart();
            }

            @SuppressLint("MissingPermission")
            @Override
            protected Boolean doInBackground(String... strings) {
                // Create a media file name
                File file = new File(imagePath);
                try {
                    FileOutputStream out = new FileOutputStream(file, false);
                    if (parentView != null) {
                        Bitmap drawingCache = parentView.getResultBitmap();
                        drawingCache.compress(Bitmap.CompressFormat.JPEG, 100, out);
                    }
                    out.flush();
                    out.close();
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                    onSaveListener.onFailure(false);
                    return false;
                }
            }

            @Override
            protected void onPostExecute(Boolean success) {
                super.onPostExecute(success);
                if (success) {
                    clearBrushAllViews();
                    onSaveListener.onSuccess(imagePath);
                } else {
                    onSaveListener.onFailure(success);
                }
            }

        }.execute();
    }

    /**
     * Check if any changes made need to save
     *
     * @return true is nothing is there to change
     */
    public boolean isCacheEmpty() {
        if (brushDrawingView != null) {
            return brushDrawingView.isCacheEmpty();
        } else {
            return true;
        }
    }


    public static class Builder {

        private Context context;
        private PhotoEditorView parentView;
        private BrushDrawingView brushDrawingView;

        public Builder(Context context, PhotoEditorView photoEditorView) {
            this.context = context;
            parentView = photoEditorView;
            brushDrawingView = photoEditorView.getBrushDrawingView();
        }

        public PhotoEditor build() {
            return new PhotoEditor(this);
        }
    }

}

使用:

public class PhotoEditorActivity extends AppCompatActivity {
    public static final String TYPE = "type";
    public static final int TYPE_ALBUM = 1;
    public static final int TYPE_OTHER = 2;
    public static final String EXTRA_EDITOR_MEDIA = "extra_editor_media";
    public static final String RESULT_EDITOR_MEDIA = "result_editor_media";
    public static final String EXTRA_IMAGE_PATH = "extra_image_path";
    public static final String RESULT_IMAGE_PATH = "result_image_path";
    public static final int REQUESTCODE = 102;
    public static final int RESULTCODE = 100;
    private static String storagePath = "";
    private static final File parentPath = Environment.getExternalStorageDirectory();
    private static String EDITOR_PHOTO_NAME = "Editor";
    private Context mContext;
    private PhotoEditorView photoEditorView;
    private TextView btnCancel;
    private TextView btnComplete;
    private ColorRadioGroup crgColors;
    private ImageButton btnUndo;
    private PhotoEditor photoEditor;
    private String url;
    private String savePath;
    private ProgressDialog mProgressDialog;
    private Intent mIntent;
    private LocalMedia localMedial;
    private int type;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.photo_editor_activity);
        mContext = this;
        btnUndo = findViewById(R.id.btn_undo);
        crgColors = findViewById(R.id.crg_colors);
        btnComplete = findViewById(R.id.btn_complete);
        btnCancel = findViewById(R.id.btn_cancel);
        photoEditorView = findViewById(R.id.photo_editor_view);
        initWidget();
        bindListener();
        startInvoke();
    }

    public void initWidget() {
        photoEditor = new PhotoEditor.Builder(this, photoEditorView).build();
        photoEditor.setBrushDrawingMode(true);
        crgColors.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                photoEditor.setPaintColor(crgColors.getCheckColor());
            }
        });
        //编辑保存地址
        savePath = saveEditorPhotoJpgPath();
    }

    private void bindListener() {
        btnCancel.setOnClickListener(listener);
        btnUndo.setOnClickListener(listener);
        btnComplete.setOnClickListener(listener);
    }

    View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int i = v.getId();
            if (i == R.id.btn_cancel) {
                finish();
            } else if (i == R.id.btn_undo) {
                photoEditor.undo();
            } else if (i == R.id.btn_complete) {
                if (photoEditor.isCacheEmpty()) {
                    if (type == TYPE_OTHER) {
                        mIntent = new Intent();
                        mIntent.putExtra(RESULT_IMAGE_PATH, url);
                        setResult(RESULTCODE, mIntent);
                    }
                    finish();
                } else {
                    saveImage();
                }
            }
        }
    };

    public void startInvoke() {
        mIntent = getIntent();
        if (mIntent != null) {
            type = mIntent.getIntExtra(TYPE, 0);
            if (type == TYPE_ALBUM) {
                localMedial = mIntent.getParcelableExtra(EXTRA_EDITOR_MEDIA);
                if (localMedial != null) {
                    url = localMedial.getPath();
                }
            } else {
                url = mIntent.getStringExtra(EXTRA_IMAGE_PATH);
            }
            if (!TextUtils.isEmpty(url)) {
                Glide.with(this).asBitmap().load(url).into(photoEditorView.getImageView());
            } else {
                finish();
            }
        }
    }


    private String initPath() {
        if (storagePath.equals("")) {
            storagePath = parentPath.getAbsolutePath() + File.separator + EDITOR_PHOTO_NAME;
            File file = new File(storagePath);
            if (!file.exists()) {
                file.mkdir();
            }
        }
        return storagePath;
    }

    public String saveEditorPhotoJpgPath() {
        return initPath() + File.separator + "editor_" + System.currentTimeMillis() + ".jpg";
    }


    @SuppressLint("MissingPermission")
    private void saveImage() {
        photoEditor.saveImage(savePath, new PhotoEditor.OnSaveListener() {
            @Override
            public void onStart() {
                showLoading("正在处理中");
            }

            @Override
            public void onSuccess(String imagePath) {
                hideLoading();
                mIntent = new Intent();
                if (type == TYPE_ALBUM) {
                    localMedial.setEditor(true);
                    localMedial.setEditorPath(imagePath);
                    mIntent.putExtra(RESULT_EDITOR_MEDIA, localMedial);
                } else {
                    mIntent.putExtra(RESULT_IMAGE_PATH, imagePath);
                }
                setResult(RESULTCODE, mIntent);
                finish();
            }

            @Override
            public void onFailure(Boolean success) {
                hideLoading();
                Toast.makeText(mContext, "失败", Toast.LENGTH_LONG).show();
            }
        });
    }

    void showLoading(@NonNull String message) {
        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage(message);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        mProgressDialog.setCancelable(false);
        mProgressDialog.show();
    }

    void hideLoading() {
        if (mProgressDialog != null) {
            mProgressDialog.dismiss();
        }
    }
}

布局photo_editor_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/black_radio"
    >


    <com.luck.picture.lib.editor.PhotoEditorView
        android:id="@+id/photo_editor_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"/>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentTop="true"
        >

        <TextView
            android:id="@+id/btn_cancel"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentStart="true"
            android:gravity="center"
            android:paddingEnd="@dimen/photo_editor_text_padding"
            android:paddingStart="@dimen/photo_editor_text_padding"
            android:text="取消"
            android:textColor="@color/white_radio"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="编辑图片"
            android:textColor="@color/white_radio"/>

        <TextView
            android:id="@+id/btn_complete"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:gravity="center"
            android:paddingEnd="@dimen/photo_editor_text_padding"
            android:paddingStart="@dimen/photo_editor_text_padding"
            android:text="完成"
            android:textColor="@color/white_radio"
            />

    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <com.luck.picture.lib.editor.ColorRadioGroup
            android:id="@+id/crg_colors"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginStart="@dimen/radio_margin"
            android:layout_weight="1"
            android:checkedButton="@+id/rb_red"
            android:orientation="horizontal">

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_black"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/black_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_red"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/red_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_yellow"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/yellow_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_blue"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/blue_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_green"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/green_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_orange"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/orange_radio"/>

            <com.luck.picture.lib.editor.ColorRadioButton
                android:id="@+id/rb_white"
                android:layout_width="@dimen/radio_color"
                android:layout_height="@dimen/radio_color"
                android:layout_margin="@dimen/radio_color_margin"
                android:button="@null"
                app:radio_color="@color/white_radio"/>
        </com.luck.picture.lib.editor.ColorRadioGroup>

        <ImageButton
            android:id="@+id/btn_undo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="@dimen/radio_margin"
            android:background="@null"
            android:src="@drawable/selector_btn_undo"/>
    </LinearLayout>
</RelativeLayout>

完整代码已经合并到这里写链接内容com.luck.picture.lib.editor目录下调用activity为com.luck.picture.lib.PhotoEditorActivity

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页