كيفية تغيير حجم الصور لتحسين أداء التحميل / التنزيل. تطوير أندرويد.

من غير الشائع رؤية مشروع لا يتطلب تحميل الصور بشكل أو بآخر. معظم أجهزة الأندرويد الراقية اليوم تخلق صورة بحجم 2 ميغابايت. هذه مشكلة ، كيف؟ تخيل أنك تقوم بإنشاء تطبيق جوال يتطلب أن يكون لدى المستخدمين لديك صورة ملف تعريف ، مع مراعاة القابلية للضبط ، يجب ألا تكون كل صورة> 100 كيلو بايت ، هل أنا على حق؟ لا تريد حفظ صورة 1 ميغابايت لكل مستخدم. لن يؤثر فقط على UX الخاص بك ، خادمك سيعاني كذلك. يمكن أن يؤدي تحميل صورة كبيرة إلى خادم بعيد إلى حدوث مشكلة واحدة أو جميعها

  • بطء عملية التحميل / التنزيل على الأجهزة ذات النطاق الترددي المنخفض للشبكة
  • تؤثر على قابلية المنتج
  • و اكثر…

حل

الحل الحقيقي الواضح هو تغيير حجم الصورة قبل إرسالها إلى الخادم البعيد. حسن!. ولكن كيف؟ هذا ما أحاول إظهاره.

واجهت مشكلة صغيرة في معرفة كيفية حل هذه المشكلة عندما بدأت في كتابة تطبيقات للأندرويد ، لذلك قررت فقط مشاركتها اليوم. إنني أتطلع إلى رؤية شخص ما سوف يُظهر لي طريقة أفضل للقيام بذلك. لنذهب!

افتراض

  • أفترض أنك ملائم مع لغة برمجة Java (وهذا ما سأستخدمه ، يمكنك بسهولة الحصول عليه إذا كانت لديك تجربة مع لغات البرمجة الأخرى أيضًا).
  • قم بتثبيت استوديو Android و SDK وجاهز.
  • إنشاء مشروع استوديو أندرويد جديد. أدعو الألغام EasyPhotoUpload

اسم المشروع - EasyPhotoUpload

دقيقة SDK - 4.0.3 ، API 15

في الأساس ، نحن نحاول

  • السماح للمستخدم بتحديد الصورة من المعرض
  • الحصول على مسار الصورة المحدد
  • تغيير حجم الصورة المحددة في سلسلة رسائل خلفية وإرجاع النتيجة إلى سلسلة الرسائل الرئيسية - كما ترى ، فإن تغيير حجم الصورة عملية باهظة الثمن ويجب ألا يتم تنفيذها على سلسلة الرسائل الرئيسية

يتوفر الرمز النهائي لهذه المقالة على github - https://github.com/adigunhammedolalekan/easyphotoupload

بمجرد أن ينتهي android studio من بناء المشروع وتكون جاهزًا بالكامل ، قم بإنشاء هذه الحزم - الأساسية والمستمعين والأدوات

ضمن حزمة use ، قم بإنشاء فئة جديدة ، Util.java ، فيما يلي محتوى الملف.

