في هذه المدونة، سنستعرض فئة

Home PDF

تمت كتابة هذه المدونة بمساعدة ChatGPT-4o.


مقدمة

في هذه المدونة، سنستعرض فئة DrawActivity، وهي مثال شامل لتنفيذ واجهة رسم مخصصة في تطبيقات Android. سنقوم بتفكيك كل مكون والخوارزميات المستخدمة، مع شرح مفصل لكيفية عملها معًا لتحقيق الوظائف المطلوبة.


جدول المحتويات

نظرة عامة على DrawActivity
تهيئة Activity
معالجة عمليات الصورة
إدارة Fragment
معالجة الأحداث
ميزات التراجع والإعادة
تخصيص DrawView
إدارة التاريخ
الخلاصة


نظرة عامة على DrawActivity

DrawActivity هي فئة رئيسية في تطبيق الرسم الذي نقوم بتطويره. هذه الفئة مسؤولة عن إدارة واجهة المستخدم الخاصة بالرسم وتوفير الأدوات اللازمة للمستخدم لإنشاء وتعديل الرسومات. في هذا القسم، سنلقي نظرة عامة على المكونات الرئيسية لوظيفة DrawActivity وكيفية تفاعلها مع المستخدم.

المكونات الرئيسية

  1. Canvas (اللوحة):
    • هذه هي المنطقة التي يتم فيها رسم الأشكال والخطوط. يتم تمثيلها بواسطة كائن Canvas في الكود.
    • يمكن للمستخدم التفاعل مع اللوحة باستخدام اللمس أو الفأرة لإنشاء رسومات.
  2. الأدوات (Tools):
    • يتضمن التطبيق مجموعة من الأدوات التي تسمح للمستخدم برسم أشكال مختلفة مثل الخطوط والدوائر والمستطيلات.
    • يتم تمثيل كل أداة بواسطة فئة خاصة بها، مثل LineTool، CircleTool، RectangleTool.
  3. الألوان والفرش (Colors and Brushes):
    • يمكن للمستخدم اختيار الألوان والفرش المختلفة لتخصيص الرسومات.
    • يتم إدارة الألوان والفرش عبر فئة Paint في Android.
  4. حفظ الرسومات (Saving Drawings):
    • يمكن للمستخدم حفظ الرسومات التي قام بإنشائها في صيغة ملفات مختلفة مثل PNG أو JPEG.
    • يتم تنفيذ هذه الوظيفة عبر مكتبات معالجة الصور في Android.

تفاعل المستخدم

مثال على الكود

public class DrawActivity extends AppCompatActivity {
    private CanvasView canvasView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_draw);

        canvasView = findViewById(R.id.canvas);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

    public void clearCanvas() {
        canvasView.clearCanvas();
    }

    public void saveDrawing() {
        Bitmap bitmap = canvasView.getBitmap();
        // Save bitmap to file or share it
    }
}

في هذا المثال، نرى كيف يتم تهيئة DrawActivity وإعداد اللوحة والأدوات. يتم تنفيذ وظائف مثل مسح اللوحة وحفظ الرسم عبر طرق محددة.

الخلاصة

DrawActivity هي الواجهة الرئيسية التي يتفاعل معها المستخدم لإنشاء الرسومات. من خلال فهم المكونات الرئيسية وكيفية تفاعلها، يمكننا تطوير تطبيق رسم قوي وسهل الاستخدام.

DrawActivity هي النشاط الرئيسي الذي يتعامل مع عمليات الرسم، اقتصاص الصور، والتفاعل مع المكونات الأخرى مثل الأجزاء (fragments) ورفع الصور. يوفر واجهة مستخدم تمكن المستخدم من الرسم، التراجع، إعادة التنفيذ، ومعالجة الصور.

public class DrawActivity extends Activity implements View.OnClickListener {
  // ثوابت لرموز الطلبات ومعرفات الأجزاء (fragments)
  public static final int CAMERA_RESULT = 1;
  public static final int CROP_RESULT = 2;
  public static final int DRAW_FRAGMENT = 0;
  public static final int RECOG_FRAGMENT = 1;
  public static final int RESULT_FRAGMENT = 2;
  public static final int WAIT_FRAGMENT = 3;
  public static final int MATERIAL_RESULT = 4;
  public static final String RESULT_JSON = "resultJson";
  public static final int INIT_FLOWER_ID = R.drawable.flower_b;
  public static final int LOGOUT = 0;
  public static final int IMAGE_RESULT = 0;
  
  // متغيرات للتعامل مع الصور وعمليات الرسم
  String baseUrl;
  DrawView drawView;
  Bitmap originImg;
  public static DrawActivity instance;
  View dir, clear, cameraView, materialView, scale;
  ImageView undoView, redoView;
  View upload;
  String cropPath;
  Tooltip toolTip;
  int curFragmentId = -1;
  int serverId = -1;
  private Bitmap resultBitmap;
  private RadioGroup radioGroup;
  Fragment curFragment;
  int curDrawMode;
  RadioButton drawBackBtn;
  private Activity cxt;
  Uri curPicUri;
}

تهيئة النشاط (Activity)

