إنشاء معرض صور بسيط باستخدام Firebase Storage

إنشاء معرض صور خاص بك باستخدام Firebase Storage و Firebase Database

AbdulAlim Rajjoubمنذ سنتين

شرحنا في الدرس السابق  أساسيات Firebase Storage وكيف قمنا برفع الصور وتحميلها باستخدام Firebase Storage وسنقوم بهذا الدرس بإنشاء تطبيق بسيط فكرته رفع صورك الخاصة الى Firebase Storage وعرضها في RecyclerView مع إمكانية تحميلها مرة أخرى ويمكنك عرضهم من أي جهاز تريد (بنفس فكرة Google Photos ولكن بشكل مبسط)

سنستخدم Firebase Realtime Database لتخزين مسار الصور وتاريخها ولهذا أنصحك بمتابعة درس Firebase Realtime Database

سنقوم بإنشاء مشروع جديد على أندرويد ستوديو ونقوم بإنشاء مشروع على Firebase ونقوم بربطهم ببعضهم البعض

ثم نبدأ بإضافة بعض المكتبات

Firebase Storage

implementation 'com.google.firebase:firebase-storage:11.0.4'

Firebase Database

implementation 'com.google.firebase:firebase-database:11.0.4'

RecyclerView

implementation 'com.android.support:design:26.1.0'

Picasso لعرض الصور 

implementation 'com.squareup.picasso:picasso:2.5.2'

 

نبدأ بتعريف Firebase Storage و Firebase Database

FirebaseStorage firebaseStorage = FirebaseStorage.getInstance();
FirebaseDatabase database = FirebaseDatabase.getInstance();

نقوم بتعريف الmainRef لFirebase Storage 

StorageReference mainRef = firebaseStorage.getReference();

الآن سنقوم بوضع المسار الذي سيتم حفظ صور Firebase Storage ومساراتها في Database

ملاحظة:من المستحسن استخدام Firebase Auth الذي يعطي UID لكل شخص يقوم بتسجيل الدخول ووضع الصور داخل هذا uid بدلاً من 'userUid',ولكن في سبيل التجربة لا مشكلة

userImagesRef = mainRef.child("userUid").child("images");
databaseReference = database.getReference().child("userUid").child("images");

 

أما الآن فسيكون xml بهذا الشكل

وعند الضغط على زر Fab سنقوم باستدعاء ميثود pickImage التي ستقوم بإنشاء Intent لفتح معرض الصور لاختيار صورة ما

    private void pickImage() {
        Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
        photoPickerIntent.setType("image/*");
        startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST);
    }

ونستلم نتيجة الاختيار في onActivityResult كما فعلنا في الدرس السابق ونستدعي الميثود upload التي قمنا بإنشائها في الدرس السابق ولكن مع بعض التعديلات 

    @Override
    protected void onActivityResult(int reqCode, int resultCode, Intent data) {
        super.onActivityResult(reqCode, resultCode, data);


        if (resultCode == RESULT_OK) {
            final Uri imageUri = data.getData();
            upload(imageUri);


        } else {
            Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
        }
    }
    private void upload(Uri uri) {
        final ProgressDialog progressDialog = new ProgressDialog(this);
        progressDialog.show();
        final String imageName = UUID.randomUUID().toString() + ".jpg";


        userImagesRef.child(imageName).putFile(uri)
                .addOnProgressListener(new OnProgressListener<UploadTask.TaskSnapshot>() {
                    @Override
                    public void onProgress(UploadTask.TaskSnapshot taskSnapshot) {
                        int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
                        progressDialog.setMessage(progress + "");
                    }
                })
                .addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) {
                        
                        progressDialog.dismiss();

                        if (task.isSuccessful()) {
                            String link = String.valueOf(task.getResult().getDownloadUrl());
                            String path = task.getResult().getStorage().getPath();
                            saveImagePathToDatabase(link, path);

                            Toast.makeText(MainActivity.this, "Uplaod Succeed", Toast.LENGTH_SHORT).show();
                        } else {
                            Log.d("3llomi", "upload Failed " + task.getException().getLocalizedMessage());
                            Toast.makeText(MainActivity.this, "Uplaod Failed :( " + task.getException().getLocalizedMessage(), Toast.LENGTH_LONG).show();
                        }
                    }
                });
    }

الشيئ الجديد الذي قمنا بإضافته هو استخراج رابط الصورة  في onComplete عند الإنتهاء والرابط يكون بهذا الشكل https://......image-name.jpg (سنستخدمه لاحقاً لعرض الصورة)

