أرشيف لـ26 أبريل, 2013

40 -اندرويد : تكبير ال View

 

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : تكبير ال View

السلام عليكم ورحمة الله وبركاته

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

  1. إنشاء views
  2. إعداد التحريك الخاص بالتكبير Set up the Zoom Animation
  3. تكبير ال View

بالامكان تحميل المثال من الرابط التالي :

Download the sample app

يشرح لنا هذه الدرس كيف بإمكاننا تحقيق تحريك على شكل تكبير لل view عند النقر عليه, ويعتبر هذا النوع من التحريك مفيد جدا في حالة البوم الصور على سبيل المثال , فعند النقر على صورة صغيرة نرغب أن يتم عرضها بشكل كبير يملأ الشاشة.

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

 

zomming a view

لتحميل المثال انقر هنا

في حال رغبتم بالانتقال بسرعة للمثال الكامل , بإمكانكم تحميله من الرابط التالي : , حيث نجد عدد من الملفات , ولكن فيما يخص درس اليوم , يهمنا منها الملفات التالية :

  • src/TouchHighlightImageButton.java (a simple helper class that shows a blue touch highlight when the image button is pressed)
  • src/ZoomActivity.java
  • layout/activity_zoom.xml

إنشاء ال Views

قم بإنشاء ملف تنسيق يحوي على نسخة صغيرة ونسخة كبيرة من المحتوى الذي ترغب بتكبيره.

يقوم المثال التالي بإنشاء غرض من نوع ImageButton  وذلك لنسخة مصغرة من صورة قابلة للنقر , و غرض من نوع ImageView  يقوم بعرض ال view ألمكبر من الصورة :

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <ImageButton
            android:id="@+id/thumb_button_1"
            android:layout_width="100dp"
            android:layout_height="75dp"
            android:layout_marginRight="1dp"
            android:src="@drawable/thumb1"
            android:scaleType="centerCrop"
            android:contentDescription="@string/description_image_1" />

    </LinearLayout>

    <!-- This initially-hidden ImageView will hold the expanded/zoomed version of
         the images above. Without transformations applied, it takes up the entire
         screen. To achieve the "zoom" animation, this view's bounds are animated
         from the bounds of the thumbnail button above, to its final laid-out
         bounds.
         -->

    <ImageView
        android:id="@+id/expanded_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible"
        android:contentDescription="@string/description_zoom_touch_close" />

</FrameLayout>

 

إعداد التحريك الخاص بالتكبير Set up the Zoom Animation

ما إن تقوم بتطبيق تنسيقك, حتى يتوجب عليك عندها ان تهيأ معالجات الاحداث event handlers التي تقوم بقدح التحريك على شكل تكبير ال view.

يقوم المثال التالي بإضافة View.OnClickListenerإلى الغرض ImageButton  وذلك بهدف تنفيذ التحريك (التكبير) المطلوب عندما ينقر المستخدم على زر الصورة:

public class ZoomActivity extends FragmentActivity {
    // Hold a reference to the current animator,
    // so that it can be canceled mid-way.
    private Animator mCurrentAnimator;

    // The system "short" animation time duration, in milliseconds. This
    // duration is ideal for subtle animations or animations that occur
    // very frequently.
    private int mShortAnimationDuration;

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

        // Hook up clicks on the thumbnail views.

        final View thumb1View = findViewById(R.id.thumb_button_1);
        thumb1View.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                zoomImageFromThumb(thumb1View, R.drawable.image1);
            }
        });

        // Retrieve and cache the system's default "short" animation time.
        mShortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }
    ...
}

تكبير ال View

والآن أنت بحاجة إلى التحريك والانتقال من ال view  ذو القياس والحجم العادي , إلى ال view بعد تكبيره, وذلك في الوقت المناسب.

بشكل عام, أنت بحاجة إلى ان يبدأ التحريك من الحدود الاساسية لل view ذو القياس العادي , إلى الحدود الخاصة بال view  ذو القياس المكبر.

يبين لنا التابع التالي كيفية تحقيق التحريك بحيث ينطلق من حدود الصورة المصغرة , ويكبر ليصل إلى حدود الصورة الكبيرة وذلك عبر اتباع وتنفيذ الامور التالية :

  1. قم بإسناد الصورة ذات الدقة العالية , إلى عن عنصر ImageViewالمخفي.يقوم المثال التالي بتحميل موارد الصورة الكبيرة ضمن مسرى UI thread وذلك بهدف التسهيل.
    تحتاج لان تقوم بعملية التحميل هذه ضمن مسرى مستقل وذلك لكي لا تحجب المسرى الاساسي الخاص بواجهة المستخدم , ومن ثم نقوم باعداد الصورة ضمن مسرى UI . يفضل إلا يكون حجم هذه الصورة bitmap اكبر من قياس الشاشة.
  2. حساب حدود الانطلاق والنهاية ل ImageView.
  3. تحريك كل موضع من المواضع الاربعة (وخصائص التقييس sizing properties)
    X, Y, (SCALE_X, and SCALE_Y) بنفس الوقت , انطلاقا من حدود البداية وصولا لحدود النهاية التي تم حسابها في الخطوات السابقة. يتم اضافة هذه المتحركات الاربعة إلى AnimatorSet  , وبالتالي يصبح بالامكان بدء تحريكهم في نفس الوقت.
  4. التصغير للعودة لنقطة البداية , وذلك عبر اعادة نفس التحريك السابق ولكن بشكل معاكس عندما ينقر المستخدم على الشاشة بعد تكبير الصورة.
    بالامكان القيام بذلك عبر اضافة View.OnClickListener إلى غرض ImageView. عند النقر , يقوم ImageView  بالتصاغر رجوعا إلى حجم الصورة المصغرة , ويسند قيمة الرؤية visibility للقيمة GONE, وبالتالي يختفي.
private void zoomImageFromThumb(final View thumbView, int imageResId) {
    // If there's an animation in progress, cancel it
    // immediately and proceed with this one.
    if (mCurrentAnimator != null) {
        mCurrentAnimator.cancel();
    }

    // Load the high-resolution "zoomed-in" image.
    final ImageView expandedImageView = (ImageView) findViewById(
            R.id.expanded_image);
    expandedImageView.setImageResource(imageResId);

    // Calculate the starting and ending bounds for the zoomed-in image.
    // This step involves lots of math. Yay, math.
    final Rect startBounds = new Rect();
    final Rect finalBounds = new Rect();
    final Point globalOffset = new Point();

    // The start bounds are the global visible rectangle of the thumbnail,
    // and the final bounds are the global visible rectangle of the container
    // view. Also set the container view's offset as the origin for the
    // bounds, since that's the origin for the positioning animation
    // properties (X, Y).
    thumbView.getGlobalVisibleRect(startBounds);
    findViewById(R.id.container)
            .getGlobalVisibleRect(finalBounds, globalOffset);
    startBounds.offset(-globalOffset.x, -globalOffset.y);
    finalBounds.offset(-globalOffset.x, -globalOffset.y);

    // Adjust the start bounds to be the same aspect ratio as the final
    // bounds using the "center crop" technique. This prevents undesirable
    // stretching during the animation. Also calculate the start scaling
    // factor (the end scaling factor is always 1.0).
    float startScale;
    if ((float) finalBounds.width() / finalBounds.height()
            > (float) startBounds.width() / startBounds.height()) {
        // Extend start bounds horizontally
        startScale = (float) startBounds.height() / finalBounds.height();
        float startWidth = startScale * finalBounds.width();
        float deltaWidth = (startWidth - startBounds.width()) / 2;
        startBounds.left -= deltaWidth;
        startBounds.right += deltaWidth;
    } else {
        // Extend start bounds vertically
        startScale = (float) startBounds.width() / finalBounds.width();
        float startHeight = startScale * finalBounds.height();
        float deltaHeight = (startHeight - startBounds.height()) / 2;
        startBounds.top -= deltaHeight;
        startBounds.bottom += deltaHeight;
    }

    // Hide the thumbnail and show the zoomed-in view. When the animation
    // begins, it will position the zoomed-in view in the place of the
    // thumbnail.
    thumbView.setAlpha(0f);
    expandedImageView.setVisibility(View.VISIBLE);

    // Set the pivot point for SCALE_X and SCALE_Y transformations
    // to the top-left corner of the zoomed-in view (the default
    // is the center of the view).
    expandedImageView.setPivotX(0f);
    expandedImageView.setPivotY(0f);

    // Construct and run the parallel animation of the four translation and
    // scale properties (X, Y, SCALE_X, and SCALE_Y).
    AnimatorSet set = new AnimatorSet();
    set
            .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
                    startBounds.left, finalBounds.left))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
                    startBounds.top, finalBounds.top))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
            startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,
                    View.SCALE_Y, startScale, 1f));
    set.setDuration(mShortAnimationDuration);
    set.setInterpolator(new DecelerateInterpolator());
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCurrentAnimator = null;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            mCurrentAnimator = null;
        }
    });
    set.start();
    mCurrentAnimator = set;

    // Upon clicking the zoomed-in image, it should zoom back down
    // to the original bounds and show the thumbnail instead of
    // the expanded image.
    final float startScaleFinal = startScale;
    expandedImageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mCurrentAnimator != null) {
                mCurrentAnimator.cancel();
            }

            // Animate the four positioning/sizing properties in parallel,
            // back to their original values.
            AnimatorSet set = new AnimatorSet();
            set.play(ObjectAnimator
                        .ofFloat(expandedImageView, View.X, startBounds.left))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.Y,startBounds.top))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.SCALE_X, startScaleFinal))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.SCALE_Y, startScaleFinal));
            set.setDuration(mShortAnimationDuration);
            set.setInterpolator(new DecelerateInterpolator());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }
            });
            set.start();
            mCurrentAnimator = set;
        }
    });
}