عند إنشاء نشاط جديد في تطبيق Android، يتم استدعاء دورة حياة النشاط (Activity Lifecycle) والتي تبدأ بمرحلة التهيئة. هذه المرحلة تُعرف بـ onCreate() وهي أول دالة يتم استدعاؤها عند إنشاء النشاط. في هذه الدالة، يتم تنفيذ المهام الأساسية مثل تهيئة واجهة المستخدم (UI) وتعيين المتغيرات الأولية.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main); // تعيين ملف التخطيط (layout) للنشاط

    // تهيئة المتغيرات والمكونات الأخرى هنا
}

في الكود أعلاه:

بعد تنفيذ onCreate()، يتم استدعاء الدوال الأخرى في دورة حياة النشاط مثل onStart() و onResume()، والتي تعني أن النشاط أصبح مرئيًا وجاهزًا للتفاعل مع المستخدم.

عند إنشاء Activity، يتم تنفيذ عمليات التهيئة المختلفة مثل إعداد العرض، تحميل الصور الأولية، وتهيئة مستمعي الأحداث.

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  instance = this;
  cxt = this;
  cropPath = PathUtils.getCropPath();
  setContentView(R.layout.draw_layout);
  findView();
  setSize();
  initOriginImage();
  toolTip = new Tooltip(this);
  initUndoRedoEnable();
  setIp();
  initDrawmode();
}

ملاحظة: الكود المقدم مكتوب بلغة Java وهو جزء من طريقة onCreate في تطبيق Android. لا يتم ترجمة أسماء الدوال أو المتغيرات أو الكود نفسه، حيث أن هذه الأسماء يجب أن تبقى كما هي لضمان عمل التطبيق بشكل صحيح.

findView()
تُستخدم هذه الطريقة لتهيئة العرض (View) المستخدم في النشاط (Activity).

private void findView() {
  drawView = findViewById(R.id.drawView);
  undoView = findViewById(R.id.undo);
  redoView = findViewById(R.id.redo);
  scale = findViewById(R.id.scale);
  upload = findViewById(R.id.upload);
  clear = findViewById(R.id.clear);
  dir = findViewById(R.id.dir);
  materialView = findViewById(R.id.material);
  cameraView = findViewById(R.id.camera);
dir.setOnClickListener(this);
materialView.setOnClickListener(this);
undoView.setOnClickListener(this);
scale.setOnClickListener(this);
redoView.setOnClickListener(this);
clear.setOnClickListener(this);
cameraView.setOnClickListener(this);
upload.setOnClickListener(this);
initRadio();
}

تم تعيين مستمعين للنقر (OnClickListener) للعناصر التالية:

ثم تم استدعاء الدالة initRadio() لتهيئة الراديو.

setSize()
تعيين حجم عرض الرسم.

private void setSize() {
  setSizeByResourceSize();
  setViewSize(drawView);
}

تمت ترجمة الكود أعلاه إلى:

private void setSize() {
  setSizeByResourceSize();
  setViewSize(drawView);
}

لا يوجد تغيير في الكود لأنه مكتوب بلغة Java، وهي لغة برمجة عالمية ولا يتم ترجمتها.

private void setSizeByResourceSize() {
  int width = getResources().getDimensionPixelSize(R.dimen.draw_width);
  int height = getResources().getDimensionPixelSize(R.dimen.draw_height);
  App.drawWidth = width;
  App.drawHeight = height;
}

تمت ترجمة الكود أعلاه إلى:

private void setSizeByResourceSize() {
  int width = getResources().getDimensionPixelSize(R.dimen.draw_width);
  int height = getResources().getDimensionPixelSize(R.dimen.draw_height);
  App.drawWidth = width;
  App.drawHeight = height;
}

في هذه الدالة، يتم تعيين حجم العرض والارتفاع بناءً على القيم المحددة في ملف الموارد (R.dimen.draw_width و R.dimen.draw_height)، ثم يتم تعيين هذه القيم في المتغيرات العامة App.drawWidth و App.drawHeight.

private void setViewSize(View v) {
  ViewGroup.LayoutParams lp = v.getLayoutParams();
  lp.width = App.drawWidth;
  lp.height = App.drawHeight;
  v.setLayoutParams(lp);
}

initOriginImage()
تحميل الصورة الأولية التي سيتم استخدامها للرسم.

private void initOriginImage() {
  Bitmap bitmap = BitmapFactory.decodeResource(getResources(), INIT_FLOWER_ID);
  String imgPath = PathUtils.getCameraPath();
  BitmapUtils.saveBitmapToPath(bitmap, imgPath);
  Uri uri1 = Uri.fromFile(new File(imgPath));
  setImageByUri(uri1);
}

ترجمة الكود إلى العربية:

private void initOriginImage() {
  // تحميل صورة من الموارد باستخدام معرف الصورة الأولية
  Bitmap bitmap = BitmapFactory.decodeResource(getResources(), INIT_FLOWER_ID);
  
  // الحصول على مسار حفظ الصورة من الكاميرا
  String imgPath = PathUtils.getCameraPath();
  
  // حفظ الصورة في المسار المحدد
  BitmapUtils.saveBitmapToPath(bitmap, imgPath);
  
  // إنشاء Uri من الملف المحفوظ
  Uri uri1 = Uri.fromFile(new File(imgPath));
  
  // تعيين الصورة باستخدام الـ Uri
  setImageByUri(uri1);
}

