نحوه ساخت Progress Bar در اندروید
اکثر برنامه هایی که امروزه نوشته میشوند، شامل پردازش های زمانبری هستند. برای مثال پردازش یک عکس یا جستجو در فایل ها یا اتصال به اینترنت و وب سرویس و … از این قبیل هستند. یک برنامهی اصولی با رابط کاربری صحیح باید در اینگونه فرآیندها کاربر را با یک progress از میزان پیشرفت کار آگاه سازد و نیز او را سرگرم کند.
در صفر تا قهرمان با ما همراه باشید تا ساخت یک circular progress را از صفر تا صد بیاموزیم.
شناخت view
در اندروید هر چیزی که در صفحه، نمایش داده میشود یک view میباشد. یعنی یا از جنس view است یا از کلاس view ارث برده است. view ها مانند activity ها یا fragment ها و دیگر اجزای اندروید شامل یک چرخهی زندگی (lifecycle) هستند.
اولین متدی (method) که در هر کلاس جاوایی صدا زده میشود متد سازنده (constructor) است. در کلاس view، فایل xml با inflate به کلاس جاوایی تبدیل میشود و یا به صورت مستقیم (با ساختن شی جدید) متد سازنده صدا زده میشود. سپس کلاس view به activity یا fragment متصل میشود. در مرحله بعد متد measure صدا زده میشود که وظیفه مشخص کردن ابعاد را دارد. در واقع عمل تعیین ابعاد در نهایت با صدا زدن onMeasure امکان پذیر است. از این رو در ارث بری از view، متد onMeasure قابلیت بازنویسی (override) را دارد. در متد layout مکان لایه و رسم ابعاد اندازه گیری شده در متد measure را برای لایه پدر و فرزندان داریم و متد onLayout نیز برای بازنویسی در ارث بری ها استفاده میکنیم. در مرحلهی بعد با متد dispatchDraw آمادهی رسم محتویات داخلی view میشویم و در متد onDraw با شی canvas میتوان دستوراتی را برای رسم به openGL ES فرستاد تا تحت GPU رسم شود. عملکرد view ها پیچیده تر از این توضیحات است ولی با توجه به موضوع مقاله از توضیح بیشتر خودداری میکنیم.
متدهای دیگری داریم که وظیفهی restart کردن view را دارند.
۱- invalidate : برای اجرای دوبارهی متد onDraw بکار میرود.
۲- reaquestLayout : برای restart کردن view (اجرای هر ۳ متد measure , layout , draw) بکار میرود.
در این مقاله قصد داریم شکلی که در تصویر زیر میبینید را در اندروید پیاده سازی کنیم.
ساخت کلاس
در مرحله اول یک کلاس جاوا با نام ZHProgress.java میسازیم و از کلاس ImageView ارث میبریم و متدهای سازنده آن را مینویسیم. متد init را میسازیم و درون هر ۴ متد سازنده آن را فراخوانی میکنیم.
private void init() { spinPaint = new Paint(); spinPaint.setAntiAlias(true); spinPaint.setStyle(Paint.Style.STROKE); backPaint = new Paint(spinPaint); backPaint.setColor(backColor); }
برای رسم هر چیزی در canvas ما نیاز داریم که یک شی از کلاس paint را به canvas معرفی کنیم، که سبک، رنگ و دیگر خصوصیات شکل را در آن تعریف میکنیم. از آنجایی که ما یک کمان تکه تکه شده به رنگ خاکستری داریم و یک کمان که به رنگ دلخواه به روی آن به صورت چرخشی حرکت میکند، پس به ۲ paint مختلف برای رسم این ۲ شکل نیاز داریم. شی spinPaint برای شکل در حال چرخش و شی backPaint برای شکل ثابت پس زمینه بکار میرود. متد setAntiAlias را با مقدار true تنظیم میکنیم تا در رسم، لبه های شکل نرمتر باشند. سبک stroke را نیز برای آنها تنظیم میکنیم چون ما فقط حاشیهی دایره را نیاز داریم و به دایرهی توپر (سبک fill) نیاز نداریم.
متد onMeasure
در مرحله بعد میخواهیم لایهی ما همیشه طول و عرض یکسانی داشته (مربع) باشد، از این رو متد onMeasure را بازنویسی میکنیم و کد زیر را درون آن قرار میدهیم.
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int minDimension = width < height ? width : height; setMeasuredDimension(minDimension, minDimension); }
با استفاده از متد getSize از کلاس MeasureSpec ، طول و عرض لایه را میگیریم و بعد از محاسبهی کمترین بعد، آن را با متد setMeasuredDimension بر روی لایه تنظیم میکنیم.
محاسبات ثابت
برای ساخت انیمیشن در اندروید، باید متد onDraw بارها فراخوانی شود که این کار با متد invalidate فراهم میشود. این فرآیند با سرعت بسیار زیادی انجام میشود تا چشم ما یک انیمیشین ببیند. برای اینکه onDraw سرعت بیشتری داشته باشد، سعی میکنیم که کد هایی که فقط یکبار نیاز به ساخته شدن ندارند را از این متد خارج کرده و در متد onSizeChanged قرار دهیم. پس متد onSizeChanged را بازنویسی میکنیم و کد زیر را درون آن مینویسیم.
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); cx = w / 2; cy = h / 2; radius = (int) (cx * 0.8f); circle = new RectF(cx - radius, cy - radius, cx + radius, cy + radius); int color[] = {startColor, endColor}; float angle[] = {0.6f, 1.0f}; gradient = new SweepGradient(cx, cy, color, angle); matrix = new Matrix(); int stroke = radius / 5; backPaint.setStrokeWidth(stroke); spinPaint.setStrokeWidth(stroke); segmentWith = radius / 108.0f; }
در این متد ابتدا مرکز کمان را بدست میآوریم. برای این کار طول و عرض را بر ۲ تقسیم میکنیم تا مرکز x و y را بدست آوریم. در اینجا چون طول و عرض یکسان است میتوان تنها یکی از ابعاد را بر ۲ تقسیم کرد (برای درک بهتر هر دو را تقسیم میکنیم). حال شعاع دایره را به اندازه ۰.۸ فاصله مرکز از طرفین قرار میدهیم. برای رسم کمان در canvas ما باید اندازه کمان را در شی کلاس RectF تنطیم کرده و به canvas بدهیم. برای اینکه شکل ما دایره شود باید به این صورت شی RectF را مقدار دهی کنیم. در پارامتر اول cx-radius را قرار میدهیم، در پارامتر دوم cy-radius را قرار میدهیم، در پارامتر سوم cx+radius و در پارامتر چهارم cy+radius را قرار میدهیم. کمان رنگی ای که روی کمان خاکستری در حال چرخش است، دارای یک sweepGradient است که رنگ آغازی آن با transparent و رنگ پایانی آن با رنگ دلخواه ما ساخته شده است و یک matrix روی آن تنظیم شده که با استفاده از آن میتوان گرادینت را چرخاند. در setStrokeWidth با توجه به کد، پهنای حاشیه paint را نیز تنظیم میکنیم.
متد onDraw
حال برای کشیدن شکل مورد نظر باید متد onDraw را بازنویسی کنیم. برای رسم کمان خاکستری رنگ ما باید به اندازهی ۳۶۰ درجه تقسیم بر segmentWidth ، تعداد تکه هایی که باید ساخته شود را محاسبه میکنیم و حلقه به اندازهی تعداد تکه ها اجرا میشود ولی ما یکی در میان کمان ها را رسم میکنیم. در متد drawArc شی RectF، زاویهی شروع، مقدار زاویهی کمان (sweepAngle) ، رسم از مرکز کمان (useCenter) و در نهایت شی backPaint را به ترتیب وارد میکنیم. همین روند را برای کمان چرخشی نیز بکار میبریم با این تفاوت که matrix را با زاویهی متفاوتی میچرخانیم و این باعث میشود که ما چرخش progress را ببینیم.
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int segments = (int) Math.ceil(360 / segmentWith); matrix.setRotate(startAngle, cx, cy); gradient.setLocalMatrix(matrix); spinPaint.setShader(gradient); for (int i = 0; i < segments; i++) { if (i % 2 == 0) { canvas.drawArc(circle, i * (segmentWith * 2), segmentWith, false, backPaint); canvas.drawArc(circle, i * (segmentWith * 2), segmentWith, false, spinPaint); } } }
حال باید matrix مورد نظر را بچرخانیم. برای این کار به یک thread نیاز داریم تا با توجه به سرعت ما آن را بچرخاند. کد زیر را وارد میکنیم.
public void startProgress() { stopProgress = false; new Thread(() -> { Handler mainHandler = new Handler(Looper.getMainLooper()); while (!stopProgress) { mainHandler.post(() -> { setStartAngle(startAngle); }); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } startAngle += 4; } }).start(); }
ما یک thread جدید ایجاد میکنیم و در آن یک حلقهی while با یک مقدار بولی قرار میدهیم که بعدا در متدی دیگر میتوانیم آن را متوقف کنیم. سپس فیلد startAngle را با متد setStartAngle مقدار دهی میکنیم و در اینجا متد invalidate نیز درون setStartAngle صدا زده میشود. تغییر در view ها باید در thread اصلی انجام شود، برای همین یک handler با متد سازندهای با ورودی Looper.getMainLooper میسازیم و زاویه را به وسیله این handler به thread اصلی پست میکنیم و بعد به مدت sleepTime به thread جاری وقفه میدهیم و سپس زاویه را ۴ درجه افزایش میدهیم.
مفهوم fps
سرعت چرخش به مقدار زمان وفقه یعنی sleepTime وابسته است. هرچه مقدار sleepTime بیشتر، سرعت کمتر است و بالعکس. در انیمیشن و فیلم، مقیاسی تحت عنوان fps (فریم بر ثانیه frame per second) داریم؛ یعنی در هر ثانیه چند تصویر در قاب تغییر میکنند. درکد زیر فرمول تبدل fps به ثانیه را داریم.
public void setSpeed(int fps) { if (fps > 200) fps = 200; this.sleepTime = 1000 / fps; }
حداکثر سرعت ۲۰۰fps است و بیش از این اجازه نمیدهیم.
کد کامل این کلاس را میتوانید از اینجا ببینید.
امیدوارم از این آموزش لذت برده باشید و بتوانید progress های زیبایی را برای خودتان بسازید.
دیدگاهتان را بنویسید
برای نوشتن دیدگاه باید وارد بشوید.