اتمنى لكم الفائدة ,وإلى لقاء في حلقة قريبة , وإلى ذلك الحين استودعكم الله والسلام عليكم ورحمة الله وبركاته

Advertisements

, , , , , , , , , , , , , , , , , , ,

أضف تعليق

39 -اندرويد : اضافة التحريك عند تغير عناصر التنسيق Animating Layout Changes

 

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : اضافة التحريك عند تغير عناصر التنسيق Animating Layout Changes

السلام عليكم ورحمة الله وبركاته

سوف نتعلم ضمن هذا الدرس مايلي :

  1. انشاء التنسيق Create the Layout
  2. اضافة , تحديث , او حذف العناصر ضمن التنسيق Add, Update  or Remove Items from the Layout

بالامكان تحميل المثال من الرابط التالي : Download the sample app

 إن عملية تحريك التنسيق layout animation عبارة عن عملية تحريك مسبقة التحميل pre-loaded  يقوم النظام بتشغيلها في كل مرة تقوم فيها باجراء تعديل على اعدادات التنسيق.

كل ما تحتاج للقيام به هو فعليا اسناد خاصية للتنسيق تخبر نظام اندرويد بان يقوم بالتحريك في كل مرة تحدث فيها تغيرات على عناصر هذا التنسيق, وبالتالي سوف يتم تطبيق التحريك الافتراضي على هذه العناصر.

ملاحظة : في حال رغبت باضافة نوع معين من التحريك, قم بإنشاء غرض من نوع LayoutTransition, ومن ثم قم بتزويد التنسيق به عبر التابع setLayoutTransition().

فيما يلي (من الرابط ) فيديو يوضح كيف يبدو اللتحريك الافتراضي  عند تطبيقه على التنسيق وذلك عند اضافة عناصر إلى القائمة (ضمن التنسيق المطروح في المثال):

في حال رغبتم بالانتقال بسرعة إلى المثال العملي , بالامكان تحميله من الرابط التالي “تحميل”, يحوي  عدد من الملفات يهمكم منها الملفات التالية :

 

 

  1. src/LayoutChangesActivity.java
  2. layout/activity_layout_changes.xml
  3. menu/activity_layout_changes.xml

إنشاء التنسيق Create the Layout

ضمن ملف التنسيق XML  layout,قم باسناد الخاصة التالية للتنسيق android:animateLayoutChanges  وضع لها القيمة true , وذلك للتنسيق الذي ترغب أن تفعل عملية التحريك عليه : على سبيل المثال:

 

<LinearLayout android:id="@+id/container"
    android:animateLayoutChanges="true"
    ...
/>

 

اضافة , تعديل , او حذف عناصر من التنسيق add, update , or remove items from the layout

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

private ViewGroup mContainerView;
...
private void addItem() {
    View newView;
    ...
    mContainerView.addView(newView, 0);
}

 ببساطة… الموضوع بهذه السهولة

اتمنى لكم الفائدة ,وإلى لقاء في حلقة قريبة , وإلى ذلك الحين استودعكم الله والسلام عليكم ورحمة الله وبركاته

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

, , , , , , , , , , , , , , , , , ,

تعليق واحد

38 -اندرويد : استخدام ViewPager للانتقال بين الشاشات Using ViewPager for Screen Slides

 

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : استخدام ViewPager للانتقال بين الشاشات Using ViewPager for Screen Slides 

سوف نتعلم ضمن هذا الدرس مايلي:

  1. إنشاء ال views
  2. إنشاء Fragment
  3. اضافة  ViewPager
  4. التحكم بالحركة عبر استخدام PageTransformer

بالامكان استعراض المثال الموجود في الرابط التالي : Download the sample app

انزلاق الشاشة screen slides هي عبارة عن عملية انتقال من شاشة إلى اخرى , وتستخدم بشكل عام في واجهات المستخدم UIs مثل wizards او slideshows.

يبين لنا هذا الدرس كيف بإمكاننا الانزلاق بين الشاشات عبر استخدام  ViewPager  المزود من قبل “مكتبة الدعم” support library .

بإمكان ViewPagers  ان تقوم بتحريك عملية الانزلاق بين الشاشات بشكل اوتوماتيكي.

فيما يلي ادناه الرابط يبين كيف تبدو عملية انزلاق الشاشات :

pageViewer

لتحميل المثال انقر على الرابط التالي : انقر هنا

 

في حال رغبت ان تتجاوز بعض الامور وترى المثال الكامل , بإمكانك تحميله من الرابط التالي: , وتشغيل التطبيق لترى مثال screen slide.

سوف ترى ضمن التطبيق عدد من الملفات , يهمك منها الملفات التالية :

  • src/ScreenSlidePageFragment.java
  • src/ScreenSlideActivity.java
  • layout/activity_screen_slide.xml
  • layout/fragment_screen_slide_page.xml

إنشاء ال views

قم بإنشاء ملف تنسيق لتستخدمه لاحقا كمحتوى لل fragment.

يحوي المثال التالي على text view تعرض بعض النصوص:

<com.example.android.animationsdemo.ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <TextView style="?android:textAppearanceMedium"
            android:padding="16dp"
            android:lineSpacingMultiplier="1.2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/lorem_ipsum" />

</com.example.android.animationsdemo.ScrollView>

انشاء ال Fragment

قم بإنشاء صف من نوع Fragment يعيد بدوره التنسيق الذي قمنا بإنشاه للتو ضمن التابع onCreateView().

من ثم يصبح بإمكانك انشاء مستنسخ من هذا ال fragment ضمن الفعالية الأب , وذلك في كل مرة تحتاج فيها إلى انشاء صفحة جديدة يتم عرضها للمستخدم:

public class ScreenSlidePageFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.fragment_screen_slide_page, container, false);

        return rootView;
    }
}

اضافة viewPager

يملك العنصر ViewPager, آلية داخلية ضمنه تمكنه من الانتقال بين الصفحات, وتقوم  بشكل افتراضي  بعرض تحريك يحاكي الانزلاق بين الصفحات.

لنبدأ بالعمل , قم بإنشاء تنسيق يحوي على ViewPager:

نستخدم pageAdapter  ليزودنا بالصفحات المطلوب الانزلاق بينها.

<android.support.v4.view.ViewPager
xmlns:android=”http://schemas.android.com/apk/res/android&#8221;
android:id=”@+id/pager”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />

قم بإنشاء فعالية تقوم بالامور التالية :

  1. نحدد التنسيق الخاص ب contentView ليكون التنسيق الذي يحوي على ViewPager.
  2. ننشأ صف يقوم ب extends للصف المجرد FragmentStatePagerAdapter  ويقوم بتنجيز التابع  getItem()لكي يزوده بمستنسخات من ScreenSlidePageFragment بمثابة صفحات جديدة. كذلك يحتاج pager adapter  إلى ان نقوم بتنجيز التابع getCount() , الذي بدوره يعيد عدد الصفحات التي سوف ينشأها المحول adapter ( خمسة على سبيل المثال).
  3. ربط PagerAdapter  مع ViewPager.
  4. معالجة والتعامل مع كبسة “الرجوع” back ضمن الجهاز, عبر ازالة التراجع من virtual stack  الخاص بال fragments.في حال كان المستخدم ضمن الصفحة الاولى, عندها يتم الرجوع إلى مكدس الرجوع الخاص بالفعالية activity back stack.
public class ScreenSlidePagerActivity extends FragmentActivity {
    /**
     * The number of pages (wizard steps) to show in this demo.
     */
    private static final int NUM_PAGES = 5;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next wizard steps.
     */
    private ViewPager mPager;

    /**
     * The pager adapter, which provides the pages to the view pager widget.
     */
    private PagerAdapter mPagerAdapter;

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

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = (ViewPager) findViewById(R.id.pager);
        mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager());
        mPager.setAdapter(mPagerAdapter);
    }

    @Override
    public void onBackPressed() {
        if (mPager.getCurrentItem() == 0) {
            // If the user is currently looking at the first step, allow the system to handle the
            // Back button. This calls finish() on this activity and pops the back stack.
            super.onBackPressed();
        } else {
            // Otherwise, select the previous step.
            mPager.setCurrentItem(mPager.getCurrentItem() - 1);
        }
    }

    /**
     * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
     * sequence.
     */
    private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
        public ScreenSlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return new ScreenSlidePageFragment();
        }

        @Override
        public int getCount() {
            return NUM_PAGES;
        }
    }
}

التحكم بالتحريك عبر PageTransformer

لكي نقوم بعرض عدة انواع تحريك مختلفة من نفس الشاشة التي نعرض من خلالها انزلاق الشاشات, يجب القيام بتنجيز الواجهة ViewPager.PageTransformer  ومن ثم نقوم بتزويد view pager  بها.

تحوي الواجهة على تابع وحيد, transformPage(). عند كل نقطة من نقاط الانتقال ضمن الشاشة , يتم استدعاء هذا التابع لكل صفحة مرئية ( عادة يكون هنالك صفحة وحيدة مرئية في وقت واحد) وتقوم بالتحكم بتوضيع الصفحات ضمن الشاشة. على سبيل المثال, في حال كانت الصفحة رقم 3 مرئية , وقام المستخدم بسحبها باتجاه الصفحة الرابع, عندها يتم استدعاء التابع transformPage(), وذلك بالنسبة لكل من الصفحتين الثانية والثالثة والرابعة وذلك ضمن كل خطوة ضمن التحرك.