ملاحظة: الكود يبقى كما هو باللغة الإنجليزية لأن أسماء الدوال والمتغيرات لا تُترجم في البرمجة. الترجمة هنا هي للتوضيح فقط.


معالجة عمليات الصور

يقوم النشاط (Activity) بمعالجة عمليات الصور المختلفة، مثل تعيين الصور عبر URI، واقتصاص الصور، وحفظ الصور المرسومة كـ Bitmap.

setImageByUri(Uri uri)
يقوم بتحميل الصورة من الـ URI المحدد وإعدادها للرسم.

private void setImageByUri(final Uri uri) {
  new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
      curPicUri = uri;
      Bitmap bitmap = null;
      try {
        if (uri != null) {
          bitmap = BitmapUtils.getBitmapByUri(DrawActivity.this, uri);
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
private void setImageByUri(final Uri uri) {
  new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
      curPicUri = uri;
      Bitmap bitmap = null;
      try {
        if (uri != null) {
          bitmap = BitmapUtils.getBitmapByUri(DrawActivity.this, uri);
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
int originW = bitmap.getWidth();
int originH = bitmap.getHeight();
if (originW != App.drawWidth || originH != App.drawHeight) {
  float originRadio = originW * 1.0f / originH;
  float radio = App.drawWidth * 1.0f / App.drawHeight;
  if (Math.abs(originRadio - radio) < 0.01) {
    Bitmap originBm = bitmap;
    bitmap = Bitmap.createScaledBitmap(originBm, App.drawWidth, App.drawHeight, false);
    originBm.recycle();
  } else {
    cropIt(uri);
    return;
  }
}
ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.addOrReplaceToMemoryCache("origin", bitmap);
originImg = bitmap;
serverId = -1;

ترجمة الكود إلى العربية:

int originW = bitmap.getWidth();
int originH = bitmap.getHeight();
if (originW != App.drawWidth || originH != App.drawHeight) {
  float originRadio = originW * 1.0f / originH;
  float radio = App.drawWidth * 1.0f / App.drawHeight;
  if (Math.abs(originRadio - radio) < 0.01) {
    Bitmap originBm = bitmap;
    bitmap = Bitmap.createScaledBitmap(originBm, App.drawWidth, App.drawHeight, false);
    originBm.recycle();
  } else {
    cropIt(uri);
    return;
  }
}
ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.addOrReplaceToMemoryCache("origin", bitmap);
originImg = bitmap;
serverId = -1;

ملاحظة: تم الحفاظ على أسماء المتغيرات والوظائف كما هي لأنها أسماء تقنية ولا يتم ترجمتها عادةً.

drawView.setSrcBitmap(originImg);
showDrawFragment(App.ALL_INFO);
curDrawMode = App.DRAW_FORE;
}
}, 500);
}

تم تعيين الصورة الأصلية originImg كصورة مصدرية لعنصر الرسم drawView.
تم استدعاء الدالة showDrawFragment لعرض جزء الرسم مع جميع المعلومات App.ALL_INFO.
تم تعيين وضع الرسم الحالي curDrawMode إلى وضع الرسم الأمامي App.DRAW_FORE.
يتم تنفيذ هذه الأوامر بعد تأخير قدره 500 مللي ثانية.

cropIt(Uri uri)
بدء نشاط اقتصاص الصورة.

public void cropIt(Uri uri) {
  Crop.startPhotoCrop(this, uri, cropPath, CROP_RESULT);
}

saveBitmap()
يحفظ الصورة النقطية المرسومة إلى ملف ويقوم بتحميلها إلى الخادم.

public void saveBitmap() {
  Bitmap handBitmap = drawView.getHandBitmap();
  Bitmap originBitmap = drawView.getSrcBitmap();
  saveBitmapToFileAndUpload(handBitmap, originBitmap);
}

saveBitmapToFileAndUpload(Bitmap handBitmap, Bitmap originBitmap)
يحفظ الصورة النقطية في ملف ويقوم بتحميلها بشكل غير متزامن.

private void saveBitmapToFileAndUpload(Bitmap handBitmap, Bitmap originBitmap) {
  final String originPath = PathUtils.getOriginPath();
  BitmapUtils.saveBitmapToPath(originBitmap, originPath);
  final String handPath = PathUtils.getHandPath();
  BitmapUtils.saveBitmapToPath(handBitmap, handPath);
  new AsyncTask<Void, Void, Void>() {
    boolean res;
    Bitmap foreBitmap;
    Bitmap backBitmap;
private void saveBitmapToFileAndUpload(Bitmap handBitmap, Bitmap originBitmap) {
  final String originPath = PathUtils.getOriginPath();
  BitmapUtils.saveBitmapToPath(originBitmap, originPath);
  final String handPath = PathUtils.getHandPath();
  BitmapUtils.saveBitmapToPath(handBitmap, handPath);
  new AsyncTask<Void, Void, Void>() {
    boolean res;
    Bitmap foreBitmap;
    Bitmap backBitmap;
@Override
protected void onPreExecute() {
  super.onPreExecute();
  showWaitFragment();
}

@Override
protected Void doInBackground(Void... params) {
  try {
    if (baseUrl == null) {
      throw new Exception("baseUrl is null");
    }
    String jsonRes = UploadImage.upload(baseUrl, serverId, Web.STATUS_CONTINUE, originPath, handPath, null, false);
    getJsonData(jsonRes);
    res = true;
  } catch (Exception e) {
    res = false;
    e.printStackTrace();
  }
  return null;
}
private void getJsonData(String jsonRes) throws Exception {
    JSONObject json = new JSONObject(jsonRes);
    if (serverId == -1) {
        serverId = json.getInt(Web.ID);
    }
    String foreUrl = json.getString(Web.FORE);
    String backUrl = json.getString(Web.BACK);
    String resultUrl = json.getString(Web.RESULT);
    foreBitmap = Web.getBitmapFromUrlByStream1(foreUrl, 0);
    backBitmap = Web.getBitmapFromUrlByStream1(backUrl, 0);
    resultBitmap = Web.getBitmapFromUrlByStream1(resultUrl, 0);
}
@Override
protected void onPostExecute(Void aVoid) {
  super.onPostExecute(aVoid);
  if (res) {
    showRecogFragment(foreBitmap, backBitmap);
  } else {
    Utils.toast(DrawActivity.this, R.string.server_error);
    recogNo();
  }
}
  }.execute();
}

إدارة Fragment

تدير Activity إدارة fragment المختلفة للتعامل مع الحالات المختلفة للتطبيق، مثل الرسم والتعرف والانتظار.

showDrawFragment(int infoId)
يعرض جزء الرسم (fragment).

private void showDrawFragment(int infoId) {
  curFragmentId = DRAW_FRAGMENT;
  curFragment = new DrawFragment(infoId);
  showFragment(curFragment);
}

الترجمة:

private void showDrawFragment(int infoId) {
  curFragmentId = DRAW_FRAGMENT;
  curFragment = new DrawFragment(infoId);
  showFragment(curFragment);
}

في الكود أعلاه، يتم تعريف دالة خاصة تُسمى showDrawFragment تأخذ معاملًا من النوع int يُسمى infoId. داخل الدالة، يتم تعيين قيمة DRAW_FRAGMENT للمتغير curFragmentId، ثم يتم إنشاء كائن جديد من النوع DrawFragment باستخدام infoId كمعامل ويتم تعيينه للمتغير curFragment. أخيرًا، يتم استدعاء دالة showFragment مع تمرير الكائن curFragment كمعامل.

showWaitFragment()
عرض جزء الانتظار (fragment).

private void showWaitFragment() {
  curFragmentId = WAIT_FRAGMENT;
  showFragment(new WaitFragment());
}

ترجمة:

private void showWaitFragment() {
  curFragmentId = WAIT_FRAGMENT;
  showFragment(new WaitFragment());
}

ملاحظة: الكود الموجود في الكتلة البرمجية لم يتم ترجمته لأنه يحتوي على أسماء متغيرات ودوال باللغة الإنجليزية، والتي عادةً ما تبقى كما هي في البرمجة.

showFragment(Fragment fragment)
استبدل الـ fragment الحالي بالـ fragment المحدد.

private void showFragment(Fragment fragment) {
  FragmentTransaction trans = getFragmentManager().beginTransaction();
  trans.replace(R.id.rightLayout, fragment);
  trans.commit();
}

ترجمة الكود إلى العربية:

private void showFragment(Fragment fragment) {
  // بدء عملية تحويل Fragment
  FragmentTransaction trans = getFragmentManager().beginTransaction();
  
  // استبدال الـ Fragment الحالي في الـ Layout المحدد بالـ Fragment الجديد
  trans.replace(R.id.rightLayout, fragment);
  
  // تنفيذ عملية التحويل
  trans.commit();
}

في هذا الكود، يتم استبدال الـ Fragment الحالي الموجود في الـ Layout المحدد (R.id.rightLayout) بآخر جديد (fragment). يتم ذلك عن طريق إنشاء كائن من نوع FragmentTransaction واستخدام الدالة replace لتنفيذ الاستبدال، ثم تأكيد العملية باستخدام commit.


معالجة الأحداث

تتعامل Activity مع مختلف تفاعلات المستخدم، مثل النقر على الأزرار واختيار القوائم.

onClick(View v)
التعامل مع أحداث النقر على واجهات العرض المختلفة.

@Override
public void onClick(View v) {
  int id = v.getId();
  if (id == R.id.drawOk) {
    if (drawView.isDrawFinish()) {
      saveBitmap();
    } else {
      Utils.alertDialog(this, R.string.please_draw_finish);
    }
  } else if (id == R.id.recogOk) {
    recogOk();
  } else if (id == R.id.recogNo) {
    recogNo();
  } else if (id == R.id.dir) {
    Utils.getGalleryPhoto(this, IMAGE_RESULT);
  } else if (id == R.id.clear) {
    clearEverything();
  } else if (id == R.id.undo) {
    drawView.undo();
  } else if (id == R.id.redo) {
    drawView.redo();
  } else if (id == R.id.camera) {
    Utils.takePhoto(cxt, CAMERA_RESULT);
  } else if (id == R.id.material) {
    goMaterial();
  } else if (id == R.id.upload) {
    com.lzw.commons.Utils.goActivity(cxt, PhotoActivity.class);
  } else if (id == R.id.scale) {
    cropIt(curPicUri);
  }
}

onActivityResult(int requestCode, int resultCode, Intent data)
معالجة نتائج الأنشطة الأخرى، مثل اختيار الصور أو اقتصاصها.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (resultCode != RESULT_CANCELED) {
    Uri uri;
    switch (requestCode) {
      case IMAGE_RESULT:
        if (data != null) {
          setImageByUri(data.getData());
        }
        break;
      case CAMERA_RESULT:
        setImageByUri(Utils.getCameraUri());
        break;
      case CROP_RESULT:
        uri = Uri.fromFile(new File(cropPath));
        setImageByUri(uri);
        break;
      case MATERIAL_RESULT:
        setImageByUri(data.getData());
    }
  }
}

ميزات التراجع والإعادة

يوفر Activity وظيفة التراجع والإعادة للعمليات الرسومية.

initUndoRedoEnable()
يقوم بتهيئة وظائف التراجع والإعادة من خلال تعيين دوال الاستدعاء (callbacks).

void initUndoRedoEnable() {
  drawView.history.setCallBack(new History.CallBack() {
    @Override
    public void onHistoryChanged() {
      setUndoRedoEnable();
      if (curFragmentId != DRAW_FRAGMENT) {
        showDrawFragment(curDrawMode);
      }
    }
  });
}

ترجمة الكود إلى العربية:

void initUndoRedoEnable() {
  drawView.history.setCallBack(new History.CallBack() {
    @Override
    public void onHistoryChanged() {
      setUndoRedoEnable();
      if (curFragmentId != DRAW_FRAGMENT) {
        showDrawFragment(curDrawMode);
      }
    }
  });
}

شرح الكود:

ملاحظة: تم الحفاظ على أسماء الدوال والمتغيرات كما هي لأنها أسماء برمجية ولا يتم ترجمتها.

void setUndoRedoEnable() {
  redoView.setEnabled(drawView.history.canRedo());
  undoView.setEnabled(drawView.history.canUndo());
}

تخصيص DrawView

في هذا المنشور، سنتعلم كيفية تخصيص DrawView لإنشاء واجهة مستخدم تفاعلية تسمح للمستخدمين بالرسم على الشاشة. سنستخدم Swift وUIKit لتحقيق ذلك.

الخطوة 1: إنشاء مشروع جديد

أولاً، قم بإنشاء مشروع جديد في Xcode باستخدام قالب “Single View App”. قم بتسمية المشروع كما تريد، واختر Swift كلغة برمجة.

الخطوة 2: إنشاء DrawView

قم بإنشاء ملف جديد باسم DrawView.swift وقم بتعريف فئة DrawView التي ترث من UIView.

import UIKit

class DrawView: UIView {
    var lines: [Line] = []
    var lastPoint: CGPoint!

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        lastPoint = touches.first!.location(in: self)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let newPoint = touches.first!.location(in: self)
        lines.append(Line(start: lastPoint, end: newPoint))
        lastPoint = newPoint
        self.setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(UIColor.black.cgColor)
        context?.setLineWidth(2)
        context?.setLineCap(.round)

        for line in lines {
            context?.move(to: line.start)
            context?.addLine(to: line.end)
            context?.strokePath()
        }
    }
}

struct Line {
    var start: CGPoint
    var end: CGPoint
}

الخطوة 3: إضافة DrawView إلى الواجهة

افتح Main.storyboard وقم بإضافة UIView إلى الواجهة. قم بتغيير الفئة الخاصة بهذه UIView إلى DrawView في قسم “Identity Inspector”.

الخطوة 4: تشغيل التطبيق

قم بتشغيل التطبيق على جهاز محاكاة أو جهاز فعلي. الآن، يمكنك الرسم على الشاشة عن طريق تحريك إصبعك على الشاشة.

الخطوة 5: تخصيص DrawView

يمكنك تخصيص DrawView بإضافة خصائص مثل تغيير لون الخط أو سمكه. على سبيل المثال، لإضافة خاصية تغيير لون الخط، يمكنك تعديل الكود كما يلي:

class DrawView: UIView {
    var lines: [Line] = []
    var lastPoint: CGPoint!
    var strokeColor: UIColor = .black
    var strokeWidth: CGFloat = 2

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        lastPoint = touches.first!.location(in: self)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let newPoint = touches.first!.location(in: self)
        lines.append(Line(start: lastPoint, end: newPoint))
        lastPoint = newPoint
        self.setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(strokeColor.cgColor)
        context?.setLineWidth(strokeWidth)
        context?.setLineCap(.round)

        for line in lines {
            context?.move(to: line.start)
            context?.addLine(to: line.end)
            context?.strokePath()
        }
    }
}

الآن، يمكنك تغيير لون الخط وسمكه عن طريق تعديل خصائص strokeColor و strokeWidth.

الخلاصة

في هذا المنشور، تعلمنا كيفية إنشاء DrawView مخصص يسمح للمستخدمين بالرسم على الشاشة. يمكنك توسيع هذه الوظيفة بإضافة المزيد من الخصائص والوظائف حسب احتياجاتك.

DrawView هو عرض مخصص يستخدم لمعالجة عمليات الرسم، أحداث اللمس، والتحجيم.

onTouchEvent(MotionEvent event)
معالجة أحداث اللمس للرسم والتحجيم.

@Override
public boolean onTouchEvent(MotionEvent event) {
  if (!scaleMode) {
    handleDrawTouchEvent(event);
  } else {
    handleScaleTouchEvent(event);
  }
  return true;
}

ترجمة:

@Override
public boolean onTouchEvent(MotionEvent event) {
  if (!scaleMode) {
    handleDrawTouchEvent(event);
  } else {
    handleScaleTouchEvent(event);
  }
  return true;
}

ملاحظة: الكود الموجود في الكتلة البرمجية لم يتم تغييره لأنه كود برمجي، والكود البرمجي لا يتم ترجمته.

private void handleDrawTouchEvent(MotionEvent event) {
  int action = event.getAction();
  float x = event.getX();
  float y = event.getY();
  if (action == MotionEvent.ACTION_DOWN) {
    path.moveTo(x, y);
  } else if (action == MotionEvent.ACTION_MOVE) {
    path.quadTo(preX, preY, x, y);
  } else if (action == MotionEvent.ACTION_UP) {
    Matrix matrix1 = new Matrix();
    matrix.invert(matrix1);
    path.transform(matrix1);
    paint.setStrokeWidth(strokeWidth * 1.0f / totalRatio);
    history.saveToStack(path, paint);
    cacheCanvas.drawPath(path, paint);
    paint.setStrokeWidth(strokeWidth);
    path.reset();
  }
  setPrev(event);
  invalidate();
}

الترجمة:

private void handleDrawTouchEvent(MotionEvent event) {
  int action = event.getAction();
  float x = event.getX();
  float y = event.getY();
  if (action == MotionEvent.ACTION_DOWN) {
    path.moveTo(x, y);
  } else if (action == MotionEvent.ACTION_MOVE) {
    path.quadTo(preX, preY, x, y);
  } else if (action == MotionEvent.ACTION_UP) {
    Matrix matrix1 = new Matrix();
    matrix.invert(matrix1);
    path.transform(matrix1);
    paint.setStrokeWidth(strokeWidth * 1.0f / totalRatio);
    history.saveToStack(path, paint);
    cacheCanvas.drawPath(path, paint);
    paint.setStrokeWidth(strokeWidth);
    path.reset();
  }
  setPrev(event);
  invalidate();
}

الشرح:

private void handleScaleTouchEvent(MotionEvent event) {
  switch (event.getActionMasked()) {
    case MotionEvent.ACTION_POINTER_DOWN:
      lastFingerDist = calFingerDistance(event);
      break;
    case MotionEvent.ACTION_MOVE:
      if (event.getPointerCount() == 1) {
        handleMove(event);
      } else if (event.getPointerCount() == 2) {
        handleZoom(event);
      }
      break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
      lastMoveX = -1;
      lastMoveY = -1;
      break;
    default:
      break;
  }
}

ترجمة الكود إلى العربية:

private void handleScaleTouchEvent(MotionEvent event) {
  switch (event.getActionMasked()) {
    case MotionEvent.ACTION_POINTER_DOWN:
      lastFingerDist = calFingerDistance(event);
      break;
    case MotionEvent.ACTION_MOVE:
      if (event.getPointerCount() == 1) {
        handleMove(event);
      } else if (event.getPointerCount() == 2) {
        handleZoom(event);
      }
      break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
      lastMoveX = -1;
      lastMoveY = -1;
      break;
    default:
      break;
  }
}

ملاحظة: تم الحفاظ على أسماء الدوال والمتغيرات كما هي لأنها أسماء تقنية ولا يتم ترجمتها.

private void handleMove(MotionEvent event) {
  float moveX = event.getX();
  float moveY = event.getY();
  if (lastMoveX == -1 && lastMoveY == -1) {
    lastMoveX = moveX;
    lastMoveY = moveY;
  }
  moveDistX = (int) (moveX - lastMoveX);
  moveDistY = (int) (moveY - lastMoveY);
  if (moveDistX + totalTranslateX > 0 || moveDistX + totalTranslateX + curBitmapWidth < width) {
    moveDistX = 0;
  }
  if (moveDistY + totalTranslateY > 0 || moveDistY + totalTranslateY + curBitmapHeight < height) {
    moveDistY = 0;
  }
  status = STATUS_MOVE;
  invalidate();
  lastMoveX = moveX;
  lastMoveY = moveY;
}

تمت ترجمة الكود أعلاه إلى:

private void handleMove(MotionEvent event) {
  float moveX = event.getX();
  float moveY = event.getY();
  if (lastMoveX == -1 && lastMoveY == -1) {
    lastMoveX = moveX;
    lastMoveY = moveY;
  }
  moveDistX = (int) (moveX - lastMoveX);
  moveDistY = (int) (moveY - lastMoveY);
  if (moveDistX + totalTranslateX > 0 || moveDistX + totalTranslateX + curBitmapWidth < width) {
    moveDistX = 0;
  }
  if (moveDistY + totalTranslateY > 0 || moveDistY + totalTranslateY + curBitmapHeight < height) {
    moveDistY = 0;
  }
  status = STATUS_MOVE;
  invalidate();
  lastMoveX = moveX;
  lastMoveY = moveY;
}

ملاحظة: الكود لم يتم تغييره لأنه يحتوي على أسماء متغيرات ووظائف محددة بلغة البرمجة Java، والتي لا يتم ترجمتها عادةً.

private void handleZoom(MotionEvent event) {
  float fingerDist = calFingerDistance(event);
  calFingerCenter(event);
  if (fingerDist > lastFingerDist) {
    status = STATUS_ZOOM_OUT;
  } else {
    status = STATUS_ZOOM_IN;
  }
  scaledRatio = fingerDist * 1.0f / lastFingerDist;
  totalRatio = totalRatio * scaledRatio;
  if (totalRatio < initRatio) {
    totalRatio = initRatio;
  } else if (totalRatio > initRatio * 4) {
    totalRatio = initRatio * 4;
  }
  lastFingerDist = fingerDist;
  invalidate();
}

onDraw(Canvas canvas)
يرسم الحالة الحالية للعرض.

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  if (scaleMode) {
    switch (status) {
      case STATUS_MOVE:
        move(canvas);
        break;
      case STATUS_ZOOM_IN:
      case STATUS_ZOOM_OUT:
        zoom(canvas);
        break;
      default:
        if (cacheBm != null) {
          canvas.drawBitmap(cacheBm, matrix, null);
          canvas.drawPath(path, paint);
        }
    }
  } else {
    if (cacheBm != null) {
      canvas.drawBitmap(cacheBm, matrix, null);
      canvas.drawPath(path, paint);
    }
  }
}