واستخرجنا مسار الصورة الذي يكون بهذا الشكل  "/userUid/images/67d6e163-701a-41e0-b235-dcea428cefc4.jpg" (سنستخدمه لاحقاً عند تحميل الصورة)

ملاحظة: يمكنك تطبيق درس توليد الصور المصغرة عند رفع الصور | Firebase Cloud Functions لتوليد الصور المصغرة عند رفع أي صورة بدلاً من عرض الصورة كاملة واستهلاك الكثير من الموارد 

الآن نقوم باستدعاء ميثود saveImagePathToDatabase ونعطي link و path كبارامترز

قبل إنشاء هذه الميثود يجب علينا إنشاء كلاس POJO او Model والذي سيكون بهذا الشكل ,وهو يحتوي على

  • رابط االصورة imageLink
  • مسار الصورة imagePath
  • تاريخ رفع الصورة timestamp 

ونقوم بإنشاء ميثود getFormattedTime التي تقوم بتحويل التاريخ من long الى تاريخ مقروء مثل 17/3/2018 وقمنا بإضافة Annotation @Exclude لمنع الFirebase من حفظ هذه الميثود عند الرفع الى قاعدة البيانات 

public class Image {
    private String imageLink;
    private String imagePath;
    private long timestamp;


    public Image() {
    }

    public Image(String imageLink, String imagePath) {
        this.imageLink = imageLink;
        this.imagePath = imagePath;
        timestamp = new Date().getTime();
    }

    public String getImageLink() {
        return imageLink;
    }

    public void setImageLink(String imageLink) {
        this.imageLink = imageLink;
    }

    public String getImagePath() {
        return imagePath;
    }

    public void setImagePath(String imagePath) {
        this.imagePath = imagePath;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    @Exclude
    public String getFormattedTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        Date date = new Date();
        date.setTime(timestamp);
        return sdf.format(date);
    }
}

نعود الآن وننشئ الميثود saveImagePathToDatabase 

نقوم بإنشاء اوبجكت جديد من Image ونعطيه link و path 

ثم نقوم بعمل push التي تقوم بتوليد key جديد ثم setValue لحفظ البيانات

 private void saveImagePathToDatabase(String link, String path) {
        Image image = new Image(link, path);
        databaseReference.push().setValue(image);
    }

نقوم الآن بتشغيل التطبيق ونقوم برفع صورة ما

عند نجاح عملية الرفع سنرى معلومات الصورة في قاعدة البيانات بهذا الشكل

 

ونذهب الى Storage لنرى الصورة

ممتاز! :D ولكن الى الآن لم نقم بعرض الصور

نبدأ بتجهيز RecyclerView Adapter (يمكنك مراجعة درس RecyclerView ) ونقوم بإنشاء ملف row_image.xml 

سيكون الأدابتر بهذا الشكل

public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ImageHolder> {
    private List<Image> images;
    private Context context;
    private OnClickListener onClickListener;

    public ImageAdapter(List<Image> images, Context context, OnClickListener onClickListener) {
        this.images = images;
        this.context = context;
        this.onClickListener = onClickListener;
    }

    @Override
    public ImageHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_image, parent, false);
        return new ImageHolder(row);
    }

    @Override
    public void onBindViewHolder(final ImageHolder holder, int position) {
        Image image = images.get(position);
        Picasso.with(context).load(image.getImageLink()).into(holder.imageThumbnail);
        holder.tvTime.setText(image.getFormattedTime());

        holder.imageThumbnail.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onClickListener != null)
                    onClickListener.onClick(/* do NOT PASS position from the Params because it will be final! ,use get adapter position instead*/holder.getAdapterPosition());
            }
        });
    }

    @Override
    public int getItemCount() {
        return images.size();
    }

    class ImageHolder extends RecyclerView.ViewHolder {
        private ImageView imageThumbnail;
        private TextView tvTime;


        public ImageHolder(View itemView) {
            super(itemView);
            imageThumbnail = itemView.findViewById(R.id.image_thumbnail);
            tvTime = itemView.findViewById(R.id.tv_time);
        }
    }

    public interface OnClickListener {
        void onClick(int index);
    }
}

ملاحظة:قمنا باستخدام interface عند الضغط على الصورة سنقوم بتحميلها من الأكتفتي

وrow_image.xml بهذا الشكل

 

بما أننا نريد عرض الصور على شكل شبكي Grid فسنقوم بإنشاء كلاس الذي سيقوم بجعل المسافات بين العنصر والآخر متساوية

 

public class GridItemDecoration extends RecyclerView.ItemDecoration {

    private int spanCount;
    private int spacing;
    private boolean includeEdge;