اثناء تنجيزك للتابع transformPage(), يصبح بإمكانك انشاء تحريك على شكل سلايدات بالشكل الذي يناسبك , وذلك عبر تحديد الصفحات التي يتوجب ان تنتقل اعتمادا على موقع الصفحة ضمن الشاشة, والذي يتم استحصاله عبر معامل position  الخاص بالتابع transformPage().

يشير معامل position,إلى موضع الصفحة المعطاة نسبة إلى مركز الشاشة.

انه عبارة عن خاصية ديناميكية تتغير مع انزلاق المستخدم بين الصفحات.

عندما تملأ احد الصفحات الشاشة, فإن قيمة المعامل position  يصبح مساوي للصفر. اما عندما يتم رسم الصفحة على الجانب اليميني للشاشة , عندها فإن قيمة موضعها تصبح مساوية ل 1.اما في حال قام المستخدم بالانزلاق بقدار نصف المسافة بين صفحتين الاولى والثانية, عندها يصبح للصفحة الاولى القيمة -0.5, وللصفحة الثانية القيمة  0.5, لمتحول الموضع.
بناءً على موضع الصفحات ضمن الشاشة , يصبح بإمكانك انشاء طريقة التحريك التي ترغب بها عبر تعديل وتغير قيم مواصفات الصفحة عبر التوابع امثال setAlpha() , setTranslationX() أو setScaleY().

ما إن يصبح لديك تنجيز للواجهة PageTransformer , قم باستدعاء التابع setPageTransformer() ضمن تنجيزك وذلك لكي يتم تطبيقك التحريك الذي قمت بتصميمه.

على سبيل المثال, في حال كان لديك PageTransformer   باسم ZoomOutPageTransformer, عندها بإمكانك اسناد التحريك المعدل الخاص بك كما يلي:

ViewPager pager = (ViewPager) findViewById(R.id.pager);
...
pager.setPageTransformer(true, new ZoomOutPageTransformer());

بالامكان الاطلاع على كل من Zoom-out page transformer وDepth page transformer لرؤية امثلة وفيديوهات حول PageTransformer.

الانتقال بين الصفحات عبر حركة zoom-out

depth
لتحميل المثال انقر على الرابط التالي : انقر هنا

يقوم هذا التحريك على  تصغير shrinks والتلاشي fade الصفحات اثناء الانزلاق بينهم.

ما إن تصبح الصفحة قريبة للمركز, حتى تعود لحجمها الحقيقي وتتلاشى لتظهرfade in.

ZoomOutPageTransformer example

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static float MIN_SCALE = 0.85f;
    private static float MIN_ALPHA = 0.5f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 1) { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA +
                    (scaleFactor - MIN_SCALE) /
                    (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

انتقال الصفحات من العمق Depth page transformer

depth

لتحميل المثال انقر على الرابط التالي : انقر هنا

يستخدم هذا التحويل التحريك الاساسي بشكل سلايدات بين الصفحات باتجاه اليسار, واثناء ذلك يستخدم التحريك بالعمق “depth” animation  للانتقال بين الصفحات إلى اليمين .

يقوم “depth animation” بجعل الصفحات تتلاشى لتختفي, ومن ثم يقوم بتقسيمها وتصغير حجمها بشكل خطي.

 ملاحظة : خلال ‘depth animation’ , يبقى الانزلاق الافتراضي للصفحات ضمن الشاشة مستخدما, لذلك يتوجب عليك ابطال مفعوله (انزلاق الصفحات)عبر اعطاء قيمة سلبية ل X   كما في المثال التالي :

view.setTranslationX(-1 * view.getWidth() * position);

يبين المثال التالي كيفية ابطال مفعول الانزلاق الافتراضي للصفحات ضمن الشاشة وذلك عبر page transformer :

public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

مثال اخر بسيط ورائع على الرابط التالي : انقر هنا

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

, , , , , , , , , , , , , , , ,

أضف تعليق

37 -اندرويد : التلاشي عند الانتقال بين ال views

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : التلاشي عند الانتقال بين ال views 

 السلام عليكم ورحمة الله وبركاته

Crossfading two views

سوف نتعلم ضمن هذا الدرس ما يلي:

  1. إنشاء ال views
  2. اعداد التحريك set up the Animation
  3. تلاشي ال views Crossfade the Views

ملاحظة : بالامكان تجريب المثال عبر تحميله من الرابط التالي : Download the sample app

التحريك بطريقة التلاشي Crossfade animation  (يعرف عادة بالانحلال dissolve): بتم بشكل تدريجي عبر تلاشي احد مكونات واجهة المستخم UI وفي نفس الاثناء تظهر مكونات اخرى من مكونات واجهة المستخدم UI components.

تعتبر طريقة التحريك هذه مفيدة في الحالات التي ترغب فيها بالانتقال من محتوى لاخر بين ال views ضمن تطبيقك.

يعتبر التلاشي بسيطا وقصيرا ولكنه يقدم لنا انتقال سلس من شاشة إلى الشاشة الاخرى.

على كل الاحوال , عندما لا نستخدم هذه الطريقة في الانتقال والتحريك , فسوف تظهر الانتقالات مفاجئة وسريعة.

فيما يلي مثال على تلاشي المؤشر الذي ينقلنا إلى النص

crossfade

بالامكان رؤية المثال من الرابط التالي : انقر هنا 

 

 التحريك بطريقة التلاشي Crossfade animation

إذا رغبت بتجاوز المراحل هنا ورؤية المثال كاملا يعمل , بإمكانك تحميله من الرابط التالي download  وتشغيل التطبيق الموجود فيه , ومن ثم قم باختيار مثال “التلاشي” crossfade.

سوف تجد الملفات التالية التي تحوي على  الكود :

  • src/CrossfadeActivity.java
  • layout/activity_crossfade.xml
  • menu/activity_crossfade.xml

إنشاء ال views:Create the Views

قم بإنشاء Two views التي ترغب بأن تطبق التلاشي عليها. يقوم المثال التالي بإنشاء مؤشر تقدم progress indicator ومساحة نصية قابلة للانزلاق scrollable text view:

 

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView style="?android:textAppearanceMedium"
            android:lineSpacingMultiplier="1.2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/lorem_ipsum"
            android:padding="16dp" />

    </ScrollView>

    <ProgressBar android:id="@+id/loading_spinner"
        style="?android:progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</FrameLayout>

اعداد التحريك Set up the Animation

لاعداد التحريك يتوجب عليك القيام بما يلي :

  1. إنشاء متحولات اعضاء member variables لل views التي نرغب بجعلها تتلاشي. سوف تحتاج إلى هذه المراجع references في وقت لاحق , وذلك عندما تقول بتعديل ال view خلال التحريك.
  2. بالنسبة لل view الذي نرغب بأن يظهر بشكل متلاشي, نسند قيمة visibility إلى القيمة GONE (بالتالي يكون هذا الview مخفي في البداية ولا يأخذ أي حيز ضمن التنسيق).إن اسناد هذه القيمة لل view تجعل من ال view  غير مرئي ولا يأخذ حيز ضمن التنسيق , ويتم تجاهله اثناء حسابات توزع الاماكن في التنسيق, ويسرع المعالجة.
  3. نقوم بخزن خاصية النظام التي تدعى config_shortAnimTime  ضمن متحول عضو member variable. تقوم هذه الخاصية بتعريف فترة زمنية “قصيرة” قياسية للتحريك. تعتبر هذه الفترة مثالية فيما يخص التحريك البسيط أو انواع التحريك التي تحدث بشكل متكرر.
  4. كذلك هنالك كل من خاصيتي config_longAnimTimeو config_mediumAnimTimeوهما ايضا متاحتان للاستخدام في حال رغبت باستخدامهما.

فيما يلي مثال يقوم باستخدام التنسيق الوارد في المثال السابق على انه التنسيق الاساسي ضمن فعالية :

 

public class CrossfadeActivity extends Activity {

    private View mContentView;
    private View mLoadingView;
    private int mShortAnimationDuration;

    ...

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

        mContentView = findViewById(R.id.content);
        mLoadingView = findViewById(R.id.loading_spinner);

        // Initially hide the content view.
        mContentView.setVisibility(View.GONE);

        // Retrieve and cache the system's default "short" animation time.
        mShortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }

 

تلاشي ال views Crossfade the

الآن , وبعد اعداد ال views, نقوم بجعلهم يتلاشون عبر اتباع الخطوات التالية :

  • بالنسبة لل views التي نرغب بجعلها تظهر بشكل متلاشي fading in , نسند قيمة 0 للخاصية alpha , ونسند القيمة VISIBLE لخاصية visibility.(تذكر بأننا في البداية اسندنا القيمة GONEإلى الخاصية visibility).هذا يجعل ال view مرئي ولكنه شفاف تماما.
  • بالنسبة لل view الذي نرغب بان يظهر بشكل متلاشي, نحرك قيمةalpha  من القيمة 0 للقيمة 1.وفي نفس الوقت, بالنسبة لل view الذي يتلاشى ليختفي , نحرك قيمة alpha  من القيمة 1 للقيمة 0.
  • باستخدام التابع onAnimationEnd()وذلك ضمن المتنصت Animator.AnimatorListener, نسند القيمة GONE لخاصية visibility الخاصة ب view الذي يجب ان يتلاشى ليختفي. بالرغم من ان قيمة alpha مساوية للصفر, ولكن اسناد القيمة لخاصية visibility للعنصر الذي نرغب باختفاءه , يمنع الview من ان ياخذ حيز ضمن التنسيق, ويحذفه من ضمن حسابات التنسيق في توزيع المساحة , وبالتالي يسرع المعالجة.

المثال التالي ادناه يبين كيف يتم الامر:

private View mContentView;
private View mLoadingView;
private int mShortAnimationDuration;

...

private void crossfade() {

    // Set the content view to 0% opacity but visible, so that it is visible
    // (but fully transparent) during the animation.
    mContentView.setAlpha(0f);
    mContentView.setVisibility(View.VISIBLE);

    // Animate the content view to 100% opacity, and clear any animation
    // listener set on the view.
    mContentView.animate()
            .alpha(1f)
            .setDuration(mShortAnimationDuration)
            .setListener(null);

    // Animate the loading view to 0% opacity. After the animation ends,
    // set its visibility to GONE as an optimization step (it won't
    // participate in layout passes, etc.)
    mHideView.animate()
            .alpha(0f)
            .setDuration(mShortAnimationDuration)
            .setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mHideView.setVisibility(View.GONE);
                }
            });
}

