آموزش برنامه نویسی و ساخت ویجت شناور در اندروید + پروژه نمونه

آموزش برنامه نویسی و ساخت ویجت شناور در اندروید + پروژه نمونه

آموزش برنامه نویسی و ساخت ویجت شناور در اندروید
به همراه پروژه نمونه
سورس کد در انتهای مطلب قرار دارد


ویجت شناور، یک یا چند View می‌باشد که می‌تواند روی صفحه‌ی نمایش، آزادانه و غیر وابسته به سایر عناصر، در هرجایی قرار گیرد! همچنین ممکن است قابلیت درگ کردن و انتقال دادن این ویو با تاچ کردن به این طرف و آن طرف نیز بری آن وجود داشته باشد!

کاربردهای مختلفی را می‌توان برای ویجت شناور عنوان کرد؛ از جمله زمانی که پیامی از مسنجرها دریافت می‌شود، ویجت باز شده تا اگر در جای دیگری باشید، به راحتی بتوانید پاسخ دهید! و اما حالا در این آموزش، مثال ما ساخت یک کنترل موزیک است که روی صفحه شناور شده و کاربر می‌تواند آن را به این طرف و آن طرف انتقال دهد و با استفاده از آن، موزیک خود را پخش کند. به صورت زیر:

1. ایجاد پروژه

ویوی شناور به خودی خود چیزی نیست! اما برای این که بتواند روی اپلیکیشن‌های دیگر و همه جا نمایش داده شود، باید دسترسی زیر را داشته باشد:

android.permission.SYSTEM_ALERT_WINDOW

سپس، از یک بک‌گراند سرویس برای اضافه کردن ویجت شناور به view hierarchy یا سلسه‌ی ویوهای اسکرین فعلی اندروید استفاده می‌کنیم. بنابراین، این ویو، همیشه بالاتر از پنجره‌ی اپلیکیشن‌ها خواهد بود.

برای شروع کار، وارد اندروید استودیو شده و یک پروژه بسازید. سپس فایل اندروید منیسفت خود را به صورت زیر تکمیل کنید:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.fullkade.floatingview">
     
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name="com.fullkade.floatingview.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
 
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
 
        <service
            android:name="com.fullkade.floatingview.FloatingViewService"
            android:enabled="true"
            android:exported="false"/>
    </application>
 
</manifest>

همانطور که می‌بینید، تنها یک دسترسی و یک سرویس را در این‌جا معرفی کرده‌ایم. این دسترسی، جزء دسترسی‌های خطرناک از اندروید مارشمالو به بعد نیست؛ اما نیاز به اجازه‌ی کاربر دارد؛ این اجازه از جای دیگری غیر از روش گرفتن دسترسی‌های خطرناک داده می‌شود؛ که در ادامه و موقع نوشتن کد اکتیویتی خواهیم گفت.


حالا یک Layout با نام «layout_floating_widget.xml» بسازید. در این Layout، ما ویو یا ویوهای شناور خود را قرار می‌دهیم تا با استفاده از آن، ویجت شناور خود را بسازیم:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:id="@+id/root_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="UselessParent">

        <RelativeLayout
            android:id="@+id/collapse_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:visibility="visible">

            <ImageView
                android:id="@+id/collapsed_iv"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_marginTop="8dp"
                android:src="@drawable/ic_android_circle"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/exit_btn"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_marginLeft="40dp"
                android:layout_marginStart="40dp"
                android:src="@drawable/ic_close"
                tools:ignore="ContentDescription" />
        </RelativeLayout>

        <LinearLayout
            android:id="@+id/expanded_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#F8BBD0"
            android:orientation="horizontal"
            android:padding="8dp"
            android:visibility="gone">

            <ImageView
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:src="@drawable/music_player"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/prev_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:layout_marginStart="20dp"
                android:src="@mipmap/ic_previous"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/play_btn"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:layout_marginStart="10dp"
                android:src="@mipmap/ic_play"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/next_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:layout_marginStart="10dp"
                android:src="@mipmap/ic_play_next"
                tools:ignore="ContentDescription,RtlHardcoded" />

            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <ImageView
                    android:id="@+id/close_button"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:src="@drawable/ic_close" />

                <ImageView
                    android:id="@+id/open_activity_button"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_alignParentBottom="true"
                    android:src="@drawable/ic_open" />
            </RelativeLayout>
        </LinearLayout>
    </RelativeLayout>
</FrameLayout>

نیازی به توضیح نیست و تنها یک طراحی ساده‌ای برای ویجت شناور موزیک پلیرمان می‌باشد که در ادامه از آن استفاده خواهیم کرد. البته شاید تنها مواردی که در طراحی بالا نیاز به توضیح دارند، این است که ما یک ویو با نام collapse_view و یک ویوی دیگر با نام expanded_container داریم:

  • collapse_view
    شامل یک آیکون + دکمه‌ی بستن ویجت می‌باشد.
  • expanded_container
    شامل دکمه‌های پخش موزیک و … می‌باشد.

به صورت پیشفرض، محتوای داخل collapse در حال نمایش است و کاربر با کلیک روی آیکون آن، محتوای expanded را مشاهده خواهد کرد و در نتیجه باید گفت که موقع کلیک روی collapse، ویوی expended باز می‌شود (به بیانی دیگر نمایش داده می‌شود) و موقع کلیک روی دکمه‌ی ضربدری (بستن) که داخل ویوی expended قرار دارد، expended بسته شده و collapse نمایش داده می‌شود! (اگر به گیف موجود در ابتدای مطلب دقت کنید، این توضیح را درک خواهید کرد.)

2. پیاده سازی سرویس و ویجت شناور

یک کلاس با نام  FloatingViewService.java که قرار است شامل کد سرویس معرفی شده در Manifest باشد را ساخته و آن را به صورت زیر تکمیل کنید:

package com.fullkade.floatingview;

import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;

public class FloatingViewService extends Service {

    private WindowManager windowManager;
    private View          floatingWidget;

    public FloatingViewService() {

    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @SuppressLint({"RtlHardcoded", "InflateParams"})
    @Override
    public void onCreate() {
        super.onCreate();

        // 1
        floatingWidget = LayoutInflater.from(this).inflate(R.layout.layout_floating_widget, null);

        // 2
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT
        );
        params.gravity = Gravity.TOP | Gravity.LEFT;
        params.x = 0;
        params.y = 100;

        // 3
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        windowManager.addView(floatingWidget, params);

        // 4
        final View collapsedView = floatingWidget.findViewById(R.id.collapse_view);
        final View expandedView  = floatingWidget.findViewById(R.id.expanded_container);

        // 5 -> Exit
        ImageView exitButton = (ImageView) floatingWidget.findViewById(R.id.exit_btn);
        exitButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                stopSelf();
            }
        });

        // 6
        ImageView playButton = (ImageView) floatingWidget.findViewById(R.id.play_btn);
        playButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingViewService.this, "Playing the song.", Toast.LENGTH_LONG).show();
            }
        });

        // 7
        ImageView nextButton = (ImageView) floatingWidget.findViewById(R.id.next_btn);
        nextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingViewService.this, "Playing next song.", Toast.LENGTH_LONG).show();
            }
        });

        // 8
        ImageView prevButton = (ImageView) floatingWidget.findViewById(R.id.prev_btn);
        prevButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingViewService.this, "Playing previous song.", Toast.LENGTH_LONG).show();
            }
        });

        // 9 -> Collapse
        ImageView closeButton = (ImageView) floatingWidget.findViewById(R.id.close_button);
        closeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(FloatingViewService.this, "///", Toast.LENGTH_LONG).show();
                collapsedView.setVisibility(View.VISIBLE);
                expandedView.setVisibility(View.GONE);
            }
        });

        // 10 -> Close Floating Widget and back to activity
        ImageView openActivityButton = (ImageView) floatingWidget.findViewById(R.id.open_activity_button);
        openActivityButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(FloatingViewService.this, MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
                stopSelf();
            }
        });

        // 11 -> Drag and Drop
        floatingWidget.findViewById(R.id.root_container).setOnTouchListener(new View.OnTouchListener() {

            private int initialX;
            private int initialY;
            private float initialTouchX;
            private float initialTouchY;

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        initialX = params.x;
                        initialY = params.y;
                        initialTouchX = event.getRawX();
                        initialTouchY = event.getRawY();
                        return true;
                    case MotionEvent.ACTION_UP:
                        int xDiff = (int) (event.getRawX() - initialTouchX);
                        int yDiff = (int) (event.getRawY() - initialTouchY);
                        if (xDiff < 10 && yDiff < 10) {
                            if (isViewCollapsed()) {
                                collapsedView.setVisibility(View.GONE);
                                expandedView.setVisibility(View.VISIBLE);
                            }
                        }
                        return true;
                    case MotionEvent.ACTION_MOVE:
                        params.x = initialX + (int) (event.getRawX() - initialTouchX);
                        params.y = initialY + (int) (event.getRawY() - initialTouchY);
                        windowManager.updateViewLayout(floatingWidget, params);
                        return true;
                }
                return false;
            }
        });
    }

    private boolean isViewCollapsed() {
        return floatingWidget == null || floatingWidget.findViewById(R.id.collapse_view).getVisibility() == View.VISIBLE;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (floatingWidget != null) windowManager.removeView(floatingWidget);
    }
}

قرار است به محض اجرای سرویس، ویجت شناور نمایش داده شود! بنابراین، کار نمایش را در داخل متد onCreate سرویس انجام می‌دهیم. (البته می‌توانید به گونه‌های دیگری عمل کرده و هیچکدام از این‌ها اجباری نیستند!)

اما در اینجا، متد onCreaete را طبق شماره گزاری هایی که در کد انجام داده‌ام، به صورت زیر توضیح می‌دهم:

  1. محتوای فایل Layout مربوط به ویجت شناور خود را به داخل یک ویو و با استفاده از LayoutInflater تزریق می‌کنیم تا بعدا آن را به ویندوز منیجر اضافه کنیم.
  2. اطلاعات و ویژگی‌هایی که می‌خواهیم ویجتمان با آن ساخته شود را در متغیر params نگه می‌داریم!
    – این اطلاعات شامل اندازه، محل قرار گیری و … هستند.
  3. ویندوز منیجر خود سیستم را به فیلد windowsManager که در ابتدای کلاس تعریف کرده‌ایم نسبت می‌دهیم و سپس ویوی ویجت خود را به داخل آن اضافه می‌کنیم.
  4. دو ویوی collapse_view و expanded_container (که در بالا گفتیم) را از ویجت خود گرفته و درون متغیری قرار می‌دهیم تا بعدا یکی را ظاهر کرده و دیگری را غیب کنیم.
  5. رویداد کلیک ویوی exit_btn که در collapse_view قرار دارد را پیاده‌سازی کرده و از آن برای بستن ویجت یا خارج شدن از ویجت استفاده می‌کنیم.
    – توجه داشته باشید که برای خارج شدن از ویجت، سرویس را متوقف می‌کنیم و به داخل رویداد onDestroy سرویس رفته و در آنجا ویجت را از ویندوز منجر حذف می‌کنیم! بنابراین هم سرویس بسته خواهد شد و هم ویجت!
  6. در بخشهای 6 و 7 و 8، دکمه‌هایی که در ویوی expand قرار دارند و برای پخش موزیک و جلو عقب کردن آن مورد استفاده قرار می‌گیرند را پیاده‌سازی می‌کنیم.
    – البته فعلا چون هدف ما آموزش مدیا پلیر نیست و ساخت چنین سیستمی به خودی خود پیچیده است، تنها یک Toast ساده نمایش می‌دهیم و خودتان بعدا این قسمت‌ها را اگر خواستید با یک مدیاپلیر ساده تکمیل کنید.
  7. توضیح 6 را بخوانید.
  8. توضیح 6 را بخوانید.
  9.  از ویویی که داخل ویوی expand قرار دارد، برای رفتن به حالت collapse استفاده می‌کنیم و بنابراین این ویو را مخفی کرده و ویوی دیگر را ظاهر می‌کنیم. البته در مورد ظاهر کردن ویوی expand در ادامه خواهم گفت.
  10. ویجت را به کلی بسته و به اکتیویتی می‌رویم! چراکه اگر موزیک پلیرهای شناور را دیده باشید، به هنگام بازگشت به اکتیویتی از طریق ویجت شناور، ادامه‌ی موزیک در اکتیویتی پخش می‌شود! البته این کار را می‌توانید توسط سرویسی برای پخش صدا انجام دهید تا وابستگی از بین برود (می‌توانید از همین سرویس هم استفاده کنید!)
  11. در اینجا باید «درگ اند دراپ» یا به عبارتی انتقال ویجت شناور با کشیدن انگشت به این طرف و آن طرف بپردازیم! همچنین تصمیم گرفته‌ایم تا رفتن به حالت expand را نیز در اینجا انجام دهیم. بدین منظور، ما ریشه‌ای‌ترین ویویی که تمام کنترل‌ها را در بر دارد را انتخاب کرده و رویداد تاچ آن را پیاده‌سازی می‌کنیم.
    – توضیح این قسمت پیچیده است چرا که اعمال ریاضی برای این کار صورت می‌گیرد و من فقط بخش رفتن به حالت expand را توضیح می‌دهم.
    – از آن‌جایی که در حالت درگ کردن و زمان collapse بودن تنها ویوهای آیکون و بستن ویجت را داریم، بنابراین وقتی روی آیکون رویداد کلیک را تعریف می‌کردیم تا به جالت expand برود، در این صورت موقع درگ شدن هم به حالت expand می‌رفت که خیلی ناخوش‌آیند بود! بنابراین رفتن به حالت expand را در حالت درگ کردن و با بررسی اینکه در حال حرکت هستیم یا خیر انجام داده‌ایم. از اینرو، موقع رها کردن حالت تاچ (رویداد MotionEvent.ACTION_UP)، با برررسی این مورد می‌توانیم به حالت expand برویم! قبل از آن نیز بررسی می‌کنیم که آیا در حالت collapse قرار داریم یا خیر؟! چراکه تاج و درگ کردن روی حالت expand هم انجام می‌شود و از اینرو، ابتدا بررسی می‌کنیم و سپس این کار را انجام می‌دهیم.
    سایر موارد را خودتان بررسی کنید.

3. نهایی سازی پروژه و پیاده سازی اکتیویتی

کد زیر را در Layout مربوط به اکتیویتی خود وارد کنید:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="info.androidhive.floatingview.MainActivity">

    <Button
        android:id="@+id/notify_me"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Create Floating Widget" />
</RelativeLayout>

چیز خاصی وجود ندارد! تنها یک دکمه برای باز کردن و ایجاد کردن ویجت شناور قرار داده‌ایم تا با کلیک روی آن، ویجت شناور برایمان ظاهر شده و اکتیویتی نیز بسته شود.


کد زیر را درون فایل MainActivity.java قرار دهید:

package info.androidhive.floatingview;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private static final int CODE_DRAW_OVER_OTHER_APP_PERMISSION = 2084;

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

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                                       Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, CODE_DRAW_OVER_OTHER_APP_PERMISSION);
        } else {
            initializeView();
        }
    }

    private void initializeView() {
        findViewById(R.id.notify_me).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startService(new Intent(MainActivity.this, FloatingViewService.class));
                finish();
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == CODE_DRAW_OVER_OTHER_APP_PERMISSION) {

            if (resultCode == RESULT_OK) {
                initializeView();
            } else {
                Toast.makeText(this,
                               "Draw over other app permission not available. Closing the application",
                               Toast.LENGTH_SHORT).show();

                finish();
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
}
نکته
توجه داشته باشید که از نسخه‌ی مارشمالو به بعد، وقتی یک اپلیکیشنی بخواهد از این قابلیت استفاده کند، موقع باز شدن به تنظیمات انتقال داده خواهد شد و در آنجا بایستی این قابلیت را برای برنامه فعال کند! لذا از این رو، ما در این جا این امکان را برای نسخه‌های مختلف تفکیک کرده‌ایم.

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

cp-codfk

نظرات ثبت شده 5 دیدگاه

    1. صدرا کاربر مهمان گفت:

      اگه بخوام داخل ویجتم یه دکمه بزارم که با کلیک روش یه کاری انجام بده اون موقع باید چی کار کنم

      21
      1. هادی اکبرزاده مدیر سایت گفت:

        سلام. خب تو همین آموزش به این مورد اشاره شده. 🙂

    2. محمد کاربر مهمان گفت:

      با سلام و تشکر بابت مطالب مفیدی که گذاشتید
      برنامه یه ایراد جزئی داره که باعث میشه روی اندروید 8 به بالا اجرا نشه برای رفع این مشکل در کلاس FloatingViewService.java قسمت

       // 2
              final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                      WindowManager.LayoutParams.WRAP_CONTENT,
                      WindowManager.LayoutParams.WRAP_CONTENT,
                      WindowManager.LayoutParams.TYPE_PHONE,
                      WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                      PixelFormat.TRANSLUCENT
              );
              params.gravity = Gravity.TOP | Gravity.LEFT;
              params.x = 0;
              params.y = 100;
      

      باید به صورت زیر اصلاح بشه

      // 2 
              int layoutType;
              if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                  layoutType = WindowManager.LayoutParams.TYPE_PHONE;
              } else {
                  layoutType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
              }
      
              final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                      WindowManager.LayoutParams.WRAP_CONTENT,
                      WindowManager.LayoutParams.WRAP_CONTENT,
                      layoutType,
                      WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                      PixelFormat.TRANSLUCENT);
              params.gravity = Gravity.TOP | Gravity.LEFT;
              params.x = 0;
              params.y = 100;
      
      11
      1. هادی اکبرزاده مدیر سایت گفت:

        سلام ممنون از اطلاع رسانی شما دوست عزیز 🙂
        بررسی میشه

    3. علی کاربر مهمان گفت:

      سلام.ممنون از سایت فوق العادتون.آقا من خط به خط کدایی که شما نوشتیدو کپی کردم و خروجی گرفتم و برنامه درست اجرا میشه ولی وقتی روی دکمه فعال شدن ویجت کلیک میکنم از برنامه خارج میشه و ویجت روی صفحه گوشیم نمیاد.نمیدونم باید چیکار کنم توروخدا زودتر جواب بدین یا اگه ممکنه فایل سورس برنامه رو برای دانلود قرار بدین.بازم ممنون

      31
توضیحات پیشنهادی نظرات اشتراک