    public GridItemDecoration(int spanCount, int spacing, boolean includeEdge) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view); // item position
        int column = position % spanCount; // item column

        if (includeEdge) {

            outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

            if (position < spanCount) { // top edge
                outRect.bottom = spacing; // item bottom
            }
        } else {
            outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
            outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
            if (position >= spanCount) {
                outRect.top = spacing; // item top
            }
        }
    }
}

 

نعود الآن الى MainActivity لنجهز الأدابتر 

سنقوم بإنشاء List<Image> لنضيف اليها العناصر  ونجعلها global ونعطيها ل adapter

List<Image> images = new ArrayList<>();

عند الضغط على الصورة سنقوم بتحميلها عبر الميثود download التي سنقوم بإنشاءها لاحقاً

ووضعنا 3 عناصر في كل صف في RecyclerView عبر SPAN_COUNT بمسافة 4px  SPACING عبر itemDceoration الذي أنشأناه

  adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
            @Override
            public void onClick(int index) {
                Image image = images.get(index);
                download(image.getImagePath());
            }
        });

        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_COUNT);
        recyclerView.setLayoutManager(gridLayoutManager);
        recyclerView.addItemDecoration(new GridItemDecoration(SPAN_COUNT, SPACING, true));
        recyclerView.setAdapter(adapter);

تم تجهيز الأدابتر ولكن اذا شغلت التطبيق فلن تجد أي بيانات لأنه لم نقم بإضافتها

ولهذا سنقوم بإضافة addChildEventListener والتي سيتم استدعاؤها عندما يتم إضافة اي عنصر الى Firebase Database  الى المسار userUid/images

في onChildAdded قمنا بعمل Cast ل Snapshot الصورة القادمة من قاعدة البيانات وقمنا بإضافتها الى الليست Images 

وأخيراً نجعل الأدابتر يقوم بتحديث البيانات عبر adapter.notifyDataSetChanged();

databaseReference.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                Image image = dataSnapshot.getValue(Image.class);
                images.add(image);
                adapter.notifyDataSetChanged();
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                adapter.notifyDataSetChanged();
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

أخيراً نقوم بتشغيل التطبيق لنرى النتيجة ونقوم بإضافة بعض الصور

الآن عند الضغط على الصورة سنقوم بتحميل هذه الصورة,سنقوم بتحميلها الى مجلد التطبيق نفسه وبالتالي لا داعي لوجود الصلاحيات WRITE_EXTERNAL_STORAGE

ولهذا نقوم بإنشاء نفس الميثود التي قمنا بإنشاءها في الدرس السابق download

 private void download(String imagePath) {
        final ProgressDialog progressDialog = new ProgressDialog(this);

        progressDialog.setMessage("Downloading...");
        progressDialog.show();
        String localFileName = UUID.randomUUID().toString() + ".jpg";
        final File file = new File(getFilesDir(), localFileName);
        mainRef.child(imagePath).getFile(file)
                .addOnCompleteListener(new OnCompleteListener<FileDownloadTask.TaskSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<FileDownloadTask.TaskSnapshot> task) {
                        progressDialog.dismiss();
                        if (task.isSuccessful()) {
                            Toast.makeText(MainActivity.this, "file Downloaded to " + file.getPath(), Toast.LENGTH_SHORT).show();
                            Log.d("3llomi", "File Downloaded to " + file.getPath());


                        } else {
                            Toast.makeText(MainActivity.this, "download Failed " + task.getException().getLocalizedMessage(), Toast.LENGTH_SHORT).show();
                            Log.d("3llomi", "Download Failed " + task.getException().getLocalizedMessage());
                        }
                    }
                }).addOnProgressListener(new OnProgressListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onProgress(FileDownloadTask.TaskSnapshot taskSnapshot) {
                int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
                progressDialog.setMessage(progress + "%");

            }
        });

    }

ونقوم باستدعاءها في adapter onClick

   adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
            @Override
            public void onClick(int index) {
                Image image = images.get(index);
                download(image.getImagePath());
            }
        });

 

ليصبح كلاس MainActivity كاملاً كالتالي