بالامكان العودة إلى المثال الوادر في الملف المرفق لتطبيقك السيناريو كاملا

وإلى لقاء قريب في الحلقة القادمة التي تتحدث ايضا عن موضوع جديد في مجال التحريك ” ViewPager”

وإلى ذلك الحين استودعكم الله والسلام عليكم ورحمة الله وبركاته

الترجمة

المصطلح

متحول عضو

Member variable

مرجع – أي مرجع للغرض الذي تم انشائه

reference

تحريك

Animation

, , , , , , , , , , , , , , ,

أضف تعليق

36 -اندرويد :اضافة التحريك Adding Animation

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : اضافة التحريك Adding Animation

 السلام عليكم ورحمة الله وبركاته

سوف تتضمن سلسلة الدروس القادمة موضوع التحريك ضمن اندرويد , ارجو لكم الفائدة advantages-of-android-over-ios

المتطلبات

  • نسخ اندرويد 4 او احدث
  • خبرة في بناء واجهة المستخدم في اندرويد

يفضل الاطلاع على

Property Animation

بالامكان تحميل المثال من الرابط التالي :

Download the sample app

بإمكانك اضافة بعض المؤثرات البصرية البسيطة التي تقوم بتنبيه المستخدم حول ما يجري ضمن تطبيقك , وتحسن النموذج الخارجي لواجهة تطبيقك.

يعتبر التحريك مفيد – بشكل خاص – عندما تتغير حالة الشاشة, على سبيل المثال , عندما يتم اعادة تحميل المحتوى بناءً على احداث جديدة.

بإمكانك ايضا اضافة التحريك لكي تحصل على مظهر افضل لتطبيقك , وبالتالي على جودة اظهار خارجي اعلى.

لا تنس بأن استخدام التحريك animation  بالشكل الغير مناسب وفي الوقت الغير مناسب يمكن ان يكون ضار, كأن يتسبب بتأخير العرض.

يبين لنا هذا الدرس كيف بإمكاننا ان نطبق بعض الانماط الشائعة من التحريك على تطبيقاتنا , والتي بدورها تزيد من سهولة استخدام التطبيقات بدون ان تزعج المستخدم.

سوف تتضمن الدروس القادمة ما يلي :

التلاشي بين ال Views ال  Crossfading Two Views

نتعلم فيه كيف بإمكاننا ان نجعل احد ال view  يتلاشى ليظهر ال view  الاخر.

يبين لنا هذا الدرس كيف نجعل المؤشر يتلاشى  ريثما يتم الانتقال إلى ال view الذي يحوي على النص المطلوب.

استخدام ViewPager من اجل عرض الصفحات على الشاشة على شكل سلايدات Using ViewPager for Screen Slides

يبين لنا هذا الدرس كيفية الانتقال بين الشاشات بشكل افقي وذلك عبر sliding transition (كما في السلايدات)

التحريك عبر قلب الصورة Displaying Card Flip Animations

نتعلم فيه كيفية الانتقال بين two views عبر حركة القلب لاحد ال view فيظهر ال view  الاخر

تكبير view Zooming a View

 نتعلم فيه كيف نكبر view عبر النقر على ال view

اضافة تحريك عند حدوث تغيرات ضمن التنسيق Animating Layout Changes

 نتعلم فيه كيف نضيف حركات عند اضافة , حذف أو  تعديل احد ابناء ال view ضمن التنسيق layout

وإلى لقاء قريب في الدرس القادم , وإلى ذلك الحين استودعكم الله والسلام عليكم ورحمة الله وبركاته

, , , , , , , , , , , , , ,

أضف تعليق

35 -اندرويد : Wi-Fi Direct

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : Wi-Fi Direct

السلام عليكم ورحمة الله وبركاته

سيتم ادراج هذا الدرس لاحقا ريما يتم الانتهاء من اعداده واتمام ترجمته

, , , , , , , , , , , , , , , , , , , , , , , , ,

أضف تعليق

34 -اندرويد : Connectivity -بلوتوث Bluetooth

ملاحظة : الموضوع عبارة عن حلقة من حلقات سلسلة برمجة وتطوير اندرويد Android Development

اندرويد : بلوتوث Bluetooth

السلام عليكم ورحمة الله وبركاته

لمحة سريعة :connectivity

تمكن واجهات التطبيقات البرمجية APIs الخاصة ببلوتوث ضمن اندرويد تطبيقك من انجاز عمليات نقل المعطيات اللاسلكي مع بقية التجهيزات.

ضمن هذه المقالة سوف نتحدث عن :

  1. الاساسيات The Basics
  2. سماحيات بلوتوث Bluetooth Permissions
  3. اعداد بلوتوث Setting Up Bluetooth
  4. ايجاد الاجهزة Finding Devices
  5. الاستعلام عن الاجهزة المقترنة Querying paired devices
  6. استكشاف الاجهزة Discovering devices
  7. توصيل الاجهزة Connecting Devices
  8. الاتصال كخادم Connecting as a server
  9. الاتصال كزبون Connecting as a client
  10. الادارة والتحكم بالاتصال Managing a Connection
  11. العمل مع الملفات Working with Profiles
  12. Vendor-specific AT commands
  13. Health Device Profile

الصفوف الاساسية والمهمة

  1. BluetoothAdapter
  2. BluetoothDevice
  3. BluetoothSocket
  4. BluetoothServerSocket

بعض الامثلة ذات العلاقة بالموضوع

  1. Bluetooth Chat
  2. Bluetooth HDP (Health Device Profile)

تتضمن منصة عمل اندرويد دعم لمكدس شكبة بلوتوث Bluetooth network stack, والذي يمكن بدوره الجهاز من تبادل المعطيات بشكل لاسلكي مع بقية اجهزة بلوتوث.

يزود اطار عمل التطبيق امكانية النفاذ إلى خدمات بلوتوث عبر واجهات التطبيقات البرمجية الخاصة ببلوتوث ضمن اندرويد Android Bluetooth APIs. تمكن هذه الواجهات APIs التطبيقات من التواصل بشكل لاسلكي مع بقية الجهزة الي تدعم بلوتوث, لتصبح بذلك ميزات الاتصال اللاسلكي بين نقطة – نقطة أو (بين عدة نقاط) متاحة.

بامكان تطبيقك انجاز المهام التالية عبر استخدام Bluetooth APIs

  • البحث عن اجهزة بلوتوث اخرىScan for other Bluetooth devices
  • الاستعلام من محول بلوتوث الداخلي عن اجهزة بلوتوث المتقرنة Query the local Bluetooth adapter for paired Bluetooth devices
  • انشاء قنوات RFCOMM.Establish RFCOMM channels
  • الاتصال مع بقية الاجهزة عبر خدمة الاستكشاف   Connect to other devices through service discovery
  • نقل المعطيات من وإلى بقية الاجهزة Transfer data to and from other devices
  • التحكم وادارة الاتصالات المتعددة Manage multiple connections

الاساسيات The Basics

تتحدث هذه المقالة عن آلية استخدام واجهات التطبيقات البرمجية الخاصة ببلوتوث ضمن اندرويد Android Bluetooth APIs وذلك بهدف انجاز المهام الاساسية الاربعة الضرورية للتواصل عبر استخدام بلوتوث:

  1. إعداد بلوتوث setting up Bluetooth
  2. ايجاد الاجهزة التي – بكل الاحوال – باحد حالتين : اما مقترنة paired او متاحة ضمن المنطقة الحالية.
  3. الاتصال بين الاجهزة connecting devices
  4. نقل المعطيات بين الاجهزة transferring data between devices

تتواجد كل واجهات بلوتوث Bluetooth APIs ضمن الحزمة  android.bluetooth.

فيما يلي ملخص للصفوف classes والواجهات interfaces التي تحتاجها لإنشاء اتصال بلوتوث:

BluetoothAdapter

تمثل محول بلوتوث محلي local Bluetooth adapter(Bluetooth radio). يعتبر BluetoothAdapter  بمثابة المدخل الاساسي لكل اساليب التواصل والتواصل عبر البلوتوث Bluetooth interactions.

باستخدام BluetoothAdapter يصبح بإمكانك

  • اكتشاف اجهزة البلوتوث الاخرى
  • والاستعلام عن قائمة بالاجهزة المتقرنة paired devices
  • استنساخ BluetoothDevice  عبر استخدام عنوان MAC معروف.
  • إنشاء BluetoothServerSocket  للتصنت والاستماع على الاتصالات بين بقية الاجهزة

BluetoothDevice

ويمثل جهاز بلوتوث عن بعد remote Bluetooth device.

استخدم BluetoothDevice لطلب الاتصال مع جهاز عن بعد وذلك عبر BluetoothSocket أوعبر الاستعلام  عن معلومات الجهاز مثل الاسم name او العنوان  address او الصف class او حالة الاقتران bonding state.

BluetoothSocket

وتمثل واجهة ل Bluetooth socket (بشكل مشابه ل TCP Socket).

عبارة عن نقطة الاتصال التي تمكن تطبيقك من تبادل المعطيات مع بقية اجهزة بلوتوث عبر InputStream  أو OutputStream.

BluetoothServerSocket

وتمثل server socket  مفتوحة تتنصت للطلبات القادمة( بشكل مشابه ل ServerSocketTCP ).

لكي تقوم باجراء اتصال بين جهازي اندرويد, يجب على احد الجهازين ان يفتح server socket  عبر هذا الصف BluetoothServerSocket class. عندما يجري جهاز بلوتوث عن بعد طلب اتصال لهذا الجهاز (أي الجهاز الذي يحوي على BluetoothServerSocket) فإن BluetoothServerSocket سوف يعيد BluetoothSocket متصلة , وذلك عندما يتم قبول الاتصال.

BluetoothClass

يوصف الصفات الامكانيات العامة لجهاز بلوتوث.عبارة عن مجموعة من الخصائص( التي يمكن فقط قرائتها) والتي تعرف الصفوف الاساسية  والثانوية للجهاز مع خدماتهاmajor and minor device classes and its services .

على كل الاحوال فإن هذا الصف لا يوصف عليه بروفايل بلوتوث والخدمات المدعومة من قبل الجهاز  بشكل يمكن الاعتماد, ولكنها مفيدة في الاشارة إلى نوع الجهاز.

BluetoothProfile

عبارة عن واجهة تمثل بروفايل بلوتوث Bluetooth profile.

Bluetooth profile (بروفايل بلوتوث) : عبارة عن خصائص ومواصفات لواجهة لا سكلية تخص الاتصالات  بين الاجهزة التي تعتمد على بلوتوث.

مثال عليها : Hands-Free profile.

لمزيد من المعلومات حول البروفايلات بالامكان الاطلاع على الرابط التالي : Working with Profiles.

BluetoothHeadset

توفر دعم لسماعات بلوتوث Bluetooth headsets التي تستخدم مع اجهزة الموبايل. تضمن كل من بروفايلات  Bluetooth headsets  و Head-Free(v1.5)

BluetoothA2dp

تحدد مدى جودة الصوت الذي يمكن ارساله (عبر stream) من جهاز إلى اخر من خلال اتصال بلوتوث.

إن “A2DP”عبارة عن اختصار  ل Advanced Audio Distribution Profile.

BluetoothHealth

 Represents a Health Device Profile proxy that controls the Bluetooth service

 BluetoothHealthCallback

صف مجرد abstract class  تستخدمه لتحقيق (تنجيز implement) استدعاءات BluetoothHealth.

يجب القيام ب extend لهذا الصف وتنجيز implement توابع الاستدعاء بهدف تلقي التحديثات حول التغيرات في حالة تسجيل التطبيق application’s registration state  وحالة قناة بلوتوث Bluetooth channel state.

BluetoothHealthAppConfiguration

يمثل اعدادات التطبيق التي يسجلها تطبيق Bluetooth Health third-party  بهدف التواصل مع جهاز بلوتوث عن بعد  remote Bluetooth health device .

BluetoothProfile.ServiceListener

واجهة تقوم بتنبيه واخطار BluetoothProfile IPC clients عندما يتم اتصالهم او انفصالهم عن الخدمة(that is, the internal service that runs a particular profile).

سماحيات وصلاحيات بلوتوث Bluetooth Permissions

لكي تستطيع استخدام ميزات بلوتوث ضمن تطبيقك , يتوجب عليك ان تصرح على الأقل عن واحد من الصلاحيات التالية :

BLUETOOTH  أو BLUETOOTH_ADMIN.

يتوجب عليك ان تطلب صلاحية BLUETOOTH  لكي تستطيع ان تنجز أي نوع من انواع اتصالات بلوتوث, مثل عملية طلب اتصال requesting a connection , قبول طلب اتصال accepting a connection  , ونقل المعطيات transferring data.

يتوجب عليك ان تطلب صلاحية BLUETOOTH_ADMIN  لكي تهيئ مستكشف الاجهزة device discovery  أو للتعامل مع اعدادات بلوتوث.

اغلب التطبيقات تحتاج لهذه الصلاحية فقط  بهدف اكتشاف اجهزة  البلوتوث المحلية. اما بالنسبة لبقية الامكانيات التي تتيحها هذه الصلاحية فلا يجب استخدامها, إلا في حال كان التطبيق عبارةعن “power manager” الذي يقوم بتعديل اعدادات بلوتوث بناء على طلب المستخدم.

ملاحظة : في حال استخدمت صلاحية BLUETOOTH_ADMIN, يتوجب عليك ايضا تملك صلاحية BLUETOOTH.

يتم التصريح عن صلاحيات بلوتوث ضمن ملف manifest ضمن تطبيقك. على سبيل المثال:

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest>

لمزيد من المعلومات حول التصريح عن صلاحيات التطبيق بالامكان مراجعة الدليل التالي : <uses-permission>

اعداد بلوتوث Setting Up Bluetooth

bluetooth conectivity

الشكل 1 : صندوق الحوار الخاص بتفعيل بلوتوث

قبل ان يستطيع تطبيقك التواصل عبر بلوتوث , انت بحاجة للتحقق من ان بلوتوث مدعوم على الجهاز, وفي حال كان مدعوما, يجب التحقق من انه مفعل.

في حال لم يكن البلوتوث مدعوما على الجهاز, عندها يتوجب عليك ازالة تفعيل ميزة البلوتوث من تطبيقك.

اما في حال كان البلوتوث مدعوما على الجهاز, ولكنه غير مفعل, عندها بإمكانك ان تطلب من المستخدم ان يفعل البلوتوث بدون مغادرة تطبيقك.

يتم الاعداد عبر الخطوتين التاليتين وذلك باستخدام BluetoothAdapter:

الحصول على BluetoothAdapter:

تعتبر ال BluetoothAdapter  مطلوبة واساسية في كل فعاليات بلوتوث Bluetooth activity.ويعيد هذا الصف  BluetoothAdapter  الذي يمثل محول البلوتوث الخاص بالجهاز ذاته device’s own Bluetooth adapter(the Bluetooth radio).
يوجد فقط محول بلوتوث وحيد Bluetooth adapter للنظام باكمله, وباستطاعة تطبيقك التفاعل معه باستخدام this object. اذا كانت القيمة المعادة من قبل التابع getDefaultAdapter() مساوية ل Null, فهذا يعني بأن الجهاز لا يدعم بلوتوث وتنتهي القصة عند هذه النقطة. على سبيل المثال:

 
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

تفعيل البلوتوث Enable Bluetooth

في المرحلة التالية : انت بحاجة للتحقق من ان البلوتوث مفعل على الجهاز.

نقوم باستدعاء التابع isEnabled() للتحقق فيما اذا كان البلوتوث حاليا مفعل. اذا كانت القيمة المعادة من هذا التابع مساوية ل false , عندها يكون البلوتوث غير مفعل. لكي تطلب ان يكون البلوتوث مفعلا عليك استدعاء التابع startActivityForResult()مع حدث ال intent  التالي : ACTION_REQUEST_ENABLE. سوف يقوم هذا بارسال طلب لتفعيل البلوتوث من خلال اعدادات النظام (بدون ايقاف تطبيقك). على سبيل المثال:

if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

عندها سوف يظهر صندوق حوار يطلب من المستخدم السماح بتفعيل البلوتوث, كما هو مبين في الشكل رقم 1. في حال وافق المستخدم وضغط على “yes”عندها سوف يبدأ النظام بتفعيل البلوتوث وسيعود التركيز إلى تطبيقك ما إن تنتهي الاجرائية (أو تفشل).

 في المثال السابق – تم تميرير الثابت REQUEST_ENABLE_BT إلى التابع startActivityForResult()وهو عبارة عن عدد صحيح معرف محليا (ويجب ان تكون قيمته اكبر من 0), ومن ثم يعيد النظام تمرير هذا الثابت مرة اخرى إليك وذلك ضمن تحقيق التابع onActivityResult()وهو بمثابة معامل “ترميز الطلب requestCode”.

في حال نجحت عملية تفعيل البلوتوث, عندها سوف تتلقى فعاليتك ترميز النتيجة التالي  RESULT_OK  وذلك ضمن استدعاء التابع onActivityResult().  في حال لم يتم تفعيل البلوتوث على الجهاز بسبب خطأ ما ( او في حال ضغط المستخدم على خيار “No”) عندها فإن ترميز النتيجة هو التالي : RESULT_CANCELED.

بشكل خياري , باستطاع تطبيقك ايضا ان يتصنت على ACTION_STATE_CHANGED (الخاصة ب Intent  البث broadcast Intent), والتي سوف يقوم النظام ببثها ما إن تتغير حالة البلوتوث. يحوي هذا البث على حقول اضافية : EXTRA_STATE و EXTRA_PREVIOUS_STATE تحوي على حالة البلوتوث القديمة والحديثة بالترتيب. القيم المحتملة لهذه الحقول الاضافية هي التالية :

يعتبر الانصات لهذا البث مفيد جدا لاكتشاف التغيرات التي تطرأ على حالة البلوتوث اثناء عمل تطبيقك.

ملاحظة : إن تفعيل الاستكشاف discoverability سوف يفعل البلوتوث بشكل تلقائي. في حال كنت تخطط بشكل دائم لتفعيل مستكشف الاجهزة قبل تنفيذ فعالية البلوتوث , عندها بإمكانك تجاوز الخطوة 2 اعلاه. بالامكان قراءة المزيد حول enabling discoverability ادناه.

ايجاد الاجهزة Finding Devices

بإمكانك عبر استخدام BluetoothAdapterايجاد اجهزة بلوتوث اما عبر مستكشف الاجهزة device discovery او عبر الاستعلام عن قائمة الاجهزة المقترنة paired (bonded)devices.

استكشاف الاجهزة Device discovery  : عبارة عن اجرائية مسح تقوم بالبحث ضمن المنطقة المحلية عن اجهزة البلوتوث المفعلة ومن ثم تطلب بعض المعلومات عن كل واحد منها (يشار في بعض الاحيان إلى هذه العملية ب ” discovering” , “inquiring” , “scanning”  ).

على كل الاحوال, سوف تستجيب اجهزة البلوتوث ضمن المنطقة المحلية إلى طلب الاكتشاف فقط في حال كانت مفعلة لأن يتم اكتشافها. في حال كان الجهاز قابل للاستكشاف, عندها فإنه سوف يستجيب لطلب الاستكشاف discovery request عبر مشاركة بعض المعلومات , مثل اسم الجهاز device name, الصف class , بالاضافة إلى عنوان MAC الفريد. باستخدام هذه المعلومات, يصبح بإمكان الجهاز الذي يقوم بالاستكشاف ان يختار ان يبدأ بعملية اتصال مع الاجهزة  التي تم ايجادها.

ما إن يتم لأول مرة انشاء الاتصال مع جهاز عن بعد, حتى يظهر طلب اقتران pairing request  بشكل اوتوماتيكي للمستخدم. عندما يقترن الجهاز, فإن المعلومات الاساسية حول الجهاز (مثل اسم الجهاز, الصف , عنوان ال MAC) سيتم حفظها ويمكن قرائتها بواسطة واجهات بلوتوث Bluetooth APIs.

باستخدام عنوان ال MAC المعروف لجهاز عن بعد , يصبح بالامكان بدء اتصال مع ذاك الجهاز في أي وقت بدون القيام بعملية البحث (حيث يعتبر الجهاز ضمن المجال).

يجب ان تتذكر بأن هنالك اختلاف بين كون الجهاز مقترن , وبين كونه متصل.

ان يكون الجهاز مقترن paired يعني بأن كلا الجهازين يعلمان بوجود بعضهما البعض, ويوجد بينهما shared link-key يمكن استخدمها بهدف التوثق authentication, وقادران على انشاء اتصال مشفر بين بعضهما البعض.

اما ان يكون الجهاز متصل connected فهذا يعني بأن الاجهزة تتشارك حاليا قناة RFCOMM وهي قادرة على ارسال المعطيات لبعضها البعض.

إن واجهات بلوتوث ضمن اندرويد الحالية Android Bluetooth API’s تتطلب ان يكون الجهازان مقترنان paired قبل انشاء اتصال من نوع RFCOMM.(يتم انجاز الاقتران بشكل اوتوماتيكي عندما تبدأ باتصال مشفر عبر Bluetooth APIs).

توصف المقاطع التالية كيفية ايجاد الاجهزة التي تم الاقتران بها, او اكتشاف الاجهزة الجديدة عبر استخدام “مكتشف الاجهزة” device discovery.

ملاحظة :لا تعتبر الاجهزة التي تحوي نظام تشغيل اندرويد قابلة للاكشاف بشكل افتراضي . يستطيع المستخدم ان يجعل الجهاز قابل للاستكشاف لوقت محدود عبر اعدادات النظام. سيتم فيما بعض مناقشة “تفعيل امكانية الاستكشاف  enable discoverabilityفيما بعد”.

الاستعلام عن الاجهزة المتقرنة querying paired devices

قبل القيام باجرائية استكشاف الاجهزة, يجدر الاستعلام عن الاجهزة المتقرنة لمعرفة فيما اذا كان الجهاز المطلوب ضمنها للتو. للقيام بذلك , يتم استدعاء التابع getBondedDevices() . سوف يعيد هذا التابع مجموعة من BluetoothDevices  تمثل الاجهزة المقترنة.

على سبيل المثال, بالإمكان الاستعلام عن كل الاجهزة المقترنة ومن ثم يتم اظهار اسم كل جهاز للمستخدم , باستخدام ArrayAdapter:

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
    // Loop through paired devices
    for (BluetoothDevice device : pairedDevices) {
        // Add the name and address to an array adapter to show in a ListView
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}

كل ما يحتاجه غرض BluetoothDevice لكي يبدأ اتصال هو عنوان ال MAC.

في هذا المثال, يتم حفظه على انه جزء من ArrayAdapter التي يتم اظهارها للمستخدم.

يمكن فيما بعد استخلاص عنوان ال MAC لكي نبدأ الاتصال. بالامكان تعلم المزيد حول انشاء الاتصال ضمن قسم Connecting Devices.

استكشاف الاجهزة Discovering devices

لكي تبدأ باستكشاف الاجهزة, بإمكانك ببساطة استدعاء التابع startDiscovery().تعتبر هذه الاجرائية غير متزامنة asynchronous  , وسوف يعيد هذا التابع فورا قيمة منطقية تشير فيما اذا كان الاستكشاف قد بدأ بشكل ناحج ام لا. عادة ما تتضمن اجرائية الاستكشاف استعلام ومسح يستمر حوالي 12 ثانية , ويتبعه مسح لكل جهاز يتم العثور عليه بهدف استعادة اسم البلوتوث.

يتوجب على تطبيقك ان يسجل BroadcastReceiver  من اجل intent  ACTION_FOUNDلكي يتلقى المعلومات حول الاجهزة المستكشفة.

Your application must register a BroadcastReceiver for the ACTION_FOUND Intent in order to receive information about each device discovered

بالنسبة لكل جهاز, سوف يقوم النظام ببث intent  من نوع ACTION_FOUND.  تحمل هذه ال intent الحقول الاضافية EXTRA_DEVICEو EXTRA_CLASS , وتحوي BluetoothDevice  و BluetoothClass, بالترتيب. على سبيل المثال, فيما يلي مثال يوضح كيف بإمكانك التسجيل بهدف التعامل مع البث عندما يتم استكشاف الاجهزة:

  

// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        // When discovery finds a device
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Get the BluetoothDevice object from the Intent
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

كل ما هو مطلوب من غرض BluetoothDevice  لكي يستطيع ان يبدأ الاتصال هو عنوان ال MAC.ضمن المثال السابق, يتم حفظ MAC address كجزء من ArrayAdapter الذي يتم اظهاره للمستخدم.

يمكن فيما بعد استخلاص عنوان ال MAC لكي نبدأ الاتصال.بالامكان تعلم المزيد حول انشاء الاتصال في القسم الخاص ب “الاتصال بين الاجهزة Connecting Devices“.

تنبيه : تعتبر عملية استكشاف الاجهزة اجرائية ثقيلة بالنسبة لمحول البلوتوث Bluetooth adapter وسوف تستهلك الكثير من الموارد. لذلك ما ان تجد جهاز لتتصل معه , تاكد من ايقاف عملية البحث عبر استدعاء التابع  cancelDiscovery()قبل محاولة القيام بالاتصال. ايضا في حال كنت للتو تملك اتصال مع جهاز ما , عندها فإن انجاز عملية الاستكشاف سوف يقوم بتقليل عرض الحزمة bandwidth المتاح بشكل كبير وخصوصا بالنسبة للاتصال, لذلك لا يتوجب عليك القيام بالبحث واستكشاف الاجهزة مادمت متصلا مع احد الاجهزة.

تفعيل الاستكشاف Enabling discoverability

في حال كنت ترغب بان تجعل جهاز محلي قابل للاستكشاف من قبل بقية الاجهزة , قم باستدعاء التابع startActivityForResult(Intent, int) مع حدث ال Intent  التالي   ACTION_REQUEST_DISCOVERABLE.  سوف يقوم هذا بارسال اشعار بطلب تفعيل نمط الاستكشاف عبر اعدادات النظام ( بدون ايقاف تطبيقك).

بشكل افتراضي , سوف يصبح الجهاز قابل للاستكشاف لمدة 120 ثانية.بإمكانك تعريف وتحديد فترة زمنية اخرى عبر اضافة EXTRA_DISCOVERABLE_DURATIONضمن المعاملات الاضافة extra الخاصة بتلك ال intent. اقصى قيمة للفترة الزمنية الخاصة التي يكون فيها الاستكشاف مفعلا تبلغ 3600 ثانية, اما القيمة 0 فهي تعني بأن الجهاز دوما قابل للاستكشاف. واي قيمة ضمن المجال 0 للقيمة 3600 فإنها سوف تسند بشكل اوتوماتيكي إلى القيمة 120 ثانية. على سبيل المثال, يقوم الكود ادناه بتحديد قيمة الفترة ب 300 :

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
bluetooth request

الشكل 2 : صندوق الحوار الخاص بتفعيل الاستكشاف.

سوف يتم عرض صندوق حوار, يطلب من المستخدم الاذن لجعل الجهاز قابل للاستكشاف, كما هو موضح ضمن الشكل 2. في حال ضغط المستخدم على كلمة “Yes” عندها سوف يصبح الجهاز قابل للاستكشاف وذلك ضمن الفترة المحددة من الزمن.

بعدها سوف تتلقى فعاليتك الاستدعاء onActivityResult())  مع ترميز نتيجة result code  مساوي للتفرة التي كان فيها الجهاز قابل للاستكشاف.

اما في حال ضغط المستخدم على “No” اوحدث خلل ما , عندها سوف يكون ترميز النتيجة مساوي ل RESULT_CANCELED.

ملاحظة : في حال لم يكن البلوتوث مفعل على الجهاز, عندها فإن تفعيل استكشاف الجهاز سوف يفعل بشكل اوتوماتيكي بلوتوث.

سوف يبقى الجهاز بشكل صامت ضمن نمط الاستكشاف خلال الفترة الزمنية المحددة. في حال كنت ترغب بأن يتم اعلامك عند تغير نمط الاستكشاف , عليك بتسجيل BroadcastReceiver لل Intent : ACTION_SCAN_MODE_CHANGED

سوف يحوي هذا ايضا على حقول اضافية  EXTRA_SCAN_MODEو EXTRA_PREVIOUS_SCAN_MODE, والتي تخبرك بقيم نمط المسح الجديدة والقديمة  بالترتيب.

القيم المحتملة لكل منها هي التالية :

SCAN_MODE_CONNECTABLE_DISCOVERABLE

SCAN_MODE_CONNECTABLE

SCAN_MODE_NONE

والتي تشير بأن الجهاز إما ضمن نمط الاستكشاف, او ليس ضمن نمط الاستكشاف ولكن لا يزال قادر على استقبال الاتصالات, او ليس ضمن نمط الاستكشاف وغير قادر على استقبال الاتصالات بالترتيب.

لا تحتاج إلى تفعيل استكشاف الجهاز في حال كنت ترغب ببدء الاتصال مع جهاز بعيد.

إن تفعيل الاستكشاف ضروري فقط عندما ترغب بأن يستضيف تطبيقك server socket التي سوف تقوم بدورها بتلقي الاتصالات connections, لأن الاجهزة عن بعد يجب ان تكون قادرة على اكتشاف الاجهزة التي ترغب الاتصال معها قبل ان تبدأ الاتصال معها.

الاتصال بالاجهزة Connecting Devices

لكي تكون قادرا على انشاء اتصال بين تطبيقك الخاص بك على جهازين, يتجب عليك ان تقوم بتنجيز implement كل من آليتي server-side  و client-side, لأن احد الجهازين يجب ان يفتح server socket , ويتوجب على الجهاز الآخر ان يبدأ الاتصال initiate the connection (وذلك باستخدام عنوان ال MAC الخاص بالجهاز الذي يعمل كمخدم ليبدأ الاتصال).

يعتبر كل من جهاز المخدم server  وجهاز الزبون client متصلان ببعضهما البعض عندما يملك كل منها BluetoothSocket  متصلة (connected)على نفس قناة RFCOMM.

عند هذه النقطة, يستطيع كل من الجهازين الحصول على مجاري دخل وخرج input/output streams ويمكن عند هذه النقطة بدء نقل المعطيات data transfer, والذي ستتم مناقشته ضمن القسم الذي يتحدث عن “ادارة الاتصالManaging a Connection“.

يوصف هذا الفصل كيفية بدء اتصال بين جهازين.

يحصل كل من جهاز المخدم server device  وجهاز العميل client device على BluetoothSocket  بطرق مختلفة.

يتلقى المخدم BluetoothSocket  عندما يتم قبول طلب اتصال قادم إليه.

يتلقى العميل BluetoothSocket  عندما يقوم بفتح قناة RFCOMM  مع المخدم server.

bluetooth pairing request

الشكل 3 :صندوق حوار اقتران البلوتوث

   

 أحد التقنيات المستخدمة لتنجيز ذلك هي بأن نجهز كل جهاز من الاجهزة على انه المخدم server, وبذلك فإن كل جهاز من الاجهزة يكون لديه امكانية بدء اتصال مع الجهاز الآخر الذي بدوره سوف يصبح العميل.

الطريقة البديلة هي التالية :بإمكان احد الاجهزة ان يستضيف host الاتصال (connection)  بشكل صريح ويقوم بفتح server socket بحسب الطلب , وبإمكان الجهاز الاخر ان يبدأ الاتصال ببساطة.

ملاحظة : في حال لم يكن كلا الجهازين قد اقرنا من قبل, عندها فإن اطار عمل اندرويد سوف يقوم بشكل اوتوماتيكي  بعرض تنبيه لطلب اقتران أو صندوق حوار للمستخدم خلال اجرائية الاتصال, كما هو مبين ضمن الشكل 3. لذلك عند محاولة الاتصال بين الاجهزة , لا يحتاج تطبيقك لأن يهتم فيما اذا كان الجهازين مقترنين ام لا. وسيتم حجب محاولات الاتصال عبر RFCOMM حتى يتم الاقتران بنجاح, او انه سيفشل في حال رفض المستخدم طلب الاقتران , او في حال فشل الاقتران , او في حال نفذ الوقت.

الاتصال كمخدم Connecting as a server

عندما ترغب بوصل جهازين, فإنه يتوجب على احد الجهازين ان يتصرف كمخدم وذلك عبر عقد BluetoothServerSocketمفتوحة.

الهدف من server socket هي الانصات إلى طلبات الاتصال القادمة , وعندما يتم قبول احد هذه الطلبات, يقوم المخدم بتزويد connected BluetoothSocket.  عندما يتم الحصول على BluetoothSocket  من BluetoothServerSocket,  فإنه يتم تجاهل ال BluetoothServerSocket, إلا في حال كنت ترغب بالحصول على المزيد من الاتصالات(connections).

حول UUID

UUID  : المعرف الفريد عالميا A universally Unique Identifier,

يستخدم كمعرف فريد للمعطيات.

الميزة المهمة ل UUID هي انه كبير بما فيه الكفاية ليصبح بإمكانك ان تختار أي قيمة عشوائية ضمن مجاله بدون ان تخاف من تضارب القيم.

في حالتنا, يستخدم كمعرف identifier فريد لخدمة بلوتوث الخاصة بتطبيقاتنا.

لكي تحصل على UUID لكي تستخدمه ضمن تطبيقك, بإمكانك استخدام واحد من العديد من مولدات UUID العشوائية الموجودة على الانترنت, ومن ثم تقوم بتهيئة UUID  ب fromString(String).

فيما يلي الاجرائية الاساسية لاعداد server socket ولكي تقبل الاتصال accept a connection:

  1. الحصول على BluetoothServerSocket  عبر استدعاء listenUsingRfcommWithServiceRecord(String, UUID).
    السلسلة المحرفية عبارة عن اسم قابل للتمييز لخدمتك identifiable name , و سوف يقوم النظام بشكل اوتوماتيكي بالكتابة ضمن مدخل جديد ضمن قاعدة لمعطيات (SDP:Service Discovery Protocol) على الجهاز(الاسم عشوائي وببساطة يمكن ان يكون اسم تطبيقك).
    يتم تضمين ال UUID ضمن مدخل SDP وسيكون بمثابة العنصر الاساسي فيما يخص معاملات (المحاورات التي تجري بين الجهازين ) الاتصال مع الجهاز الزبون.
    عندما يحاول الزبون الاتصال مع الجهاز, فإنه سوف يحمل ال UUID الذي يميز بشكل فريد الخدمة التي ترغب بالاتصال بها.
    يجب ان تكون هذه ال UUIDs متطابقة لكي يتم قبول الاتصال(في الخطوة التالية).
  2. البدء بالتصنت لطلبات الاتصال عبر استدعاء  accept().
    هذا الاستدعاء يمثل استدعاء حجب blocking call (أي انه يقوم بمنع أي نوع اخر من التفاعلات ضمن التطبيق).يعود هذا الاستدعاء في احد حالتين , اما عندما يتم قبول الاتصال , او عند حدوث استثناء ما. يتم قبول الاتصال فقط عندما يرسل جهاز بعيد طلب اتصال ب UUID مطابقة لتلك المسجلة مع server socket  الذي يقوم بالتنصت. عند نجاح العملية سوف يعيد التابعaccept() connected BluetoothSocket.
  3. في حال لم ترغب بقبول المزيد من الاتصالات , عندها يتوجب عليك استدعاء التابعclose().
    يقوم هذا التابع بتحرير server socket وكل الموارد, ولكنه لا يقوم باغلاق connected BluetoothSocket التي تمت اعادتها من قبل التابع accept().على نقيض  TCP/IP فإن RFCOMM يسمح فقط لزبون متصل واحد على القناة في نفس الوقت, لذلك فإنه في اغلب الحالات من المنطقي استدعاء close()على  BluetoothServerSocketفورا بعد قبول a connected socket.

لا يتوجب ان يتم استدعاء التابع accept()في نفس thread  الخاص بواحهة المستخدم الخاصة بالفعالية الاساسية وذلك لانه عبارة عن استدعاء حجب blocking call الذي سيقوم بدوره بمنع أي نوع اخر من التفاعلات ضمن التطبيق. عادة من المنطقي ان نقوم بكل العمل المتضمن ل BluetoothServerSocket  أو BluetoothSocket  ضمن مجرى جديد New thread تتم ادارته والتحكم به من قبل تطبيقك.

لكي نجهض وننهي استدعاء حضر ما blocked call , ,يتم استدعاء التابع accept()على غرض BluetoothServerSocket  (أو BluetoothSocket ) من مسرى آخرanother thread  و بذلك سوف يتم انهاء استدعاء الحضر blocked call  بشكل فوري (وسوف يعود return).
لاحظ بأن كل التوابع الخاصة ب BluetoothServerSocket  أو BluetoothSocket عبارة عن توابع thread-safe.

مثال:

فيما يلي مثال على مسرى بسيط لمكون مخدم server component مسؤول عن تلقي طلبات الاتصال القادمة

privateclassAcceptThreadextendsThread{
    privatefinalBluetoothServerSocket mmServerSocket;

    publicAcceptThread(){
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp =null;
        try{
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        }catch(IOException e){}
        mmServerSocket = tmp;
    }

    publicvoid run(){
        BluetoothSocket socket =null;
        // Keep listening until exception occurs or a socket is returned
        while(true){
            try{
                socket = mmServerSocket.accept();
            }catch(IOException e){
                break;
            }
            // If a connection was accepted
            if(socket !=null){
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    /** Will cancel the listening socket, and cause the thread to finish */
    publicvoid cancel(){
        try{
            mmServerSocket.close();
        }catch(IOException e){}
    }}

في المثال السابق, المطلوب قبول طلب اتصال وحيد connection, ما إن يتم قبول طلب اتصال ما ويتم الحصول على BluetoothSocket, حتى يقوم التطبيق بارسال BluetoothSocketالمطلوبة إلى مسرى منفصل , ويغلق  BluetoothServerSocket ويكسر الحلقة.

لاحظ بأنه عندما يعيد التابع accept(), ال BluetoothSocket, فإن ال socket تكون متصلة للتو , لذلك لست بحاجة لاستدعاء التابع connect() (كما تفعل عندما تكون من طرف الزبون client –side).

التابع manageConnectedSocket() عبارة عن تابع متخيل ضمن التطبيق, مهمته القيام بتهيئة المسرى initiate the thread  بهدف نقل المعطيات , والتي سيتم مناقشة موضعها ضمن القسم الذي يتحدث عن “ادارة الاتصال about Managing a Connection“.

يتوجب اغلاق BluetoothServerSocket  ما إن تنتهي من التنصت على الاتصالات القادمة incoming connections. على سبيل المثال, يتم استدعاء التابع close()ما إن يتم الحصول على BluetoothSocket.

ربما قد ترغب ايضا ان تضع تابع عام ضمن المسرى thread يستطيع ان يغلق private BluetoothSocket عند الحدث التي ترغب عنده بايقاف التنصت على server socket.

الاتصال كعميل Connecting as a client

لكي تبدأ الاتصال initiate a connection  مع جهاز عن بعد ( مع جهاز يستحوز على open server socket), يتوجب عليك في البداية ان تحصل على غرض BluetoothDevice  الذي يمثل جهاز عن بعد.

(تمت تغطية موضوع الحصول على BluetoothDevice   ضمن المقطع “ايجاد الاجهزة Finding Devices. اعلاه).

ومن ثم يجب استخدام BluetoothDevice  بهدف الحصول على  BluetoothSocket  وبدء الاتصال.

فيما يلي الاجرائية الاساسية :

  1. باستخدام BluetoothDevice, يتم الحصول على BluetoothSocket  عبر استدعاء التابع createRfcommSocketToServiceRecord(UUID).
    يقوم هذا بتهيئة BluetoothSocket  التي سوف تقوم بالاتصال ب BluetoothDevice.
    إن UUID الممرر هنا يجب ان يطابق ال UUID المستخدم من قبل الجهاز المخدم server device عندما يفتح BluetoothServerSocket  الخاصة به(عبر listenUsingRfcommWithServiceRecord(String, UUID)).

إن عملية استخدام نفس ال UUID هي ببساطة عملية تثبيت قيمة السلسلة المحرفية ل UUID ضمن تطبيقك ومن ثم الاشارة إليها من ترميزي (كودي) المخدم server  والعميل client.

  • بدء الاتصال عبر استدعاء التابع connect().
    عند هذا الاستدعاء, سيقوم النظام بتنفيذ عملية بحث SDP lookup على الجهاز الاخر بهدف المطابقة بين UUID. في حال كانت نتيجة البحث ناحجة , وقبل الجهاز الاخر عن بعد طلب الاتصال , عندها فإنه سوف يقوم بمشاركة قناة RFCOMM مع الجهاز المرسل لطلب الاتصال (الجهاز العميل) بهدف الاستخدام اثناء الاتصال , وسوف يتم اعادة connect().
    يعتبر هذا الاستدعاء بمثابة استدعاء حظر blocking call, في حال-  لاي سبب من الاسباب-  فشل الاتصال , او انتهى وقت التابع connect() (بعد 12 ثانية), عندها فإنه سوف يلقي استثناء exception.

وبما ان التابع عبارة عن استدعاء حظر blocking call, لذلك فإن اجراء الاتصال هذا يجب ان يتم دوما ضمن مسرى منفصل separate thread بعيدا عن مسرى الفعالية الاساسية.

ملاحظة : يجب دوما التاكد من ان الجهاز لا ينفذ عملية استكشاف للاجهزة اثناء استدعاء التابع connect() .
في حال كانت عملية البحث جارية , عندها فإن محاولات الاتصال سوف تتباطئ بشكل ملحوظ  ونسبة احتمال فشلها كبيرة.

مثال :

فيما يلي مثال على مسرى يقوم بتهيئة اتصال بلوتوث:

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final
        BluetoothSocket tmp = null;
        mmDevice = device;

        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();

        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }

        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }

    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

لاحظ بأن استدعاء التابع cancelDiscovery()قد تم قبل انشاء الاتصال. يجب عليك دوما القيام بذلك قبل الاتصال , ومن الآمن استدعاءه حتى قبل التحقق الفعلي فيما اذا كان البحث عن الاجهزة جاريا ام لا( ولكن اذا كنت ترغب بالتحقق بالامكان استدعاء التابع isDiscovering().

 التابع manageConnectedSocket() عبارة عن تابع تخيلي ضمن تطبيقك الهدف منه هو تهيئة المسرى بهدف نقل المعطيات, وهذا ما ستتم مناقشته ضمن مقطع “ادارة الاتصال Managing a Connection“.

عندما تنتهي من BluetoothSocket, دوما قم باستدعاء التابع close()بهدف التنظيف. ان ذلك يفعّل القيام بشكل اوتوماتيكي باغلاق كل ال connected socket وينظف كل الوارد الداخلية.

ادارة الاتصال Managing a Connection

عندما تحقق بنجاح الاتصال بين جهازين (او اكثر), عندها سيكون لدى كل جهاز من الاجهزة connected BluetoothSocket. هنا تبدأ الحكاية , حيث يصبح بالامكان مشاركة المعطيات بين الاجهزة.

إن الاجرائية العامة لنقل معطيات عشوائية بين الاجهزة بسيطة , وتتم عبر الاستفادة من BluetoothSocket.

  1. الحصول على InputStream  و OutputStream   بهدف التعامل مع عمليات النقل عبر ال socket, عبر  getInputStream(), وgetOutputStream() بالترتيب.
  2. قراءة وكتابة المعطيات للمجاري streams عبر استخدام كل من التابعين read(byte[]) وwrite(byte[]) .

هذا هو كل شيء !!

طبعا هنالك تفاصيل لها علاقة بتنجيز الموضوع يجب ان تاخذ بعين الاعتبار.
اولا وقبل كل شيء, يجب ان تستخدم مسرى مخصص dedicated thread لكل مجاري القراءة والكتابه هذه .هذا مهم جدا لأن كل من تابعي read(byte[])write(byte[])يعتبران استدعاءات حظر.

  read(byte[])سوف يقوم بالحظر حتى يجد شيء يقوم بقرائته من المجرى stream.

write(byte[])لا يقوم بالحظر عادة , ولكن يمكن ان يقوم بحظر متحكمات التدفق في حال لم يكن الجهاز الاخر يستدعي التابع read(byte[])بالسرعة الكافية , وكانت ال buffers الوسيطة ممتلأة.

اذن الحلقة الاساسية ضمن المسرى thread يجب ان تكون مخصصة للقراءة من InputStream.

ويمكن استخدام تابع عام منفصل بهدف بدء الكتابة من OutputStream.

مثال :

فيما يلي مثال يوضح الفكرة :

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()

        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }

    /* Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }

    /* Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

يستحصل الباني على المجاري streams  الضرورية , وما ان يتنفذ, حتى يبدأ المجرى thread بانتظار المعطيات القادمة عبر InuptStream.

عندما يعود التابع read(byte[])مع البايتات القادمة من المجرى stream , عندها يتم ارسال المعطيات إلى الفعالية الاساسية باستخدام مرجع عضو  member Handler ضمن الصف الاب. ومن ثم يعود وينتظر المزيد من البايتات القادمة من المسرىstream.

ان عملية ارسال المعطيات الصادرة عملية سهلة جدا , تتم ببساطة عبر استدعاء التابع write() ضمن المسرى من الفعالية الاساسية ونمرر عبره البايتات التي نرغب بارسالها.

ومن ثم يستدعي التابع write(byte[]) لارسال المعطيات إلى الجهاز عن بعد.

يعتبر التابع cancel() الخاص بالمسرى مهم جدا لانهاء الاتصال في أي وقت عبر اغلاق BluetoothSocket. يجب دوما استدعاء هذا التابع عندما تنتهي من استخدام اتصال البلوتوث.

للحصول على ديمو حول استخدام واجهات بلوتوث APIs , بالامكان مراجعة الرابط التالي: Bluetooth Chat sample app.

التعامل مع البروفايل Working with Profile

انطلاقا من نسخة اندرويد 3.0 , تضمنت واجهات بلوتوث دعم للتعامل مع بروفايلات بلوتوث.

بروفايل بلوتوث Bluetooth profile: عبارة عن مواصفات وخصائص لواجهة لاسلكية wireless interface specification  خاصة بالاتصالات بين الاجهزة التي تعتمد على بلوتوث .

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

وإلى لقاء قريب في الحلقة القادمة , وإلى ذلك الحين استودعكم الله والسلام عليكم ورحمة الله وبركاته

الترجمة

المصطلح

مخدم

Server

زبون

Client

اتصال

Connection

مجاري(تدفقات) دخل أو خرج

input/output streams

مسرى

thread

استثناء

exception

استدعاء حظر

blocking call

بروفايل

Profile

طرف المخدم

Server-side

طرف العميل

Client-side

, , , , , , , , , , , , , , , , , , , , , , , , , ,

أضف تعليق