‘فئة عامة Util {

// SDF لإنشاء اسم فريد للملف المضغوط.
 Final static public SimpleDateFormat SDF = new SimpleDateFormat ("yyyymmddhhmmss"، Locale.getDefault ())؛

/ *
 ضغط الملف / الصورة منparam المسار إلى موقع خاص على الجهاز الحالي وإعادة الملف المضغوط.
 param path = مسار الصورة الأصلي
 param context = سياق Android الحالي
 * /
 ملف ثابت getCompressed عام (سياق السياق ، مسار السلسلة) يلقي IOException {

إذا (السياق == فارغة)
 رمي NullPointerException جديد ("يجب ألا يكون السياق فارغًا.") ؛
 // الحصول على دليل ذاكرة التخزين المؤقت الخارجي للجهاز ، قد لا يكون متاحًا في بعض الأجهزة ،
 // لذلك يعود رمزنا إلى دليل ذاكرة التخزين المؤقت الداخلية ، والذي يتوفر دائمًا ولكن بكميات أقل
 ملف cacheDir = context.getExternalCacheDir ()؛
 إذا (cacheDir == فارغة)
 //تراجع
 cacheDir = context.getCacheDir ()؛

سلسلة rootDir = cacheDir.getAbsolutePath () + "/ ImageCompressor"؛
 ملف الجذر = ملف جديد (rootDir) ؛

/ / قم بإنشاء مجلد ImageCompressor إذا لم يكن موجودًا بالفعل.
 إذا (! root.exists ())
 root.mkdirs ()؛

/ / فك شفرة وتغيير حجم الصورة النقطية الأصلية من مسارparam.
 صورة نقطية صورة نقطية = decodeImageFromFiles (المسار ، / * العرض المطلوب * / 300 ، / * الطول المرغوب * / 300) ؛

/ / قم بإنشاء عنصر نائب لملف الصور المضغوط
 ملف مضغوط = ملف جديد (root ، SDF.format (تاريخ جديد ()) + ".jpg" / * التنسيق الذي تريده * /) ؛

/ / قم بتحويل الصورة النقطية المشفرة إلى تيار
 ByteArrayOutputStream byteArrayOutputStream = جديد ByteArrayOutputStream ()؛

/ * ضغط الصورة النقطية إلى byteArrayOutputStream
 Bitmap.compress (التنسيق والجودة و OutputStream)

حيث تتراوح الجودة من 1 إلى 100.
 * /
 bitmap.compress (Bitmap.CompressFormat.JPEG ، 80 ، byteArrayOutputStream) ؛

/ *
 الآن ، لدينا صورة نقطية داخل كائن byteArrayOutputStream ، كل ما نحتاج إليه بعد ذلك هو كتابته إلى الملف المضغوط الذي أنشأناه مسبقًا ،
 java.io.FileOutputStream يمكن أن يساعدنا على فعل ذلك!

* /
 FileOutputStream fileOutputStream = جديد FileOutputStream (مضغوط) ؛
 fileOutputStream.write (byteArrayOutputStream.toByteArray ())؛
 fileOutputStream.flush ()؛

fileOutputStream.close ()؛

// الملف المكتوب ، والعودة إلى المتصل. منجز!
 عودة مضغوط.
 }

ثابت صورة نقطية ثابتة عامة decodeImageFromFiles (مسار السلسلة ، عرض كثافة العمليات ، ارتفاع كثافة العمليات) {
 BitmapFactory.Options scaleOptions = جديد BitmapFactory.Options ()؛
 scaleOptions.inJustDecodeBounds = true؛
 BitmapFactory.decodeFile (المسار ، scaleOptions) ؛
 مقياس كثافة العمليات = 1 ؛
 بينما (scaleOptions.outWidth / scale / 2> = العرض
 && scaleOptions.outHeight / scale / 2> = height) {
 مقياس * = 2 ؛
 }
 / / فك شفرة مع حجم العينة
 BitmapFactory.Options outOptions = جديد BitmapFactory.Options ()؛
 outOptions.inSampleSize = scale ؛
 إرجاع BitmapFactory.decodeFile (المسار ، outOptions) ؛
 }
} '

الطريقة التي تتعامل مع ضغط الصور وتخزينها هي "الحصول على ملف ثابت مضغوط (سياق ، سلسلة)" ، كما رأيت من الرمز أعلاه ، تأخذ هذه الطريقة مسارًا لصورة موجودة على الجهاز ، وتغيير حجمها ، وتخزينها في موقع خاص على الجهاز وإرجاع الملف المضغوط حديثًا. فويلا!

يسمى الملف التالي الذي سنبحثه ImageCompressTask.java ، هذه الفئة تطبق Runnable ، مع مُنشئ الوسيطات الثلاث وفي طريقة التشغيل () ، يحدث الضغط في مؤشر ترابط الخلفية ، ثم ينشر النتيجة النهائية إلى الرئيسية موضوع بمساعدة android.os.Handler أو الإبلاغ عن الخطأ على خلاف ذلك.

ImageCompressTask.java

Image الطبقة العامة ImageCompressTask تنفذ Runnable {

السياق الخاص mContext؛
 قائمة خاصة originalPaths = new ArrayList <> ()؛
 معالج خاص mHandler = جديد معالج (Looper.getMainLooper ()) ؛
 قائمة خاصة result = new ArrayList <> () ؛
 IImageCompressTaskListener الخاص mIImageCompressTaskListener.

ImageCompressTask العامة (سياق السياق ، مسار السلسلة ، IImageCompressTaskListener compressTaskListener) {

originalPaths.add (مسار)؛
 mContext = السياق ؛

mIImageCompressTaskListener = compressTaskListener؛
 }
 ImageCompressTask العامة (سياق السياق ، مسارات قائمة <سلسلة> ، IImageCompressTaskListener compressTaskListener) {
 originalPaths = المسارات ؛
 mContext = السياق ؛
 mIImageCompressTaskListener = compressTaskListener؛
 }
 @تجاوز
 تشغيل الفراغ العام () {

محاولة {

// تنفيذ جميع المسارات المحددة وجمع الملف المضغوط من Util.getCompressed (سياق ، سلسلة)
 لـ (مسار السلسلة: originalPaths) {
 ملف ملف = Util.getCompressed (mContext ، مسار) ؛
 / أضفها!
 result.add (ملف)؛
 }
 / / استخدم معالج لنشر النتيجة مرة أخرى إلى الموضوع الرئيسي
 mHandler.post (جديد Runnable () {
 @تجاوز
 تشغيل الفراغ العام () {

إذا (mIImageCompressTaskListener! = فارغة)
 mIImageCompressTaskListener.onComplete (نتيجة).
 }
 })؛
 } catch (final IOException ex) {
 // كان هناك خطأ ، الإبلاغ عن الخطأ مرة أخرى من خلال رد الاتصال
 mHandler.post (جديد Runnable () {
 @تجاوز
 تشغيل الفراغ العام () {
 إذا (mIImageCompressTaskListener! = فارغة)
 mIImageCompressTaskListener.onError (على سبيل المثال)؛
 }
 })؛
 }
 }
} '

أخيرًا ، أنشئ MainActivity.java ، واجهة المستخدم لعينة التطبيق بأكملها.

MainActivity.java

'
MainActivity فئة عامة يمتد AppCompatActivity {

اختيار الزر
 ImageView selectImage ؛

نهائي ثابت خاص int REQUEST_STORAGE_PERMISSION = 100 ؛
 نهائي ثابت خاص int REQUEST_PICK_PHOTO = 101 ؛

/ / قم بإنشاء تجمع مؤشر ترابط واحد إلى فئة ضغط الصور الخاصة بنا.
 ExecutorService الخاص mExecutorService = Executors.newFixedThreadPool (1) ؛

ImageCompressTask الخاص imageCompressTask.

@تجاوز
 محمي void onCreate (تم حفظ الحزمة في InstanceState) {
 super.onCreate (savedInstanceState)؛
 setContentView (R.layout.activity_main)؛

selectImage = (ImageView) findViewById (R.id.iv_selected_photo)؛
 selectImage = (زر) findViewById (R.id.btn_select_image) ؛

selectImage.setOnClickListener (عرض جديد. OnClickListener () {
 @تجاوز
 باطل عام onClick (عرض عرض) {
 requestPermission ()؛
 }
 })؛
 }

طلب باطلإذن () {

إذا (PackageManager.PERMISSION_GRANTED! =
 ContextCompat.checkSelfPermission (هذا ، Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
 إذا كان (ActivityCompat.shouldShowRequestPermissionRationale (هذا ، Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
 ActivityCompat.requestPermissions (هذا ، سلسلة جديدة [] {Manifest.permission.WRITE_EXTERNAL_STORAGE} ،
 REQUEST_STORAGE_PERMISSION)؛
 } آخر {
 //بلى! أريد أن تقوم كلتا المجموعتين بنفس الشيء ، يمكنك كتابة منطقك الخاص ، لكن هذا الأمر يناسبني.
 ActivityCompat.requestPermissions (هذا ، سلسلة جديدة [] {Manifest.permission.WRITE_EXTERNAL_STORAGE} ،
 REQUEST_STORAGE_PERMISSION)؛
 }
 } آخر {
 // إذن الممنوح ، دعنا نذهب اختيار الصورة
Intent intent = new Intent (Intent.ACTION_PICK)؛
intent.setAction (Intent.ACTION_GET_CONTENT)؛
intent.setType ( "صورة / *")؛
startActivityForResult (نية ، REQUEST_PICK_PHOTO) ؛
 }

}

@تجاوز
 الفراغ المحمي onActivityResult (int requestCode ، int resultCode ، بيانات Intent) {
 super.onActivityResult (requestCode ، resultCode ، data) ؛
 إذا (requestCode == REQUEST_PICK_PHOTO && resultCode == RESULT_OK &&
 البيانات! = فارغة) {
 // استخراج مسار الصورة المطلقة من أوري
 Uri uri = data.getData ()؛
 Cursor cursor = MediaStore.Images.Media.query (getContentResolver ()، uri، new String [] {MediaStore.Images.Media.DATA})؛
 إذا (المؤشر! = فارغة) {
 مسار السلسلة = cursor.getString (cursor.getColumnIndexOrThrow (MediaStore.Images.Media.DATA)) ؛

/ / إنشاء ImageCompressTask وتنفيذها مع Executor.
 imageCompressTask = جديد ImageCompressTask (هذا ، المسار ، iImageCompressTaskListener) ؛

mExecutorService.execute (imageCompressTask)؛
 }
 }
 }

// صورة ضغط مهمة معاودة الاتصال
 IImageCompressTaskListener الخاص iImageCompressTaskListener = جديد IImageCompressTaskListener () {
 @تجاوز
 الفراغ العام onComplete (قائمة <ملف> مضغوط) {
 // الصورة مضغوطة. ياي!

// الاستعداد للتحميلات.

ملف ملف = compressed.get (0) ؛

selectedImage.setImageBitmap (BitmapFactory.decodeFile (file.getAbsolutePath ()))؛
 }

@تجاوز
 باطل onError (خطأ قابل للإلغاء) {
 / / من غير المحتمل جدًا ، ولكنه قد يحدث على جهاز ذي سعة تخزين منخفضة للغاية.
 // قم بتسجيله ، log.WhatTheFuck؟ ، أو أظهر مربع حوار يطلب من المستخدم حذف بعض الملفات ... إلخ ، إلخ.
 Log.wtf ("ImageCompressor" ، "حدث خطأ" ، خطأ) ؛
 }
 }؛

@تجاوز
 باطل محمي onDestroy () {
 super.onDestroy ()؛

//نظف!
 mExecutorService.shutdown ()؛

mExecutorService = فارغة.
 imageCompressTask = فارغة.
 }
} '

يتم تعليق جميع الرموز بشكل جيد ، ولكن إذا كان لديك مشكلة مع أي جزء. اسمحوا لي أن أعرف! جميع الرموز على جيثب ، زرها لرؤية أفضل. لقطة الشاشة من النتيجة النهائية.

أنا أحب مساهمتك في هذا. شكر!

عني

أنا مطور تطبيق جوال عاطفي ولديه 2.5+ خبرة في إنشاء تطبيقات رائعة لمنصة android. إذا كنت بحاجة إلى موهبتي ، فلا تتردد في الاتصال بي!

جيثب - www.github.com/adigunhammedolalekan

البريد - adigunhammed.lekan@gmail.com

الاتصال - 07035452307