move(Canvas canvas)
يتعامل مع عملية الحركة أثناء التكبير/التصغير.

private void move(Canvas canvas) {
  matrix.reset();
  matrix.postScale(totalRatio, totalRatio);
  totalTranslateX = moveDistX + totalTranslateX;
  totalTranslateY = moveDistY + totalTranslateY;
  matrix.postTranslate(totalTranslateX, totalTranslateY);
  canvas.drawBitmap(cacheBm, matrix, null);
}

ترجمة الكود إلى العربية:

private void move(Canvas canvas) {
  matrix.reset(); // إعادة تعيين المصفوفة
  matrix.postScale(totalRatio, totalRatio); // تطبيق التحجيم باستخدام النسبة الكلية
  totalTranslateX = moveDistX + totalTranslateX; // تحديث الإزاحة الأفقية الكلية
  totalTranslateY = moveDistY + totalTranslateY; // تحديث الإزاحة الرأسية الكلية
  matrix.postTranslate(totalTranslateX, totalTranslateY); // تطبيق الإزاحة على المصفوفة
  canvas.drawBitmap(cacheBm, matrix, null); // رسم الصورة المخزنة باستخدام المصفوفة
}

ملاحظة: تمت ترجمة التعليقات التوضيحية فقط، حيث أن الكود نفسه لا يتم ترجمته.

zoom(Canvas canvas)
التعامل مع عملية التكبير/التصغير.

private void zoom(Canvas canvas) {
  matrix.reset();
  matrix.postScale(totalRatio, totalRatio);
  int scaledWidth = (int) (cacheBm.getWidth() * totalRatio);
  int scaledHeight = (int) (cacheBm.getHeight() * totalRatio);
  int translateX;
  int translateY;
  if (curBitmapWidth < width) {
    translateX = (width - scaledWidth) / 2;
  } else {
    translateX = (int) (centerPointX + (totalTranslateX - centerPointX) * scaledRatio);
    if (translateX > 0) {
      translateX = 0;
    } else if (scaledWidth + translateX < width) {
      translateX = width - scaledWidth;
    }
  }
  if (curBitmapHeight < height) {
    translateY = (height - scaledHeight) / 2;
  } else {
    translateY = (int) (centerPointY + (totalTranslateY - centerPointY) * scaledRatio);
    if (translateY > 0) {
      translateY = 0;
    } else if (scaledHeight + translateY < height) {
      translateY = height - scaledHeight;
    }
  }
  matrix.postTranslate(translateX, translateY);
  canvas.drawBitmap(cacheBm, matrix, null);
}

الترجمة:

private void zoom(Canvas canvas) {
  matrix.reset();
  matrix.postScale(totalRatio, totalRatio);
  int scaledWidth = (int) (cacheBm.getWidth() * totalRatio);
  int scaledHeight = (int) (cacheBm.getHeight() * totalRatio);
  int translateX;
  int translateY;
  if (curBitmapWidth < width) {
    translateX = (width - scaledWidth) / 2;
  } else {
    translateX = (int) (centerPointX + (totalTranslateX - centerPointX) * scaledRatio);
    if (translateX > 0) {
      translateX = 0;
    } else if (scaledWidth + translateX < width) {
      translateX = width - scaledWidth;
    }
  }
  if (curBitmapHeight < height) {
    translateY = (height - scaledHeight) / 2;
  } else {
    translateY = (int) (centerPointY + (totalTranslateY - centerPointY) * scaledRatio);
    if (translateY > 0) {
      translateY = 0;
    } else if (scaledHeight + translateY < height) {
      translateY = height - scaledHeight;
    }
  }
  matrix.postTranslate(translateX, translateY);
  canvas.drawBitmap(cacheBm, matrix, null);
}
Y = height - scaledHeight;
    }
  }
  totalTranslateX = translateX;
  totalTranslateY = translateY;
  curBitmapWidth = scaledWidth;
  curBitmapHeight = scaledHeight;
  matrix.postTranslate(translateX, translateY);
  canvas.drawBitmap(cacheBm, matrix, null);
}

إدارة التاريخ

فئة History تدير سجل الرسم لتمكين وظائف التراجع والإعادة.

saveToStack(Path path, Paint paint)
يحفظ المسار الحالي والفرشاة في المكدس.

public void saveToStack(Path path, Paint paint) {
  Draw draw = new Draw();
  draw.path = new Path(path);
  draw.paint = new Paint(paint);
  saveToStack(draw);
}

تمت ترجمة الكود أعلاه إلى العربية كما يلي:

public void saveToStack(Path path, Paint paint) {
  Draw draw = new Draw();
  draw.path = new Path(path);
  draw.paint = new Paint(paint);
  saveToStack(draw);
}

ملاحظة: الكود المقدم مكتوب بلغة Java، ولا يتم ترجمة أسماء الدوال أو المتغيرات أو الكائنات مثل Path، Paint، Draw، إلخ. يتم الحفاظ على هذه الأسماء كما هي لأنها جزء من لغة البرمجة ولا يتم ترجمتها.

public void saveToStack(Draw draw) {
  curPos++;
  while (histroy.size() > curPos) {
    histroy.pop();
  }
  histroy.push(draw);
  if (callBack != null) {
    callBack.onHistoryChanged();
  }
}

تمت ترجمة الكود أعلاه إلى:

public void saveToStack(Draw draw) {
  curPos++;
  while (histroy.size() > curPos) {
    histroy.pop();
  }
  histroy.push(draw);
  if (callBack != null) {
    callBack.onHistoryChanged();
  }
}

ملاحظة: الكود لم يتم تغييره لأنه يحتوي على أسماء متغيرات ووظائف بالإنجليزية، ويجب الحفاظ عليها كما هي لضمان عمل الكود بشكل صحيح.

getBitmapAtDraw(int n)
تُرجع صورة نقطية (bitmap) تمثل حالة النقطة المحددة في سجل الرسم (history).

public Bitmap getBitmapAtDraw(int n) {
  Canvas canvas = new Canvas();
  Bitmap bm = Utils.getCopyBitmap(srcBitmap);
  canvas.setBitmap(bm);
  for (int i = 0; i <= n; i++) {
    Draw draw = histroy.get(i);
    canvas.drawPath(draw.path, draw.paint);
  }
  return bm;
}

undo()
يقوم بتنفيذ عملية التراجع.

public Bitmap undo() throws UnsupportedOperationException {
  if (canUndo()) {
    curPos--;
    if (callBack != null) {
      callBack.onHistoryChanged();
    }
    return getBitmapAtDraw(curPos);
  } else {
    throw new UnsupportedOperationException("لا توجد سجلات يمكن التراجع عنها");
  }
}

redo()
يقوم بتنفيذ عملية الإعادة (Redo).

public Bitmap redo() throws UnsupportedOperationException {
  if (canRedo()) {
    curPos++;
    if (callBack != null) {
      callBack.onHistoryChanged();
    }
    return getBitmapAtDraw(curPos);
  } else {
    throw new UnsupportedOperationException("لا توجد سجلات قابلة للإعادة");
  }
}

canUndo()
يتحقق مما إذا كان بالإمكان التراجع عن الإجراء الأخير.

public boolean canUndo() {
  return curPos > 0;
}

الترجمة:

public boolean canUndo() {
  return curPos > 0;
}

ملاحظة: الكود المقدم لا يحتاج إلى ترجمة حيث أنه مكتوب بلغة Java وهي لغة برمجة عالمية ولا تتغير باختلاف اللغة.

canRedo()
يتحقق مما إذا كان بالإمكان إعادة الإجراء.

public boolean canRedo() {
  return curPos + 1 < histroy.size();
}

الترجمة:

public boolean canRedo() {
  return curPos + 1 < histroy.size();
}

ملاحظة: الكود المقدم مكتوب بلغة Java ويبدو أنه جزء من تطبيق يتعامل مع إعادة تنفيذ الإجراءات (Redo). الكود يتحقق مما إذا كان بالإمكان تنفيذ إعادة الإجراء (Redo) عن طريق التحقق من أن الموضع الحالي (curPos) زائد واحد أقل من حجم قائمة التاريخ (histroy.size()). إذا كان الشرط صحيحًا، فهذا يعني أنه يمكن تنفيذ إعادة الإجراء.


الخلاصة

يوفر DrawActivity والفئات المرتبطة به مثالًا شاملاً لتنفيذ عرض رسم مخصص في نظام Android. يعرض هذا المثال تقنيات متنوعة، بما في ذلك التعامل مع أحداث اللمس، وإدارة سجل الرسم، والتكامل مع مكونات أخرى مثل الأجزاء (fragments) والمهام غير المتزامنة (async tasks). من خلال فهم كل مكون وخوارزمية، يمكنك الاستفادة من هذه التقنيات في تطبيقاتك الخاصة لإنشاء ميزات رسم قوية وتفاعلية.


Back 2025.01.18 Donate