public class MainActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private static final int SPAN_COUNT = 3;
    private static final int SPACING = 4;

    private static final int PICK_IMG_REQUEST = 7588;
    FirebaseStorage firebaseStorage;
    StorageReference mainRef, userImagesRef;
    FirebaseDatabase database;
    DatabaseReference databaseReference;
    ImageAdapter adapter;
    List<Image> images = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        recyclerView = findViewById(R.id.recycler_view);

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pickImage();
            }
        });

        adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
            @Override
            public void onClick(int index) {
                Image image = images.get(index);
                download(image.getImagePath());
            }
        });

        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_COUNT);
        recyclerView.setLayoutManager(gridLayoutManager);
        recyclerView.addItemDecoration(new GridItemDecoration(SPAN_COUNT, SPACING, true));
        recyclerView.setAdapter(adapter);

        firebaseStorage = FirebaseStorage.getInstance();
        database = FirebaseDatabase.getInstance();
        mainRef = firebaseStorage.getReference();
        userImagesRef = mainRef.child("userUid").child("images");
        databaseReference = database.getReference().child("userUid").child("images");


        databaseReference.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                Image image = dataSnapshot.getValue(Image.class);
                images.add(image);
                adapter.notifyDataSetChanged();
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                adapter.notifyDataSetChanged();
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

    private void pickImage() {
        Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
        photoPickerIntent.setType("image/*");
        startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST);
    }

    @Override
    protected void onActivityResult(int reqCode, int resultCode, Intent data) {
        super.onActivityResult(reqCode, resultCode, data);


        if (resultCode == RESULT_OK) {
            final Uri imageUri = data.getData();
            upload(imageUri);


        } else {
            Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
        }
    }

    private void upload(Uri uri) {
        final ProgressDialog progressDialog = new ProgressDialog(this);
        progressDialog.show();
        final String imageName = UUID.randomUUID().toString() + ".jpg";


        userImagesRef.child(imageName).putFile(uri)
                .addOnProgressListener(new OnProgressListener<UploadTask.TaskSnapshot>() {
                    @Override
                    public void onProgress(UploadTask.TaskSnapshot taskSnapshot) {
                        int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
                        progressDialog.setMessage(progress + "");
                    }
                })
                .addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) {

                        progressDialog.dismiss();

                        if (task.isSuccessful()) {
                            String link = String.valueOf(task.getResult().getDownloadUrl());
                            String path = task.getResult().getStorage().getPath();
                            saveImagePathToDatabase(link, path);

                            Toast.makeText(MainActivity.this, "Uplaod Succeed", Toast.LENGTH_SHORT).show();
                        } else {
                            Log.d("3llomi", "upload Failed " + task.getException().getLocalizedMessage());
                            Toast.makeText(MainActivity.this, "Uplaod Failed :( " + task.getException().getLocalizedMessage(), Toast.LENGTH_LONG).show();
                        }
                    }
                });
    }

    private void saveImagePathToDatabase(String link, String path) {
        Image image = new Image(link, path);
        databaseReference.push().setValue(image);
    }

    private void download(String imagePath) {
        final ProgressDialog progressDialog = new ProgressDialog(this);

        progressDialog.setMessage("Downloading...");
        progressDialog.show();
        String localFileName = UUID.randomUUID().toString() + ".jpg";
        final File file = new File(getFilesDir(), localFileName);
        mainRef.child(imagePath).getFile(file)
                .addOnCompleteListener(new OnCompleteListener<FileDownloadTask.TaskSnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<FileDownloadTask.TaskSnapshot> task) {
                        progressDialog.dismiss();
                        if (task.isSuccessful()) {
                            Toast.makeText(MainActivity.this, "file Downloaded to " + file.getPath(), Toast.LENGTH_SHORT).show();
                            Log.d("3llomi", "File Downloaded to " + file.getPath());


                        } else {
                            Toast.makeText(MainActivity.this, "download Failed " + task.getException().getLocalizedMessage(), Toast.LENGTH_SHORT).show();
                            Log.d("3llomi", "Download Failed " + task.getException().getLocalizedMessage());
                        }
                    }
                }).addOnProgressListener(new OnProgressListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onProgress(FileDownloadTask.TaskSnapshot taskSnapshot) {
                int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
                progressDialog.setMessage(progress + "%");

            }
        });

    }


}

 

المشروع كاملاً على Github 

ملاحظة:المشروع على Github للمعاينة فقط ولايمكنك تجربته على Android Studio لعدم وجود google-services.json الخاص بك  

كلمات دليلية: database firebase storage
2
إعجاب
2646
مشاهدات
0
مشاركة
2
متابع
متميز
محتوى رهيب

التعليقات (1)

soso:

لدي سؤالين ..

سؤالي الاول ...

في الfirebase  الimage link تحفظ كـ  com.google.android.gms.tasks....etc وليس  ارتباط http وهذا يسبب فشل اثناء الretrive قرأت ان هذا بسبب  التحديث الجديد للfirebase

بحثت كثير بخصوص هذي المشكله ولم اجد حل : كيف يمكن حل الشكله واسترجاع الصوره من storge .

 

سؤالي الثاني ..

هناك gridview جاهزه للتعامل معها لماذا فضلت انشاء gridview من جديد؟؟

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !